Merge pull request #66 from qinfendebingshuo/copilot/add-email-collection-feature
feat: persona-studio — self-evolution engine, 200k context injection, split-screen preview
This commit is contained in:
commit
5d117847af
|
|
@ -6,6 +6,9 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const modelRouter = require('./model-router');
|
||||
const knowledgeExtractor = require('./knowledge-extractor');
|
||||
const patternAnalyzer = require('./pattern-analyzer');
|
||||
const evolutionLogger = require('./evolution-logger');
|
||||
|
||||
const WORKSPACE_DIR = path.join(__dirname, '..', '..', 'workspace');
|
||||
|
||||
|
|
@ -31,6 +34,7 @@ async function generate({ dev_id, conversation }) {
|
|||
const requirements = extractRequirements(conversation);
|
||||
const projectName = 'project-' + Date.now();
|
||||
const projectDir = path.join(WORKSPACE_DIR, dev_id, projectName);
|
||||
const startTime = Date.now();
|
||||
|
||||
// 确保工作目录存在
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
|
@ -39,7 +43,9 @@ async function generate({ dev_id, conversation }) {
|
|||
|
||||
if (!apiKey) {
|
||||
// 无 API 密钥时生成模板项目
|
||||
return generateTemplate(projectDir, projectName, requirements);
|
||||
const result = generateTemplate(projectDir, projectName, requirements);
|
||||
triggerPostBuildEvolution(dev_id, conversation, result, startTime, true);
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -84,14 +90,47 @@ async function generate({ dev_id, conversation }) {
|
|||
files.push('README.md');
|
||||
}
|
||||
|
||||
return {
|
||||
const result = {
|
||||
projectName,
|
||||
files,
|
||||
summary: `项目 ${projectName} 已生成,包含 ${files.length} 个文件。`
|
||||
};
|
||||
triggerPostBuildEvolution(dev_id, conversation, result, startTime, true);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error('Code generation failed:', err.message);
|
||||
return generateTemplate(projectDir, projectName, requirements);
|
||||
const result = generateTemplate(projectDir, projectName, requirements);
|
||||
triggerPostBuildEvolution(dev_id, conversation, result, startTime, false);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目完成后触发自进化:知识提取 + 模式识别
|
||||
*/
|
||||
function triggerPostBuildEvolution(devId, conversation, result, startTime, success) {
|
||||
try {
|
||||
const buildTimeMs = Date.now() - startTime;
|
||||
|
||||
// 知识提取
|
||||
knowledgeExtractor.autoExtractKnowledge(devId, conversation, result.projectName);
|
||||
|
||||
// 模式识别
|
||||
const patterns = patternAnalyzer.analyzeAndUpdatePatterns(
|
||||
devId, conversation, result.files, buildTimeMs, success
|
||||
);
|
||||
|
||||
// 进化日志
|
||||
evolutionLogger.logEvent('build_complete', '项目构建完成: ' + result.projectName, {
|
||||
dev_id: devId,
|
||||
project: result.projectName,
|
||||
files_count: (result.files || []).length,
|
||||
build_time_ms: buildTimeMs,
|
||||
success: success,
|
||||
patterns_detected: patterns
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Post-build evolution error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* persona-studio · 进化日志记录器
|
||||
*
|
||||
* 功能:记录系统每一次自进化事件
|
||||
* 事件类型:profile_update / knowledge_extract / pattern_update /
|
||||
* model_benchmark / quality_score / system_init
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const EVOLUTION_LOG_PATH = path.join(BRAIN_DIR, 'evolution-log.json');
|
||||
|
||||
/**
|
||||
* 加载进化日志
|
||||
*/
|
||||
function loadEvolutionLog() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(EVOLUTION_LOG_PATH, 'utf-8'));
|
||||
} catch {
|
||||
return {
|
||||
schema_version: '1.0',
|
||||
description: '系统进化日志',
|
||||
last_updated: null,
|
||||
total_events: 0,
|
||||
events: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存进化日志
|
||||
*/
|
||||
function saveEvolutionLog(log) {
|
||||
log.last_updated = new Date().toISOString();
|
||||
log.total_events = log.events.length;
|
||||
fs.writeFileSync(EVOLUTION_LOG_PATH, JSON.stringify(log, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录进化事件
|
||||
* @param {string} type - 事件类型
|
||||
* @param {string} description - 事件描述
|
||||
* @param {object} metadata - 事件元数据
|
||||
*/
|
||||
function logEvent(type, description, metadata) {
|
||||
const log = loadEvolutionLog();
|
||||
|
||||
const event = {
|
||||
id: 'EVT-' + Date.now(),
|
||||
type: type,
|
||||
description: description,
|
||||
metadata: metadata || {},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
log.events.push(event);
|
||||
|
||||
// 保留最近 1000 条
|
||||
if (log.events.length > 1000) {
|
||||
log.events = log.events.slice(-1000);
|
||||
}
|
||||
|
||||
saveEvolutionLog(log);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录画像更新事件
|
||||
*/
|
||||
function logProfileUpdate(devId, fieldsUpdated) {
|
||||
return logEvent('profile_update', '用户画像更新: ' + devId, {
|
||||
dev_id: devId,
|
||||
fields_updated: fieldsUpdated || []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录知识提取事件
|
||||
*/
|
||||
function logKnowledgeExtract(devId, entriesCount, projectName) {
|
||||
return logEvent('knowledge_extract', '知识提取: ' + entriesCount + ' 条新知识', {
|
||||
dev_id: devId,
|
||||
entries_count: entriesCount,
|
||||
project: projectName || null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录模式更新事件
|
||||
*/
|
||||
function logPatternUpdate(patternNames, devId) {
|
||||
return logEvent('pattern_update', '模式识别: ' + (patternNames || []).join(', '), {
|
||||
dev_id: devId,
|
||||
patterns: patternNames || []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录质量评分事件
|
||||
*/
|
||||
function logQualityScore(devId, projectName, score) {
|
||||
return logEvent('quality_score', '质量评分: ' + projectName + ' = ' + score, {
|
||||
dev_id: devId,
|
||||
project: projectName,
|
||||
score: score
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录模型评测事件
|
||||
*/
|
||||
function logModelBenchmark(modelsCount, routingChanges) {
|
||||
return logEvent('model_benchmark', '模型评测完成: ' + modelsCount + ' 个模型', {
|
||||
models_count: modelsCount,
|
||||
routing_changes: routingChanges || []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近事件
|
||||
* @param {number} limit - 数量限制
|
||||
* @param {string} type - 可选,按类型过滤
|
||||
* @returns {Array} 事件列表
|
||||
*/
|
||||
function getRecentEvents(limit, type) {
|
||||
const log = loadEvolutionLog();
|
||||
let events = log.events;
|
||||
|
||||
if (type) {
|
||||
events = events.filter(function (e) { return e.type === type; });
|
||||
}
|
||||
|
||||
return events.slice(-(limit || 20)).reverse();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logEvent,
|
||||
logProfileUpdate,
|
||||
logKnowledgeExtract,
|
||||
logPatternUpdate,
|
||||
logQualityScore,
|
||||
logModelBenchmark,
|
||||
getRecentEvents,
|
||||
loadEvolutionLog
|
||||
};
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* persona-studio · 知识提取引擎
|
||||
*
|
||||
* 触发:代码生成完成时 / 用户确认方案时 / 解决技术问题时
|
||||
* 功能:从对话中提取有价值的知识条目存入 knowledge-base.json
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const KB_PATH = path.join(BRAIN_DIR, 'knowledge-base.json');
|
||||
|
||||
/**
|
||||
* 加载知识库
|
||||
*/
|
||||
function loadKnowledgeBase() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(KB_PATH, 'utf-8'));
|
||||
} catch {
|
||||
return {
|
||||
schema_version: '1.0',
|
||||
description: '系统知识库 · 自动积累',
|
||||
last_updated: null,
|
||||
total_entries: 0,
|
||||
entries: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存知识库
|
||||
*/
|
||||
function saveKnowledgeBase(kb) {
|
||||
kb.last_updated = new Date().toISOString();
|
||||
kb.total_entries = kb.entries.length;
|
||||
fs.writeFileSync(KB_PATH, JSON.stringify(kb, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对话中提取技术方案
|
||||
*/
|
||||
function extractSolutions(conversation, sourceExp, sourceProject) {
|
||||
const entries = [];
|
||||
const messages = conversation || [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg.role !== 'assistant') continue;
|
||||
const content = msg.content || '';
|
||||
|
||||
// 检测方案类关键词
|
||||
if (/技术方案|解决方案|实现方式|推荐.*方案|建议使用/i.test(content)) {
|
||||
entries.push({
|
||||
id: 'KB-' + Date.now() + '-' + i,
|
||||
type: 'solution',
|
||||
category: detectCategory(content),
|
||||
tags: extractTags(content),
|
||||
title: extractTitle(content),
|
||||
content: content.substring(0, 1000),
|
||||
source_exp: sourceExp,
|
||||
source_project: sourceProject || null,
|
||||
usage_count: 0,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// 检测最佳实践
|
||||
if (/最佳实践|best practice|推荐做法|正确.*方式/i.test(content)) {
|
||||
entries.push({
|
||||
id: 'KB-' + Date.now() + '-bp-' + i,
|
||||
type: 'best_practice',
|
||||
category: detectCategory(content),
|
||||
tags: extractTags(content),
|
||||
title: extractTitle(content),
|
||||
content: content.substring(0, 1000),
|
||||
source_exp: sourceExp,
|
||||
source_project: sourceProject || null,
|
||||
usage_count: 0,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// 检测常见陷阱
|
||||
if (/注意|陷阱|坑|避免|不要.*这样|常见错误|pitfall/i.test(content)) {
|
||||
entries.push({
|
||||
id: 'KB-' + Date.now() + '-pit-' + i,
|
||||
type: 'pitfall',
|
||||
category: detectCategory(content),
|
||||
tags: extractTags(content),
|
||||
title: extractTitle(content),
|
||||
content: content.substring(0, 1000),
|
||||
source_exp: sourceExp,
|
||||
source_project: sourceProject || null,
|
||||
usage_count: 0,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测技术类别
|
||||
*/
|
||||
function detectCategory(content) {
|
||||
const lower = content.toLowerCase();
|
||||
if (/react|vue|angular|前端|frontend|html|css|ui/i.test(lower)) return 'frontend';
|
||||
if (/node|express|api|后端|backend|server|数据库|sql/i.test(lower)) return 'backend';
|
||||
if (/docker|部署|deploy|ci|cd|运维|devops|nginx/i.test(lower)) return 'devops';
|
||||
if (/设计|design|ux|ui|交互|布局|layout/i.test(lower)) return 'design';
|
||||
if (/安全|security|auth|鉴权|加密|token/i.test(lower)) return 'security';
|
||||
if (/性能|performance|优化|缓存|cache/i.test(lower)) return 'performance';
|
||||
return 'general';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取标签
|
||||
*/
|
||||
function extractTags(content) {
|
||||
const tags = [];
|
||||
const techKeywords = [
|
||||
'javascript', 'typescript', 'python', 'react', 'vue', 'node',
|
||||
'express', 'html', 'css', 'sql', 'mongodb', 'redis', 'docker',
|
||||
'nginx', 'git', 'api', 'rest', 'graphql', 'websocket'
|
||||
];
|
||||
const lower = content.toLowerCase();
|
||||
techKeywords.forEach(function (kw) {
|
||||
if (lower.includes(kw)) tags.push(kw);
|
||||
});
|
||||
return tags.slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取标题(取第一行非空文本的前50字符)
|
||||
*/
|
||||
function extractTitle(content) {
|
||||
const lines = content.split('\n').filter(function (l) { return l.trim().length > 0; });
|
||||
if (lines.length === 0) return '未命名知识条目';
|
||||
const first = lines[0].replace(/^[#*\-\s]+/, '').trim();
|
||||
return first.length > 50 ? first.substring(0, 50) + '…' : first;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将提取的知识添加到知识库(自动去重)
|
||||
*/
|
||||
function addToKnowledgeBase(entries) {
|
||||
if (!entries || entries.length === 0) return;
|
||||
|
||||
const kb = loadKnowledgeBase();
|
||||
|
||||
entries.forEach(function (entry) {
|
||||
// 简单去重:同标题+同类型 = 已存在
|
||||
const exists = kb.entries.some(function (existing) {
|
||||
return existing.title === entry.title && existing.type === entry.type;
|
||||
});
|
||||
if (!exists) {
|
||||
kb.entries.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// 保留最近 500 条
|
||||
if (kb.entries.length > 500) {
|
||||
kb.entries = kb.entries.slice(-500);
|
||||
}
|
||||
|
||||
saveKnowledgeBase(kb);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询知识库(供 persona-engine 调用)
|
||||
* @param {string} query - 搜索关键词
|
||||
* @param {number} limit - 最大返回数量
|
||||
* @returns {Array} 匹配的知识条目
|
||||
*/
|
||||
function queryKnowledge(query, limit) {
|
||||
const kb = loadKnowledgeBase();
|
||||
if (!query || kb.entries.length === 0) return [];
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const keywords = lowerQuery.split(/[\s,,。.!??!]+/).filter(Boolean);
|
||||
|
||||
const scored = kb.entries.map(function (entry) {
|
||||
let score = 0;
|
||||
const entryText = ((entry.title || '') + ' ' + (entry.content || '') + ' ' + (entry.tags || []).join(' ')).toLowerCase();
|
||||
|
||||
keywords.forEach(function (kw) {
|
||||
if (entryText.includes(kw)) score += 1;
|
||||
});
|
||||
|
||||
// 使用次数加权
|
||||
score += (entry.usage_count || 0) * 0.1;
|
||||
|
||||
return { entry: entry, score: score };
|
||||
});
|
||||
|
||||
return scored
|
||||
.filter(function (s) { return s.score > 0; })
|
||||
.sort(function (a, b) { return b.score - a.score; })
|
||||
.slice(0, limit || 5)
|
||||
.map(function (s) {
|
||||
// 增加使用计数
|
||||
s.entry.usage_count = (s.entry.usage_count || 0) + 1;
|
||||
return s.entry;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心方法:自动提取知识
|
||||
*/
|
||||
function autoExtractKnowledge(devId, conversation, projectName) {
|
||||
const entries = extractSolutions(conversation, devId, projectName);
|
||||
if (entries.length > 0) {
|
||||
addToKnowledgeBase(entries);
|
||||
}
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
autoExtractKnowledge,
|
||||
queryKnowledge,
|
||||
loadKnowledgeBase,
|
||||
addToKnowledgeBase,
|
||||
extractSolutions
|
||||
};
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
/**
|
||||
* persona-studio · 记忆注入Agent
|
||||
*
|
||||
* 三步走机制:
|
||||
* Step 1 压缩:把长对话压缩成结构化摘要
|
||||
* Step 2 注入:每次调模型前,摘要注入 system prompt
|
||||
* Step 3 刷新:每10轮或token>50k时重新压缩
|
||||
*
|
||||
* 五层 system prompt 结构:
|
||||
* 第1层:人格体身份(固定·来自 persona-config.json)
|
||||
* 第2层:通感语言风格(固定)
|
||||
* 第3层:用户画像(来自 profile.json)
|
||||
* 第4层:记忆摘要(来自 compressed.json · ≤8k token)
|
||||
* 第5层:最近10轮原始对话(滑动窗口)
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const MEMORY_DIR = path.join(BRAIN_DIR, 'memory');
|
||||
const CONFIG_PATH = path.join(__dirname, 'model-config.json');
|
||||
|
||||
/**
|
||||
* 加载注入配置
|
||||
*/
|
||||
function loadInjectionConfig() {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
return config.memory_injection || getDefaultConfig();
|
||||
} catch {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultConfig() {
|
||||
return {
|
||||
enabled: true,
|
||||
compression_model: 'quick_reply',
|
||||
compression_trigger: {
|
||||
every_n_rounds: 10,
|
||||
token_threshold: 50000,
|
||||
force_on_session_start: true
|
||||
},
|
||||
injection_strategy: {
|
||||
system_prompt_max_tokens: 8000,
|
||||
sliding_window_rounds: 10,
|
||||
priority: ['confirmed_decisions', 'requirements', 'open_questions', 'user_preferences', 'emotional_signals']
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取滑动窗口大小
|
||||
*/
|
||||
function getSlidingWindowSize() {
|
||||
const config = loadInjectionConfig();
|
||||
return (config.injection_strategy && config.injection_strategy.sliding_window_rounds) || 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载压缩摘要
|
||||
*/
|
||||
function loadCompressed(devId) {
|
||||
const file = path.join(MEMORY_DIR, devId, 'compressed.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存压缩摘要
|
||||
*/
|
||||
function saveCompressed(devId, compressed) {
|
||||
const dir = path.join(MEMORY_DIR, devId);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'compressed.json'),
|
||||
JSON.stringify(compressed, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载注入日志
|
||||
*/
|
||||
function loadInjectionLog(devId) {
|
||||
const file = path.join(MEMORY_DIR, devId, 'injection-log.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch {
|
||||
return { entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存注入日志
|
||||
*/
|
||||
function saveInjectionLog(devId, log) {
|
||||
const dir = path.join(MEMORY_DIR, devId);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const entries = log.entries || [];
|
||||
// 保留最近 100 条日志
|
||||
if (entries.length > 100) {
|
||||
log.entries = entries.slice(-100);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'injection-log.json'),
|
||||
JSON.stringify(log, null, 2),
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户画像
|
||||
*/
|
||||
function loadProfile(devId) {
|
||||
const file = path.join(MEMORY_DIR, devId, 'profile.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 粗略估算 token 数量
|
||||
* 中文约 1.5 token/字(CJK字符经 BPE 分词通常为 1-2 token)
|
||||
* 英文/其他约 0.4 token/字符(英文单词平均 4 字符 ≈ 1 token)
|
||||
*/
|
||||
function estimateTokens(text) {
|
||||
if (!text) return 0;
|
||||
const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
|
||||
const otherChars = text.length - chineseChars;
|
||||
return Math.ceil(chineseChars * 1.5 + otherChars * 0.4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要重新压缩
|
||||
*/
|
||||
function needsRecompression(devId, history) {
|
||||
const config = loadInjectionConfig();
|
||||
if (!config.enabled) return false;
|
||||
|
||||
const trigger = config.compression_trigger || {};
|
||||
const compressed = loadCompressed(devId);
|
||||
const totalRounds = Math.floor((history || []).length / 2);
|
||||
|
||||
// 首次对话强制压缩
|
||||
if (!compressed && trigger.force_on_session_start) return true;
|
||||
|
||||
// 每 N 轮压缩一次
|
||||
if (compressed && trigger.every_n_rounds) {
|
||||
const processedRounds = compressed.total_rounds_processed || 0;
|
||||
if (totalRounds - processedRounds >= trigger.every_n_rounds) return true;
|
||||
}
|
||||
|
||||
// token 阈值
|
||||
if (trigger.token_threshold) {
|
||||
const totalText = (history || []).map(function (m) { return m.content || ''; }).join('');
|
||||
if (estimateTokens(totalText) > trigger.token_threshold) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地压缩对话历史(不依赖 AI 模型的规则式压缩)
|
||||
* 当模型不可用时的降级方案
|
||||
*/
|
||||
function localCompress(devId, history) {
|
||||
const messages = history || [];
|
||||
const userMessages = messages.filter(function (m) { return m.role === 'user'; });
|
||||
const assistantMessages = messages.filter(function (m) { return m.role === 'assistant'; });
|
||||
|
||||
// 提取需求
|
||||
const requirements = [];
|
||||
const decisions = [];
|
||||
const openQuestions = [];
|
||||
|
||||
userMessages.forEach(function (msg) {
|
||||
const content = msg.content || '';
|
||||
if (/想做|需要|功能|要求|做一个/i.test(content)) {
|
||||
requirements.push(content.substring(0, 100));
|
||||
}
|
||||
if (/[??]/.test(content)) {
|
||||
openQuestions.push(content.substring(0, 100));
|
||||
}
|
||||
});
|
||||
|
||||
assistantMessages.forEach(function (msg) {
|
||||
const content = msg.content || '';
|
||||
if (/方案已确认|确认|可以开始|建议使用|推荐/i.test(content)) {
|
||||
decisions.push(content.substring(0, 100));
|
||||
}
|
||||
});
|
||||
|
||||
const compressed = {
|
||||
version: 1,
|
||||
last_compressed_at: new Date().toISOString(),
|
||||
total_rounds_processed: Math.floor(messages.length / 2),
|
||||
summary: {
|
||||
requirements: requirements.slice(-10),
|
||||
confirmed_decisions: decisions.slice(-10),
|
||||
user_preferences: {},
|
||||
open_questions: openQuestions.slice(-5),
|
||||
emotional_signals: {
|
||||
overall: 'neutral',
|
||||
last_mood: 'neutral'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 融入画像信息
|
||||
const profile = loadProfile(devId);
|
||||
if (profile) {
|
||||
if (profile.communication_style) {
|
||||
compressed.summary.user_preferences.communication_style =
|
||||
profile.communication_style.verbosity || 'normal';
|
||||
}
|
||||
if (profile.design_preferences) {
|
||||
compressed.summary.user_preferences.design_preference =
|
||||
profile.design_preferences.color_scheme || null;
|
||||
}
|
||||
if (profile.tech_assessment) {
|
||||
compressed.summary.user_preferences.tech_level =
|
||||
profile.tech_assessment.level || null;
|
||||
}
|
||||
if (profile.emotional_profile) {
|
||||
compressed.summary.emotional_signals.overall =
|
||||
profile.emotional_profile.current_mood || 'neutral';
|
||||
}
|
||||
}
|
||||
|
||||
saveCompressed(devId, compressed);
|
||||
return compressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建注入的五层 system prompt
|
||||
* @param {string} basePrompt - 基础 system prompt(第1-2层)
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {object} memory - 记忆对象
|
||||
* @param {Array} history - 对话历史
|
||||
* @returns {string} 注入后的完整 system prompt
|
||||
*/
|
||||
function buildInjectedSystemPrompt(basePrompt, devId, memory, history) {
|
||||
const config = loadInjectionConfig();
|
||||
if (!config.enabled || !devId || devId === 'GUEST') {
|
||||
return basePrompt;
|
||||
}
|
||||
|
||||
const parts = [basePrompt]; // 第1-2层已包含在 basePrompt 中
|
||||
|
||||
// 第3层:用户画像
|
||||
const profile = loadProfile(devId);
|
||||
if (profile) {
|
||||
const profileParts = [];
|
||||
if (profile.tech_assessment && profile.tech_assessment.level) {
|
||||
profileParts.push('技术水平:' + profile.tech_assessment.level);
|
||||
}
|
||||
if (profile.tech_assessment && profile.tech_assessment.known_skills && profile.tech_assessment.known_skills.length > 0) {
|
||||
profileParts.push('已知技能:' + profile.tech_assessment.known_skills.join(', '));
|
||||
}
|
||||
if (profile.communication_style && profile.communication_style.verbosity) {
|
||||
profileParts.push('沟通偏好:' + profile.communication_style.verbosity);
|
||||
}
|
||||
if (profile.design_preferences && profile.design_preferences.color_scheme) {
|
||||
profileParts.push('设计偏好:' + profile.design_preferences.color_scheme);
|
||||
}
|
||||
if (profile.emotional_profile && profile.emotional_profile.current_mood) {
|
||||
profileParts.push('当前情绪:' + profile.emotional_profile.current_mood);
|
||||
}
|
||||
|
||||
if (profileParts.length > 0) {
|
||||
parts.push('\n## 用户画像\n' + profileParts.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
// 第4层:记忆摘要
|
||||
if (needsRecompression(devId, history)) {
|
||||
localCompress(devId, history);
|
||||
}
|
||||
|
||||
const compressed = loadCompressed(devId);
|
||||
if (compressed && compressed.summary) {
|
||||
const summaryParts = [];
|
||||
const priority = (config.injection_strategy && config.injection_strategy.priority) || [];
|
||||
const summary = compressed.summary;
|
||||
|
||||
priority.forEach(function (key) {
|
||||
if (key === 'confirmed_decisions' && summary.confirmed_decisions && summary.confirmed_decisions.length > 0) {
|
||||
summaryParts.push('已确认的决策:\n- ' + summary.confirmed_decisions.slice(-5).join('\n- '));
|
||||
}
|
||||
if (key === 'requirements' && summary.requirements && summary.requirements.length > 0) {
|
||||
summaryParts.push('用户需求要点:\n- ' + summary.requirements.slice(-5).join('\n- '));
|
||||
}
|
||||
if (key === 'open_questions' && summary.open_questions && summary.open_questions.length > 0) {
|
||||
summaryParts.push('待解决问题:\n- ' + summary.open_questions.slice(-3).join('\n- '));
|
||||
}
|
||||
if (key === 'user_preferences' && summary.user_preferences) {
|
||||
const prefs = Object.entries(summary.user_preferences)
|
||||
.filter(function (pair) { return pair[1] != null; })
|
||||
.map(function (pair) { return pair[0] + ': ' + pair[1]; });
|
||||
if (prefs.length > 0) {
|
||||
summaryParts.push('用户偏好:' + prefs.join(', '));
|
||||
}
|
||||
}
|
||||
if (key === 'emotional_signals' && summary.emotional_signals) {
|
||||
if (summary.emotional_signals.overall && summary.emotional_signals.overall !== 'neutral') {
|
||||
summaryParts.push('情感信号:整体 ' + summary.emotional_signals.overall);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (summaryParts.length > 0) {
|
||||
parts.push('\n## 记忆摘要(远期记忆)\n' + summaryParts.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
// 记录注入日志
|
||||
try {
|
||||
const log = loadInjectionLog(devId);
|
||||
log.entries.push({
|
||||
timestamp: new Date().toISOString(),
|
||||
layers_injected: parts.length,
|
||||
has_profile: !!profile,
|
||||
has_compressed: !!compressed,
|
||||
history_rounds: Math.floor((history || []).length / 2)
|
||||
});
|
||||
saveInjectionLog(devId, log);
|
||||
} catch (_e) { /* log failed silently */ }
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildInjectedSystemPrompt,
|
||||
getSlidingWindowSize,
|
||||
needsRecompression,
|
||||
localCompress,
|
||||
loadCompressed,
|
||||
saveCompressed,
|
||||
loadProfile,
|
||||
estimateTokens,
|
||||
loadInjectionConfig
|
||||
};
|
||||
|
|
@ -4,6 +4,9 @@
|
|||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const profileLearner = require('./profile-learner');
|
||||
const knowledgeExtractor = require('./knowledge-extractor');
|
||||
const evolutionLogger = require('./evolution-logger');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const MEMORY_DIR = path.join(BRAIN_DIR, 'memory');
|
||||
|
|
@ -125,6 +128,43 @@ function loadProfile(devId) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户画像(调用 profile-learner)
|
||||
* @param {string} devId - 开发编号
|
||||
* @param {string} userMessage - 用户消息
|
||||
* @param {string} assistantReply - 助手回复
|
||||
*/
|
||||
function updateProfile(devId, userMessage, assistantReply) {
|
||||
if (!devId || devId === 'GUEST') return;
|
||||
try {
|
||||
const result = profileLearner.updateProfile(devId, userMessage, assistantReply);
|
||||
if (result) {
|
||||
evolutionLogger.logProfileUpdate(devId, Object.keys(result));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Profile update error:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动提取知识(调用 knowledge-extractor)
|
||||
* @param {string} devId - 开发编号
|
||||
* @param {Array} conversation - 对话历史
|
||||
* @param {string} projectName - 项目名
|
||||
*/
|
||||
function autoExtractKnowledge(devId, conversation, projectName) {
|
||||
try {
|
||||
const count = knowledgeExtractor.autoExtractKnowledge(devId, conversation, projectName);
|
||||
if (count > 0) {
|
||||
evolutionLogger.logKnowledgeExtract(devId, count, projectName);
|
||||
}
|
||||
return count;
|
||||
} catch (err) {
|
||||
console.error('Knowledge extraction error:', err.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadMemory,
|
||||
saveMemory,
|
||||
|
|
@ -132,5 +172,7 @@ module.exports = {
|
|||
updateLastTopic,
|
||||
loadProjects,
|
||||
addProject,
|
||||
loadProfile
|
||||
loadProfile,
|
||||
updateProfile,
|
||||
autoExtractKnowledge
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"api_source": "third_party_combined",
|
||||
"api_key_env": "MODEL_API_KEY",
|
||||
"base_url": "https://api.yunwu.ai/v1",
|
||||
"context_window_target": 200000,
|
||||
"auto_detect": {
|
||||
"enabled": true,
|
||||
"schedule": "daily_0300",
|
||||
|
|
@ -13,7 +14,7 @@
|
|||
},
|
||||
"routing_rules": {
|
||||
"chat": {
|
||||
"priority": ["chinese_ability", "conversation_quality", "speed"],
|
||||
"priority": ["chinese_ability", "conversation_quality", "context_window", "speed"],
|
||||
"max_latency_ms": 5000
|
||||
},
|
||||
"code_generation": {
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
"max_latency_ms": 30000
|
||||
},
|
||||
"code_review": {
|
||||
"priority": ["reasoning", "code_quality"],
|
||||
"priority": ["reasoning", "code_quality", "context_window"],
|
||||
"max_latency_ms": 15000
|
||||
},
|
||||
"quick_reply": {
|
||||
|
|
@ -29,6 +30,20 @@
|
|||
"max_latency_ms": 2000
|
||||
}
|
||||
},
|
||||
"memory_injection": {
|
||||
"enabled": true,
|
||||
"compression_model": "quick_reply",
|
||||
"compression_trigger": {
|
||||
"every_n_rounds": 10,
|
||||
"token_threshold": 50000,
|
||||
"force_on_session_start": true
|
||||
},
|
||||
"injection_strategy": {
|
||||
"system_prompt_max_tokens": 8000,
|
||||
"sliding_window_rounds": 10,
|
||||
"priority": ["confirmed_decisions", "requirements", "open_questions", "user_preferences", "emotional_signals"]
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"max_retries": 3,
|
||||
"timeout_ms": 30000,
|
||||
|
|
|
|||
|
|
@ -53,9 +53,26 @@ function selectModel(taskType) {
|
|||
const benchmark = loadBenchmark();
|
||||
const apiKey = process.env.MODEL_API_KEY || '';
|
||||
const baseUrl = config.base_url || 'https://api.yunwu.ai/v1';
|
||||
const contextTarget = config.context_window_target || 200000;
|
||||
|
||||
// 如果有 benchmark 且有路由表,使用路由表
|
||||
if (benchmark && benchmark.routing_table && benchmark.routing_table[taskType]) {
|
||||
// 优先选择 context_window >= contextTarget 的模型
|
||||
if (benchmark.benchmark) {
|
||||
const preferred = benchmark.benchmark.find(function (m) {
|
||||
return m.available &&
|
||||
m.scores &&
|
||||
m.scores.context_window >= contextTarget;
|
||||
});
|
||||
if (preferred) {
|
||||
return {
|
||||
model: preferred.model_id,
|
||||
baseUrl,
|
||||
apiKey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model: benchmark.routing_table[taskType],
|
||||
baseUrl,
|
||||
|
|
@ -92,6 +109,7 @@ function selectModel(taskType) {
|
|||
async function callModel({ model, baseUrl, apiKey, messages, maxTokens = 2000, temperature = 0.8 }) {
|
||||
const config = loadConfig();
|
||||
const fallbackConfig = config.fallback || { max_retries: 3, timeout_ms: 30000 };
|
||||
const contextWindowTarget = config.context_window_target || 200000;
|
||||
|
||||
// 尝试调用,支持降级
|
||||
const benchmark = loadBenchmark();
|
||||
|
|
@ -227,10 +245,13 @@ async function autoDetect() {
|
|||
});
|
||||
|
||||
// 生成基准测试结果
|
||||
const contextTarget = config.context_window_target || 200000;
|
||||
const benchmarkData = {
|
||||
last_updated: new Date().toISOString(),
|
||||
models_detected: modelsList.length,
|
||||
context_window_target: contextTarget,
|
||||
benchmark: modelsList.slice(0, 10).map(function (m) {
|
||||
const ctxWindow = m.context_window || m.context_length || 32000;
|
||||
return {
|
||||
model_id: m.id,
|
||||
available: true,
|
||||
|
|
@ -240,9 +261,10 @@ async function autoDetect() {
|
|||
code_quality: 80,
|
||||
reasoning: 80,
|
||||
speed_ms: 2000,
|
||||
context_window: m.context_window || 32000,
|
||||
context_window: ctxWindow,
|
||||
cost_per_1k_tokens: 0.002
|
||||
},
|
||||
meets_context_target: ctxWindow >= contextTarget,
|
||||
best_for: ['chat']
|
||||
};
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* persona-studio · 模式识别引擎
|
||||
*
|
||||
* 触发:每完成一个项目 + 每日凌晨聚合
|
||||
* 功能:从已完成项目中识别高频模式,建立模式库
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const PATTERN_PATH = path.join(BRAIN_DIR, 'pattern-library.json');
|
||||
const MEMORY_DIR = path.join(BRAIN_DIR, 'memory');
|
||||
|
||||
/**
|
||||
* 加载模式库
|
||||
*/
|
||||
function loadPatternLibrary() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(PATTERN_PATH, 'utf-8'));
|
||||
} catch {
|
||||
return {
|
||||
schema_version: '1.0',
|
||||
description: '系统模式库 · 自动识别',
|
||||
last_updated: null,
|
||||
total_patterns: 0,
|
||||
patterns: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模式库
|
||||
*/
|
||||
function savePatternLibrary(lib) {
|
||||
lib.last_updated = new Date().toISOString();
|
||||
lib.total_patterns = lib.patterns.length;
|
||||
fs.writeFileSync(PATTERN_PATH, JSON.stringify(lib, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从项目中检测模式类型
|
||||
*/
|
||||
function detectPatternType(conversation, projectFiles) {
|
||||
const allText = (conversation || [])
|
||||
.map(function (m) { return m.content || ''; })
|
||||
.join('\n')
|
||||
.toLowerCase();
|
||||
|
||||
const patterns = [];
|
||||
|
||||
const patternDefs = [
|
||||
{ name: '个人博客', keywords: ['博客', 'blog', '文章', '发布', '个人主页'] },
|
||||
{ name: '登录注册', keywords: ['登录', '注册', 'login', 'register', '用户认证', '用户系统'] },
|
||||
{ name: '管理后台', keywords: ['管理', '后台', 'dashboard', '管理面板', 'admin'] },
|
||||
{ name: '电商页面', keywords: ['商城', '购物', '商品', '购买', '电商', 'shop'] },
|
||||
{ name: '展示页面', keywords: ['展示', '介绍', '落地页', 'landing', '产品页'] },
|
||||
{ name: '数据表格', keywords: ['表格', '数据', '列表', '筛选', 'table', '搜索'] },
|
||||
{ name: '表单系统', keywords: ['表单', '提交', '验证', 'form', '输入'] },
|
||||
{ name: '聊天应用', keywords: ['聊天', 'chat', '即时通讯', '消息', '对话'] },
|
||||
{ name: '小工具', keywords: ['工具', '计算器', '转换器', '生成器', 'tool', 'utility'] },
|
||||
{ name: 'API服务', keywords: ['api', '接口', '后端服务', 'restful', 'server'] }
|
||||
];
|
||||
|
||||
patternDefs.forEach(function (def) {
|
||||
const matchCount = def.keywords.filter(function (kw) {
|
||||
return allText.includes(kw);
|
||||
}).length;
|
||||
|
||||
if (matchCount >= 2) {
|
||||
patterns.push(def.name);
|
||||
}
|
||||
});
|
||||
|
||||
return patterns.length > 0 ? patterns : ['通用项目'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测技术栈
|
||||
*/
|
||||
function detectTechStack(conversation, projectFiles) {
|
||||
const allText = (conversation || [])
|
||||
.map(function (m) { return m.content || ''; })
|
||||
.join('\n')
|
||||
.toLowerCase();
|
||||
|
||||
const stack = [];
|
||||
const techMap = {
|
||||
'HTML/CSS/JS': ['html', 'css', 'javascript'],
|
||||
'React': ['react', 'jsx', 'tsx'],
|
||||
'Vue': ['vue', 'vuex'],
|
||||
'Node.js': ['node', 'express', 'koa'],
|
||||
'Python': ['python', 'flask', 'django'],
|
||||
'TypeScript': ['typescript', 'ts'],
|
||||
'Tailwind': ['tailwind'],
|
||||
'Bootstrap': ['bootstrap']
|
||||
};
|
||||
|
||||
for (const [tech, keywords] of Object.entries(techMap)) {
|
||||
if (keywords.some(function (kw) { return allText.includes(kw); })) {
|
||||
stack.push(tech);
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件扩展名推断
|
||||
if (projectFiles && projectFiles.length > 0) {
|
||||
const exts = projectFiles.map(function (f) {
|
||||
return path.extname(f).toLowerCase();
|
||||
});
|
||||
|
||||
if (exts.includes('.html')) stack.push('HTML/CSS/JS');
|
||||
if (exts.includes('.jsx') || exts.includes('.tsx')) stack.push('React');
|
||||
if (exts.includes('.vue')) stack.push('Vue');
|
||||
if (exts.includes('.py')) stack.push('Python');
|
||||
}
|
||||
|
||||
return [...new Set(stack)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心方法:分析项目并更新模式库
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {Array} conversation - 对话历史
|
||||
* @param {Array} projectFiles - 项目文件列表
|
||||
* @param {number} buildTimeMs - 构建耗时(毫秒)
|
||||
* @param {boolean} success - 是否成功
|
||||
*/
|
||||
function analyzeAndUpdatePatterns(devId, conversation, projectFiles, buildTimeMs, success) {
|
||||
const lib = loadPatternLibrary();
|
||||
const patternNames = detectPatternType(conversation, projectFiles);
|
||||
const techStack = detectTechStack(conversation, projectFiles);
|
||||
|
||||
patternNames.forEach(function (patternName) {
|
||||
// 查找已有模式
|
||||
const existing = lib.patterns.find(function (p) { return p.name === patternName; });
|
||||
|
||||
if (existing) {
|
||||
// 更新已有模式
|
||||
existing.frequency = (existing.frequency || 0) + 1;
|
||||
existing.common_tech_stack = mergeArrays(existing.common_tech_stack, techStack);
|
||||
if (buildTimeMs) {
|
||||
existing.avg_build_time = existing.avg_build_time
|
||||
? Math.round((existing.avg_build_time + buildTimeMs) / 2)
|
||||
: buildTimeMs;
|
||||
}
|
||||
if (success !== undefined) {
|
||||
const totalAttempts = existing.frequency;
|
||||
const prevSuccesses = Math.round((existing.success_rate || 100) / 100 * (totalAttempts - 1));
|
||||
existing.success_rate = Math.round((prevSuccesses + (success ? 1 : 0)) / totalAttempts * 100);
|
||||
}
|
||||
existing.last_seen = new Date().toISOString();
|
||||
} else {
|
||||
// 添加新模式
|
||||
lib.patterns.push({
|
||||
name: patternName,
|
||||
frequency: 1,
|
||||
common_features: [],
|
||||
common_tech_stack: techStack,
|
||||
avg_build_time: buildTimeMs || null,
|
||||
success_rate: success !== false ? 100 : 0,
|
||||
first_seen: new Date().toISOString(),
|
||||
last_seen: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
savePatternLibrary(lib);
|
||||
return patternNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询模式库(供 persona-engine 调用)
|
||||
* @param {string} query - 搜索关键词
|
||||
* @returns {Array} 匹配的模式
|
||||
*/
|
||||
function queryPatterns(query) {
|
||||
const lib = loadPatternLibrary();
|
||||
if (!query || lib.patterns.length === 0) return [];
|
||||
|
||||
const lower = query.toLowerCase();
|
||||
|
||||
return lib.patterns
|
||||
.filter(function (p) {
|
||||
return p.name.toLowerCase().includes(lower) ||
|
||||
(p.common_tech_stack || []).some(function (t) { return t.toLowerCase().includes(lower); });
|
||||
})
|
||||
.sort(function (a, b) { return (b.frequency || 0) - (a.frequency || 0); });
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具:合并去重数组
|
||||
*/
|
||||
function mergeArrays(arr1, arr2) {
|
||||
return [...new Set((arr1 || []).concat(arr2 || []))];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeAndUpdatePatterns,
|
||||
queryPatterns,
|
||||
loadPatternLibrary,
|
||||
detectPatternType,
|
||||
detectTechStack
|
||||
};
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const modelRouter = require('./model-router');
|
||||
const knowledgeExtractor = require('./knowledge-extractor');
|
||||
const patternAnalyzer = require('./pattern-analyzer');
|
||||
const memoryInjector = require('./memory-injector');
|
||||
|
||||
const PERSONA_CONFIG_PATH = path.join(__dirname, '..', '..', 'brain', 'persona-config.json');
|
||||
|
||||
|
|
@ -154,15 +157,41 @@ async function respond({ dev_id, message, history, memory, isGreeting }) {
|
|||
return getLocalReply(message, memory, config, isGuest);
|
||||
}
|
||||
|
||||
const systemPrompt = buildSystemPrompt(config, memory, devInfo);
|
||||
// 查询知识库和模式库,为回复提供参考
|
||||
let knowledgeContext = '';
|
||||
try {
|
||||
const relevantKnowledge = knowledgeExtractor.queryKnowledge(message, 3);
|
||||
if (relevantKnowledge.length > 0) {
|
||||
knowledgeContext += '\n\n## 系统知识库参考(不要直接暴露给用户,作为回复参考)\n';
|
||||
relevantKnowledge.forEach(function (k) {
|
||||
knowledgeContext += '- [' + k.type + '] ' + k.title + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
const relevantPatterns = patternAnalyzer.queryPatterns(message);
|
||||
if (relevantPatterns.length > 0) {
|
||||
knowledgeContext += '\n## 已知高频模式\n';
|
||||
relevantPatterns.slice(0, 3).forEach(function (p) {
|
||||
knowledgeContext += '- ' + p.name + '(使用 ' + p.frequency + ' 次,成功率 ' + p.success_rate + '%,常用技术栈:' + (p.common_tech_stack || []).join(', ') + ')\n';
|
||||
});
|
||||
}
|
||||
} catch (_e) { /* knowledge/pattern query failed silently */ }
|
||||
|
||||
const systemPrompt = buildSystemPrompt(config, memory, devInfo) + knowledgeContext;
|
||||
|
||||
// 使用记忆注入构建五层 system prompt
|
||||
const injectedPrompt = memoryInjector.buildInjectedSystemPrompt(
|
||||
systemPrompt, dev_id, memory, history
|
||||
);
|
||||
|
||||
// 构建消息列表
|
||||
const messages = [
|
||||
{ role: 'system', content: systemPrompt }
|
||||
{ role: 'system', content: injectedPrompt }
|
||||
];
|
||||
|
||||
// 加入最近历史(最多 20 条)
|
||||
const recentHistory = (history || []).slice(-20);
|
||||
// 使用滑动窗口加入最近历史
|
||||
const windowSize = memoryInjector.getSlidingWindowSize();
|
||||
const recentHistory = (history || []).slice(-windowSize);
|
||||
recentHistory.forEach(function (msg) {
|
||||
messages.push({
|
||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
/**
|
||||
* persona-studio · 用户画像自动学习引擎
|
||||
*
|
||||
* 触发:每轮对话结束后自动调用 updateProfile()
|
||||
* 功能:从对话内容中推断用户技术水平、沟通风格、设计偏好等
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BRAIN_DIR = path.join(__dirname, '..', '..', 'brain');
|
||||
const MEMORY_DIR = path.join(BRAIN_DIR, 'memory');
|
||||
|
||||
/**
|
||||
* 确保用户目录存在
|
||||
*/
|
||||
function ensureDevDir(devId) {
|
||||
const dir = path.join(MEMORY_DIR, devId);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户画像
|
||||
*/
|
||||
function loadProfile(devId) {
|
||||
const dir = ensureDevDir(devId);
|
||||
const file = path.join(dir, 'profile.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch {
|
||||
return createInitialProfile(devId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户画像
|
||||
*/
|
||||
function saveProfile(devId, profile) {
|
||||
const dir = ensureDevDir(devId);
|
||||
const file = path.join(dir, 'profile.json');
|
||||
profile.updated_at = new Date().toISOString();
|
||||
fs.writeFileSync(file, JSON.stringify(profile, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建初始画像
|
||||
*/
|
||||
function createInitialProfile(devId) {
|
||||
return {
|
||||
dev_id: devId,
|
||||
version: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: null,
|
||||
tech_assessment: {
|
||||
level: null,
|
||||
known_skills: [],
|
||||
learning_skills: [],
|
||||
weakness: [],
|
||||
growth_velocity: null,
|
||||
assessment_confidence: 0
|
||||
},
|
||||
communication_style: {
|
||||
verbosity: null,
|
||||
prefers_examples: null,
|
||||
question_frequency: null,
|
||||
patience_level: null
|
||||
},
|
||||
design_preferences: {
|
||||
color_scheme: null,
|
||||
style_keywords: [],
|
||||
layout_preference: null
|
||||
},
|
||||
project_patterns: {
|
||||
avg_requirements_rounds: 0,
|
||||
decision_speed: null,
|
||||
change_frequency: 0,
|
||||
total_projects: 0
|
||||
},
|
||||
emotional_profile: {
|
||||
current_mood: 'neutral',
|
||||
satisfaction_trend: [],
|
||||
frustration_triggers: [],
|
||||
delight_triggers: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从用户消息中推断技术技能
|
||||
*/
|
||||
function inferSkills(message) {
|
||||
const skills = [];
|
||||
const skillMap = {
|
||||
'javascript': ['javascript', 'js', 'node', 'nodejs', 'npm'],
|
||||
'typescript': ['typescript', 'ts'],
|
||||
'python': ['python', 'pip', 'django', 'flask', 'fastapi'],
|
||||
'react': ['react', 'jsx', 'tsx', 'hooks', 'useState', 'useEffect'],
|
||||
'vue': ['vue', 'vuex', 'pinia', 'nuxt'],
|
||||
'html': ['html', 'dom', 'div', 'css'],
|
||||
'css': ['css', 'scss', 'sass', 'tailwind', 'styled'],
|
||||
'sql': ['sql', 'mysql', 'postgres', 'sqlite', '数据库', 'database'],
|
||||
'git': ['git', 'github', 'gitlab', 'commit', 'branch'],
|
||||
'docker': ['docker', 'container', '容器', 'k8s', 'kubernetes'],
|
||||
'api': ['api', 'rest', 'graphql', 'grpc', '接口'],
|
||||
'mobile': ['android', 'ios', 'flutter', 'react native', '移动端', '小程序']
|
||||
};
|
||||
|
||||
const lowerMsg = message.toLowerCase();
|
||||
for (const [skill, keywords] of Object.entries(skillMap)) {
|
||||
if (keywords.some(function (kw) { return lowerMsg.includes(kw); })) {
|
||||
skills.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断沟通风格
|
||||
*/
|
||||
function inferCommunicationStyle(message) {
|
||||
const style = {};
|
||||
const msgLen = message.length;
|
||||
|
||||
// 简洁度
|
||||
if (msgLen < 20) {
|
||||
style.verbosity = 'concise';
|
||||
} else if (msgLen > 200) {
|
||||
style.verbosity = 'detailed';
|
||||
}
|
||||
|
||||
// 偏好示例
|
||||
if (/给.*例子|示例|比如|example|举个/i.test(message)) {
|
||||
style.prefers_examples = true;
|
||||
}
|
||||
|
||||
// 提问频率
|
||||
if ((message.match(/[??]/g) || []).length >= 2) {
|
||||
style.question_frequency = 'high';
|
||||
}
|
||||
|
||||
// 直接命令式 = patience_level low
|
||||
if (/直接告诉|直接给|快速|尽快|赶紧/i.test(message)) {
|
||||
style.patience_level = 'low';
|
||||
style.verbosity = 'concise';
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断技术水平
|
||||
*/
|
||||
function inferTechLevel(message) {
|
||||
const advancedKeywords = ['架构', '微服务', '分布式', '高并发', 'CI/CD', 'kubernetes', '设计模式', 'design pattern', '重构', '性能优化', 'SSR', 'ISR', 'WebSocket', 'gRPC'];
|
||||
const intermediateKeywords = ['组件', '路由', '中间件', 'middleware', '接口', 'API', '数据库', '前后端', '部署', '框架'];
|
||||
const beginnerKeywords = ['怎么开始', '入门', '新手', '不太懂', '什么是', '帮我做', '教我'];
|
||||
|
||||
const lower = message.toLowerCase();
|
||||
|
||||
if (advancedKeywords.some(function (kw) { return lower.includes(kw.toLowerCase()); })) {
|
||||
return 'advanced';
|
||||
}
|
||||
if (intermediateKeywords.some(function (kw) { return lower.includes(kw.toLowerCase()); })) {
|
||||
return 'intermediate';
|
||||
}
|
||||
if (beginnerKeywords.some(function (kw) { return lower.includes(kw.toLowerCase()); })) {
|
||||
return 'beginner';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断设计偏好
|
||||
*/
|
||||
function inferDesignPreferences(message) {
|
||||
const prefs = { style_keywords: [] };
|
||||
|
||||
if (/暗色|深色|dark|黑色/i.test(message)) {
|
||||
prefs.color_scheme = 'dark';
|
||||
} else if (/亮色|浅色|light|白色/i.test(message)) {
|
||||
prefs.color_scheme = 'light';
|
||||
}
|
||||
|
||||
const styleKeywords = ['简约', '科技感', '可爱', '商务', '极简', 'minimal', '现代', 'modern', '复古', '扁平', 'flat', '渐变', '毛玻璃', '赛博朋克'];
|
||||
styleKeywords.forEach(function (kw) {
|
||||
if (message.toLowerCase().includes(kw.toLowerCase())) {
|
||||
prefs.style_keywords.push(kw);
|
||||
}
|
||||
});
|
||||
|
||||
return prefs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断情感信号
|
||||
*/
|
||||
function inferEmotionalSignals(message) {
|
||||
const signals = {};
|
||||
|
||||
if (/太好了|完美|厉害|赞|棒|不错|满意|感谢|谢谢|开心|👍|🎉|✅/i.test(message)) {
|
||||
signals.current_mood = 'positive';
|
||||
} else if (/不行|不对|错了|差|不满|失望|难用|麻烦|头疼|崩溃|😤|😡|❌/i.test(message)) {
|
||||
signals.current_mood = 'negative';
|
||||
} else {
|
||||
signals.current_mood = 'neutral';
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心方法:更新用户画像
|
||||
* @param {string} devId - 开发编号
|
||||
* @param {string} userMessage - 用户消息
|
||||
* @param {string} assistantReply - 助手回复
|
||||
*/
|
||||
function updateProfile(devId, userMessage, assistantReply) {
|
||||
if (!devId || devId === 'GUEST') return null;
|
||||
|
||||
const profile = loadProfile(devId);
|
||||
|
||||
// 1. 推断技术技能
|
||||
const skills = inferSkills(userMessage);
|
||||
if (skills.length > 0) {
|
||||
const known = new Set(profile.tech_assessment.known_skills || []);
|
||||
skills.forEach(function (s) { known.add(s); });
|
||||
profile.tech_assessment.known_skills = Array.from(known);
|
||||
profile.tech_assessment.assessment_confidence = Math.min(
|
||||
(profile.tech_assessment.assessment_confidence || 0) + 5, 100
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 推断技术水平
|
||||
const techLevel = inferTechLevel(userMessage);
|
||||
if (techLevel) {
|
||||
profile.tech_assessment.level = techLevel;
|
||||
}
|
||||
|
||||
// 3. 推断沟通风格
|
||||
const commStyle = inferCommunicationStyle(userMessage);
|
||||
Object.keys(commStyle).forEach(function (key) {
|
||||
if (commStyle[key] != null) {
|
||||
profile.communication_style[key] = commStyle[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 推断设计偏好
|
||||
const designPrefs = inferDesignPreferences(userMessage);
|
||||
if (designPrefs.color_scheme) {
|
||||
profile.design_preferences.color_scheme = designPrefs.color_scheme;
|
||||
}
|
||||
if (designPrefs.style_keywords.length > 0) {
|
||||
const existing = new Set(profile.design_preferences.style_keywords || []);
|
||||
designPrefs.style_keywords.forEach(function (kw) { existing.add(kw); });
|
||||
profile.design_preferences.style_keywords = Array.from(existing);
|
||||
}
|
||||
|
||||
// 5. 推断情感信号
|
||||
const emotions = inferEmotionalSignals(userMessage);
|
||||
profile.emotional_profile.current_mood = emotions.current_mood;
|
||||
const trend = profile.emotional_profile.satisfaction_trend || [];
|
||||
trend.push({ mood: emotions.current_mood, at: new Date().toISOString() });
|
||||
if (trend.length > 50) trend.splice(0, trend.length - 50);
|
||||
profile.emotional_profile.satisfaction_trend = trend;
|
||||
|
||||
// 6. 更新版本
|
||||
profile.version = (profile.version || 0) + 1;
|
||||
|
||||
saveProfile(devId, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
updateProfile,
|
||||
loadProfile,
|
||||
saveProfile,
|
||||
createInitialProfile,
|
||||
inferSkills,
|
||||
inferTechLevel,
|
||||
inferCommunicationStyle,
|
||||
inferDesignPreferences,
|
||||
inferEmotionalSignals
|
||||
};
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^7.0.13"
|
||||
"nodemailer": "^7.0.13",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
|
|
@ -872,6 +873,27 @@
|
|||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"nodemailer": "^7.0.13"
|
||||
"nodemailer": "^7.0.13",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ const memoryManager = require('../brain/memory-manager');
|
|||
const codeGenerator = require('../brain/code-generator');
|
||||
const emailSender = require('../utils/email-sender');
|
||||
|
||||
// 邮箱后端正则二次校验
|
||||
const EMAIL_RE = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
|
||||
// POST /api/ps/build/start
|
||||
router.post('/start', async (req, res) => {
|
||||
const { dev_id, email, conversation } = req.body || {};
|
||||
const { dev_id, email, contact, conversation } = req.body || {};
|
||||
|
||||
if (!dev_id || !/^EXP-\d{3,}$/.test(dev_id)) {
|
||||
return res.status(400).json({
|
||||
|
|
@ -28,6 +31,18 @@ router.post('/start', async (req, res) => {
|
|||
});
|
||||
}
|
||||
|
||||
// 后端二次邮箱校验
|
||||
if (!EMAIL_RE.test(email)) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'INVALID_EMAIL',
|
||||
message: '邮箱格式不正确'
|
||||
});
|
||||
}
|
||||
|
||||
// 联系方式输入清理(去除潜在 HTML/script 注入)
|
||||
const safeContact = contact ? String(contact).replace(/[<>&"']/g, '').substring(0, 100) : null;
|
||||
|
||||
// 先立即响应,后台异步处理
|
||||
res.json({
|
||||
error: false,
|
||||
|
|
@ -37,7 +52,16 @@ router.post('/start', async (req, res) => {
|
|||
|
||||
// 异步执行代码生成 + 邮件通知
|
||||
(async () => {
|
||||
const broadcast = req.app.locals.broadcastToClient || function () {};
|
||||
|
||||
try {
|
||||
broadcast(dev_id, {
|
||||
type: 'progress',
|
||||
message: '🔧 正在创建项目骨架...',
|
||||
status: 'building',
|
||||
status_text: '构建中'
|
||||
});
|
||||
|
||||
const result = await codeGenerator.generate({
|
||||
dev_id,
|
||||
conversation: conversation || [],
|
||||
|
|
@ -46,20 +70,42 @@ router.post('/start', async (req, res) => {
|
|||
// 记录项目
|
||||
memoryManager.addProject(dev_id, {
|
||||
name: result.projectName || 'untitled',
|
||||
email: email,
|
||||
contact: safeContact,
|
||||
status: 'completed',
|
||||
created_at: new Date().toISOString(),
|
||||
files: result.files || []
|
||||
});
|
||||
|
||||
// 自动提取知识
|
||||
memoryManager.autoExtractKnowledge(dev_id, conversation, result.projectName);
|
||||
|
||||
// 通知预览就绪
|
||||
broadcast(dev_id, {
|
||||
type: 'preview_ready',
|
||||
project: result.projectName,
|
||||
message: '✅ 预览已就绪'
|
||||
});
|
||||
|
||||
broadcast(dev_id, {
|
||||
type: 'complete',
|
||||
message: '🎉 全部完成!邮件正在发送'
|
||||
});
|
||||
|
||||
// 发邮件
|
||||
await emailSender.sendCompletion({
|
||||
to: email,
|
||||
dev_id,
|
||||
projectName: result.projectName,
|
||||
summary: result.summary
|
||||
summary: result.summary,
|
||||
files: result.files
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Build pipeline error:', err.message);
|
||||
broadcast(dev_id, {
|
||||
type: 'error',
|
||||
message: '构建过程出错: ' + err.message
|
||||
});
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ router.post('/message', async (req, res) => {
|
|||
|
||||
// 更新最后话题
|
||||
memoryManager.updateLastTopic(dev_id, message);
|
||||
|
||||
// 自动更新用户画像
|
||||
memoryManager.updateProfile(dev_id, message, result.reply);
|
||||
}
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* persona-studio · 预览 API
|
||||
* GET /api/ps/preview/:devId/:project 提供 iframe 实时预览
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const WORKSPACE_DIR = path.join(__dirname, '..', '..', 'workspace');
|
||||
|
||||
// 简易速率限制:每个 IP 每分钟最多 60 次请求
|
||||
const rateLimitMap = new Map();
|
||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
||||
const RATE_LIMIT_MAX = 60;
|
||||
|
||||
function rateLimit(req, res, next) {
|
||||
const ip = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const now = Date.now();
|
||||
const entry = rateLimitMap.get(ip);
|
||||
|
||||
if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
||||
rateLimitMap.set(ip, { start: now, count: 1 });
|
||||
return next();
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
if (entry.count > RATE_LIMIT_MAX) {
|
||||
return res.status(429).json({
|
||||
error: true,
|
||||
code: 'RATE_LIMITED',
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
// 定期清理过期条目
|
||||
setInterval(function () {
|
||||
const now = Date.now();
|
||||
for (const [ip, entry] of rateLimitMap) {
|
||||
if (now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
||||
rateLimitMap.delete(ip);
|
||||
}
|
||||
}
|
||||
}, RATE_LIMIT_WINDOW_MS);
|
||||
|
||||
// GET /api/ps/preview/:devId/:project
|
||||
router.get('/:devId/:project', rateLimit, (req, res) => {
|
||||
const { devId, project } = req.params;
|
||||
|
||||
if (!devId || !project) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_PARAMS',
|
||||
message: '缺少必要参数'
|
||||
});
|
||||
}
|
||||
|
||||
// 安全校验:确保 devId 和 project 匹配预期格式(防止路径遍历)
|
||||
const safeDevId = path.basename(devId);
|
||||
const safeProject = path.basename(project);
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(safeDevId) || !/^[a-zA-Z0-9_.-]+$/.test(safeProject)) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'INVALID_PARAMS',
|
||||
message: '参数格式无效'
|
||||
});
|
||||
}
|
||||
|
||||
const previewDir = path.join(WORKSPACE_DIR, safeDevId, safeProject, 'preview');
|
||||
const projectDir = path.join(WORKSPACE_DIR, safeDevId, safeProject);
|
||||
|
||||
// 优先从 preview/ 子目录查找
|
||||
let targetDir = previewDir;
|
||||
if (!fs.existsSync(previewDir)) {
|
||||
targetDir = projectDir;
|
||||
}
|
||||
|
||||
const indexPath = path.join(targetDir, 'index.html');
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
return res.status(404).send(
|
||||
'<html><body style="background:#0a0e1a;color:#94a3b8;display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">' +
|
||||
'<div style="text-align:center"><p style="font-size:2rem">🌊</p><p>预览正在准备中…</p></div>' +
|
||||
'</body></html>'
|
||||
);
|
||||
}
|
||||
|
||||
res.sendFile(indexPath);
|
||||
});
|
||||
|
||||
// GET /api/ps/preview/:devId/:project/:file (sub-resources like CSS/JS)
|
||||
router.get('/:devId/:project/:file', rateLimit, (req, res) => {
|
||||
const { devId, project, file } = req.params;
|
||||
|
||||
const safeDevId = path.basename(devId);
|
||||
const safeProject = path.basename(project);
|
||||
const safeFile = path.basename(file);
|
||||
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(safeDevId) || !/^[a-zA-Z0-9_.-]+$/.test(safeProject) || !/^[a-zA-Z0-9_.-]+$/.test(safeFile)) {
|
||||
return res.status(400).send('Invalid parameters');
|
||||
}
|
||||
|
||||
const previewDir = path.join(WORKSPACE_DIR, safeDevId, safeProject, 'preview');
|
||||
const projectDir = path.join(WORKSPACE_DIR, safeDevId, safeProject);
|
||||
|
||||
let targetDir = previewDir;
|
||||
if (!fs.existsSync(previewDir)) {
|
||||
targetDir = projectDir;
|
||||
}
|
||||
|
||||
const filePath = path.join(targetDir, safeFile);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).send('File not found');
|
||||
}
|
||||
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -2,12 +2,14 @@ require('dotenv').config();
|
|||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const chatRoutes = require('./routes/chat');
|
||||
const buildRoutes = require('./routes/build');
|
||||
const notifyRoutes = require('./routes/notify');
|
||||
const apikeyRoutes = require('./routes/apikey');
|
||||
const previewRoutes = require('./routes/preview');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
|
@ -22,6 +24,7 @@ app.use('/api/ps/chat', chatRoutes);
|
|||
app.use('/api/ps/build', buildRoutes);
|
||||
app.use('/api/ps/notify', notifyRoutes);
|
||||
app.use('/api/ps/apikey', apikeyRoutes);
|
||||
app.use('/api/ps/preview', previewRoutes);
|
||||
|
||||
// ── 健康检查 ──
|
||||
app.get('/api/ps/health', (_req, res) => {
|
||||
|
|
@ -48,7 +51,7 @@ app.get('/', (_req, res) => {
|
|||
res.json({
|
||||
status: 'ok',
|
||||
message: 'Persona Studio 后端服务运行中',
|
||||
version: '1.0.0',
|
||||
version: '2.0.0',
|
||||
routes: [
|
||||
'/api/ps/auth/login',
|
||||
'/api/ps/chat/message',
|
||||
|
|
@ -57,6 +60,7 @@ app.get('/', (_req, res) => {
|
|||
'/api/ps/notify/send',
|
||||
'/api/ps/apikey/detect-models',
|
||||
'/api/ps/apikey/chat',
|
||||
'/api/ps/preview/:devId/:project',
|
||||
'/api/ps/health'
|
||||
]
|
||||
});
|
||||
|
|
@ -64,7 +68,60 @@ app.get('/', (_req, res) => {
|
|||
|
||||
const PORT = process.env.PS_PORT || 3002;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
// ── WebSocket 服务(预览进度推送) ──
|
||||
const server = http.createServer(app);
|
||||
|
||||
// WebSocket clients map: dev_id -> Set<ws>
|
||||
const wsClients = new Map();
|
||||
|
||||
try {
|
||||
const WebSocket = require('ws');
|
||||
const wss = new WebSocket.Server({ server, path: '/ws/preview' });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const devId = url.searchParams.get('dev_id') || 'unknown';
|
||||
|
||||
if (!wsClients.has(devId)) {
|
||||
wsClients.set(devId, new Set());
|
||||
}
|
||||
wsClients.get(devId).add(ws);
|
||||
|
||||
ws.on('close', () => {
|
||||
const clients = wsClients.get(devId);
|
||||
if (clients) {
|
||||
clients.delete(ws);
|
||||
if (clients.size === 0) wsClients.delete(devId);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
const clients = wsClients.get(devId);
|
||||
if (clients) {
|
||||
clients.delete(ws);
|
||||
if (clients.size === 0) wsClients.delete(devId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export broadcast function for other modules
|
||||
app.locals.broadcastToClient = function (devId, data) {
|
||||
const clients = wsClients.get(devId);
|
||||
if (!clients) return;
|
||||
const msg = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
clients.forEach(function (ws) {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
} catch (_e) {
|
||||
// ws module not installed, WebSocket disabled
|
||||
console.warn('[Persona Studio] ws module not available, WebSocket features disabled');
|
||||
app.locals.broadcastToClient = function () {};
|
||||
}
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`🌊 Persona Studio 后端服务启动 · 端口 ${PORT}`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -50,21 +50,51 @@ async function send({ to, subject, body }) {
|
|||
/**
|
||||
* 发送开发完成通知
|
||||
*/
|
||||
async function sendCompletion({ to, dev_id, projectName, summary }) {
|
||||
async function sendCompletion({ to, dev_id, projectName, summary, files, downloadUrl }) {
|
||||
const subject = `✅ 你的模块已完成 · ${projectName}`;
|
||||
|
||||
// 生成文件列表
|
||||
let fileListHtml = '';
|
||||
if (files && files.length > 0) {
|
||||
fileListHtml = '<h3>📁 文件列表</h3><ul style="color:#e2e8f0;padding-left:20px">';
|
||||
files.forEach(function (f) {
|
||||
fileListHtml += '<li style="margin:4px 0">' + f + '</li>';
|
||||
});
|
||||
fileListHtml += '</ul>';
|
||||
}
|
||||
|
||||
// 下载链接
|
||||
let downloadHtml = '';
|
||||
if (downloadUrl) {
|
||||
downloadHtml = '<p><a href="' + downloadUrl + '" style="display:inline-block;padding:10px 24px;background:linear-gradient(135deg,#3b82f6,#22d3ee);color:#fff;text-decoration:none;border-radius:8px;font-weight:600">📦 下载项目包</a></p>';
|
||||
}
|
||||
|
||||
const body = [
|
||||
'<div style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px">',
|
||||
'<h2 style="color:#0969da">🌊 光湖 Persona Studio</h2>',
|
||||
'<hr>',
|
||||
`<p>你好 ${dev_id},</p>`,
|
||||
`<p>你的项目 <strong>${projectName}</strong> 已经完成开发!</p>`,
|
||||
'<div style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px;background:#0f172a;color:#e2e8f0;border-radius:12px">',
|
||||
'<div style="text-align:center;padding:20px 0;border-bottom:1px solid #334155">',
|
||||
'<h2 style="color:#60a5fa;margin:0">🌊 光湖 Persona Studio</h2>',
|
||||
'<p style="color:#94a3b8;font-size:14px;margin:8px 0 0">HoloLake Era · AGE OS · 人格语言操作系统</p>',
|
||||
'</div>',
|
||||
'<div style="padding:20px 0">',
|
||||
'<p>你好 ' + dev_id + ',</p>',
|
||||
'<p>你的项目 <strong style="color:#22d3ee">' + projectName + '</strong> 已经完成开发!</p>',
|
||||
'<h3>📋 开发摘要</h3>',
|
||||
`<p>${summary || '项目代码已生成'}</p>`,
|
||||
'<hr>',
|
||||
'<p style="color:#656d76;font-size:12px">',
|
||||
'光湖语言人格系统 · HoloLake Era · AGE OS<br>',
|
||||
'此邮件由铸渊自动发送',
|
||||
'<p>' + (summary || '项目代码已生成') + '</p>',
|
||||
fileListHtml,
|
||||
downloadHtml,
|
||||
'<h3>📖 使用说明</h3>',
|
||||
'<ol style="padding-left:20px">',
|
||||
'<li>下载项目包并解压</li>',
|
||||
'<li>在浏览器中打开 index.html 查看效果</li>',
|
||||
'<li>如需修改,用任意代码编辑器打开项目文件</li>',
|
||||
'</ol>',
|
||||
'</div>',
|
||||
'<div style="border-top:1px solid #334155;padding:16px 0;text-align:center">',
|
||||
'<p style="color:#64748b;font-size:12px;margin:0">',
|
||||
'🌀 铸渊 · 代码守护人格体 · 自动发送<br>',
|
||||
'光湖语言人格系统 · HoloLake Era · AGE OS',
|
||||
'</p>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join('\n');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"description": "系统进化日志 · 记录每次自进化事件",
|
||||
"last_updated": null,
|
||||
"total_events": 0,
|
||||
"events": []
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"description": "系统知识库 · 自动积累 · 每次代码生成/方案确认/技术问题解决时更新",
|
||||
"last_updated": null,
|
||||
"total_entries": 0,
|
||||
"entries": []
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"description": "系统模式库 · 自动识别 · 每完成一个项目+每日聚合更新",
|
||||
"last_updated": null,
|
||||
"total_patterns": 0,
|
||||
"patterns": []
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"description": "质量评分 · 反馈驱动 · 项目完成后自动评分",
|
||||
"last_updated": null,
|
||||
"total_scores": 0,
|
||||
"scores": []
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="chat-layout">
|
||||
<div class="chat-layout" id="chatLayout">
|
||||
<!-- ── 左侧边栏 ── -->
|
||||
<aside class="chat-sidebar" id="chatSidebar">
|
||||
<div class="sidebar-header">
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ── 主聊天区域 ── -->
|
||||
<div class="chat-main">
|
||||
<!-- ── 主聊天区域(分屏左侧) ── -->
|
||||
<div class="chat-main" id="chatMain">
|
||||
<header class="chat-header">
|
||||
<div class="header-left">
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()" title="展开侧栏" id="sidebarToggleBtn">☰</button>
|
||||
|
|
@ -106,20 +106,54 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 可拖拽分割线 ── -->
|
||||
<div class="resizer" id="resizer" style="display:none;"></div>
|
||||
|
||||
<!-- ── 右侧预览面板 ── -->
|
||||
<div class="preview-panel" id="previewPanel" style="display:none;">
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">
|
||||
<span class="preview-icon">🌊</span>
|
||||
<span>实时预览 · <span id="previewProjectName">项目</span></span>
|
||||
</div>
|
||||
<div class="preview-status" id="previewStatus">
|
||||
<span class="status-dot status-waiting"></span>
|
||||
<span class="status-text">等待中</span>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="preview-btn" onclick="refreshPreview()" title="刷新预览">↻ 刷新</button>
|
||||
<button class="preview-btn" onclick="openPreviewNewWindow()" title="新窗口打开">↗ 新窗口</button>
|
||||
</div>
|
||||
</div>
|
||||
<iframe id="previewFrame" class="preview-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email Modal -->
|
||||
<!-- Email Modal (增强版:邮箱+联系方式+二次校验) -->
|
||||
<div id="emailModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h3>📧 请填写模块开发完成后发送的邮箱</h3>
|
||||
<input
|
||||
type="email"
|
||||
id="emailInput"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
<h3>📧 开发完成后,代码将发送到你的邮箱</h3>
|
||||
<div class="modal-field">
|
||||
<label class="modal-label" for="emailInput">邮箱地址 <span class="required">*</span></label>
|
||||
<input
|
||||
type="email"
|
||||
id="emailInput"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
<div class="modal-error" id="emailError" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label class="modal-label" for="contactInput">联系方式 <span class="optional">(选填)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="contactInput"
|
||||
placeholder="微信号 / 手机号(选填)"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-primary" onclick="confirmBuild()">确定</button>
|
||||
<button class="btn-primary" onclick="confirmBuild()">确认</button>
|
||||
<button class="btn-secondary" onclick="closeEmailModal()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -584,21 +584,67 @@ function appendMessage(role, content) {
|
|||
|
||||
/* ---- Build Flow ---- */
|
||||
function handleBuild() {
|
||||
document.getElementById('emailModal').style.display = 'flex';
|
||||
document.getElementById('emailInput').focus();
|
||||
var modal = document.getElementById('emailModal');
|
||||
var emailInput = document.getElementById('emailInput');
|
||||
var contactInput = document.getElementById('contactInput');
|
||||
var errorDiv = document.getElementById('emailError');
|
||||
|
||||
// 预填已存储的邮箱
|
||||
var savedEmail = sessionStorage.getItem('ps_build_email') || '';
|
||||
var savedContact = sessionStorage.getItem('ps_build_contact') || '';
|
||||
if (savedEmail) emailInput.value = savedEmail;
|
||||
if (savedContact) contactInput.value = savedContact;
|
||||
|
||||
errorDiv.style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
emailInput.focus();
|
||||
}
|
||||
|
||||
function closeEmailModal() {
|
||||
document.getElementById('emailModal').style.display = 'none';
|
||||
document.getElementById('emailError').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端邮箱格式校验(与后端 build.js 使用相同正则,双重校验)
|
||||
* @param {string} email - 邮箱地址
|
||||
* @returns {boolean} 是否合法
|
||||
*/
|
||||
function validateEmail(email) {
|
||||
var re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
async function confirmBuild() {
|
||||
var email = document.getElementById('emailInput').value.trim();
|
||||
if (!email) return;
|
||||
var emailInput = document.getElementById('emailInput');
|
||||
var contactInput = document.getElementById('contactInput');
|
||||
var errorDiv = document.getElementById('emailError');
|
||||
var email = emailInput.value.trim();
|
||||
var contact = contactInput.value.trim();
|
||||
|
||||
// 前端校验
|
||||
if (!email) {
|
||||
errorDiv.textContent = '请填写邮箱地址';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
errorDiv.textContent = '邮箱格式不正确,请检查';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// 存储邮箱(下次预填)
|
||||
sessionStorage.setItem('ps_build_email', email);
|
||||
if (contact) sessionStorage.setItem('ps_build_contact', contact);
|
||||
|
||||
closeEmailModal();
|
||||
appendMessage('system', '🚀 开发任务已提交,完成后会发送到 ' + email);
|
||||
|
||||
// 进入分屏模式
|
||||
enterDevMode();
|
||||
|
||||
try {
|
||||
await fetch(API_BASE + '/api/ps/build/start', {
|
||||
method: 'POST',
|
||||
|
|
@ -606,12 +652,227 @@ async function confirmBuild() {
|
|||
body: JSON.stringify({
|
||||
dev_id: DEV_ID,
|
||||
email: email,
|
||||
contact: contact,
|
||||
conversation: conversationHistory
|
||||
})
|
||||
});
|
||||
} catch (_err) {
|
||||
appendMessage('system', '任务提交失败,请稍后再试');
|
||||
}
|
||||
|
||||
// 连接 WebSocket 获取进度更新
|
||||
connectPreviewWebSocket();
|
||||
}
|
||||
|
||||
/* ---- Dev Mode: Split Screen ---- */
|
||||
var isDevMode = false;
|
||||
var wsConnection = null;
|
||||
var currentPreviewUrl = '';
|
||||
|
||||
function enterDevMode() {
|
||||
if (isDevMode) return;
|
||||
isDevMode = true;
|
||||
|
||||
var layout = document.getElementById('chatLayout');
|
||||
var resizer = document.getElementById('resizer');
|
||||
var previewPanel = document.getElementById('previewPanel');
|
||||
|
||||
layout.classList.add('dev-mode');
|
||||
resizer.style.display = 'block';
|
||||
previewPanel.style.display = 'flex';
|
||||
|
||||
// 初始各占50%
|
||||
var chatMain = document.getElementById('chatMain');
|
||||
chatMain.style.flex = '1 1 50%';
|
||||
previewPanel.style.flex = '1 1 50%';
|
||||
|
||||
updatePreviewStatus('waiting', '等待中');
|
||||
initResizer();
|
||||
}
|
||||
|
||||
function exitDevMode() {
|
||||
isDevMode = false;
|
||||
|
||||
var layout = document.getElementById('chatLayout');
|
||||
var resizer = document.getElementById('resizer');
|
||||
var previewPanel = document.getElementById('previewPanel');
|
||||
var chatMain = document.getElementById('chatMain');
|
||||
|
||||
layout.classList.remove('dev-mode');
|
||||
resizer.style.display = 'none';
|
||||
previewPanel.style.display = 'none';
|
||||
chatMain.style.flex = '';
|
||||
|
||||
if (wsConnection) {
|
||||
wsConnection.close();
|
||||
wsConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Draggable Resizer ---- */
|
||||
function initResizer() {
|
||||
var resizer = document.getElementById('resizer');
|
||||
var chatMain = document.getElementById('chatMain');
|
||||
var previewPanel = document.getElementById('previewPanel');
|
||||
var layout = document.getElementById('chatLayout');
|
||||
|
||||
var startX, startChatWidth, startPreviewWidth;
|
||||
|
||||
function onMouseDown(e) {
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
startChatWidth = chatMain.getBoundingClientRect().width;
|
||||
startPreviewWidth = previewPanel.getBoundingClientRect().width;
|
||||
resizer.classList.add('resizing');
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
function onMouseMove(e) {
|
||||
var dx = e.clientX - startX;
|
||||
var layoutWidth = layout.getBoundingClientRect().width;
|
||||
var sidebarWidth = document.getElementById('chatSidebar').getBoundingClientRect().width;
|
||||
var resizerWidth = resizer.getBoundingClientRect().width;
|
||||
var available = layoutWidth - sidebarWidth - resizerWidth;
|
||||
|
||||
var newChatWidth = startChatWidth + dx;
|
||||
var newPreviewWidth = startPreviewWidth - dx;
|
||||
|
||||
// Enforce min-width 360px
|
||||
if (newChatWidth < 360) newChatWidth = 360;
|
||||
if (newPreviewWidth < 360) newPreviewWidth = 360;
|
||||
if (newChatWidth + newPreviewWidth > available) return;
|
||||
|
||||
chatMain.style.flex = '0 0 ' + newChatWidth + 'px';
|
||||
previewPanel.style.flex = '0 0 ' + newPreviewWidth + 'px';
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
resizer.classList.remove('resizing');
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
resizer.addEventListener('mousedown', onMouseDown);
|
||||
|
||||
// Touch support for mobile
|
||||
resizer.addEventListener('touchstart', function (e) {
|
||||
var touch = e.touches[0];
|
||||
startX = touch.clientX;
|
||||
startChatWidth = chatMain.getBoundingClientRect().width;
|
||||
startPreviewWidth = previewPanel.getBoundingClientRect().width;
|
||||
resizer.classList.add('resizing');
|
||||
|
||||
function onTouchMove(ev) {
|
||||
var t = ev.touches[0];
|
||||
var fakeEvent = { clientX: t.clientX };
|
||||
onMouseMove(fakeEvent);
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
resizer.classList.remove('resizing');
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
|
||||
document.addEventListener('touchmove', onTouchMove);
|
||||
document.addEventListener('touchend', onTouchEnd);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- Preview Panel ---- */
|
||||
function updatePreviewStatus(status, text) {
|
||||
var statusEl = document.getElementById('previewStatus');
|
||||
if (!statusEl) return;
|
||||
|
||||
var dotClass = 'status-dot status-' + status;
|
||||
statusEl.innerHTML = '<span class="' + dotClass + '"></span><span class="status-text">' + escapeHtml(text) + '</span>';
|
||||
}
|
||||
|
||||
function updatePreviewUrl(url) {
|
||||
currentPreviewUrl = url;
|
||||
var frame = document.getElementById('previewFrame');
|
||||
if (frame) frame.src = url;
|
||||
}
|
||||
|
||||
function refreshPreview() {
|
||||
var frame = document.getElementById('previewFrame');
|
||||
if (frame && frame.src) {
|
||||
frame.src = frame.src;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreviewNewWindow() {
|
||||
if (currentPreviewUrl) {
|
||||
window.open(currentPreviewUrl, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
function setPreviewProjectName(name) {
|
||||
var el = document.getElementById('previewProjectName');
|
||||
if (el) el.textContent = name || '项目';
|
||||
}
|
||||
|
||||
/* ---- WebSocket for Preview Updates ---- */
|
||||
function connectPreviewWebSocket() {
|
||||
var wsBase = API_BASE.replace(/^http/, 'ws');
|
||||
var wsUrl = wsBase + '/ws/preview?dev_id=' + encodeURIComponent(DEV_ID);
|
||||
|
||||
try {
|
||||
wsConnection = new WebSocket(wsUrl);
|
||||
|
||||
wsConnection.onopen = function () {
|
||||
appendMessage('system', '🔧 正在创建项目骨架...');
|
||||
updatePreviewStatus('building', '构建中');
|
||||
};
|
||||
|
||||
wsConnection.onmessage = function (event) {
|
||||
try {
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'progress') {
|
||||
appendMessage('system', data.message || '构建进度更新');
|
||||
if (data.status) {
|
||||
updatePreviewStatus(data.status, data.status_text || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (data.type === 'preview_ready') {
|
||||
var previewUrl = API_BASE + '/api/ps/preview/' + encodeURIComponent(DEV_ID) + '/' + encodeURIComponent(data.project);
|
||||
updatePreviewUrl(previewUrl);
|
||||
setPreviewProjectName(data.project);
|
||||
updatePreviewStatus('done', '完成');
|
||||
appendMessage('system', '✅ 预览已就绪,右侧可以查看');
|
||||
}
|
||||
|
||||
if (data.type === 'reload') {
|
||||
refreshPreview();
|
||||
}
|
||||
|
||||
if (data.type === 'complete') {
|
||||
updatePreviewStatus('done', '全部完成');
|
||||
appendMessage('system', '🎉 全部完成!邮件正在发送');
|
||||
}
|
||||
|
||||
if (data.type === 'error') {
|
||||
updatePreviewStatus('error', '出错');
|
||||
appendMessage('system', '❌ ' + (data.message || '构建出错'));
|
||||
}
|
||||
} catch (_e) { /* ignore malformed WS message */ }
|
||||
};
|
||||
|
||||
wsConnection.onerror = function () {
|
||||
// WebSocket not available, graceful degradation
|
||||
updatePreviewStatus('waiting', '离线模式');
|
||||
};
|
||||
|
||||
wsConnection.onclose = function () {
|
||||
wsConnection = null;
|
||||
};
|
||||
} catch (_e) {
|
||||
// WebSocket connection failed, graceful degradation
|
||||
updatePreviewStatus('waiting', '离线模式');
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Logout ---- */
|
||||
|
|
|
|||
|
|
@ -1205,7 +1205,34 @@ body {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-content input[type="email"] {
|
||||
.modal-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.modal-label .required {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.modal-label .optional {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.modal-error {
|
||||
color: #f87171;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.modal-content input[type="email"],
|
||||
.modal-content input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.7rem 1rem;
|
||||
font-size: 1rem;
|
||||
|
|
@ -1214,15 +1241,15 @@ body {
|
|||
border: 2px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
margin-bottom: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.modal-content input[type="email"]:focus {
|
||||
.modal-content input[type="email"]:focus,
|
||||
.modal-content input[type="text"]:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.modal-content input[type="email"]::placeholder {
|
||||
.modal-content input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
|
@ -1232,6 +1259,139 @@ body {
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* ---- Split-screen / Dev Mode ---- */
|
||||
.chat-layout.dev-mode {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-layout.dev-mode .chat-main {
|
||||
min-width: 360px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* ---- Resizer (draggable split line) ---- */
|
||||
.resizer {
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: linear-gradient(180deg, transparent, rgba(59, 130, 246, 0.4), transparent);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.resizer:hover,
|
||||
.resizer.resizing {
|
||||
background: linear-gradient(180deg, transparent, rgba(59, 130, 246, 0.8), transparent);
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
/* ---- Preview Panel ---- */
|
||||
.preview-panel {
|
||||
min-width: 360px;
|
||||
background: #0a0e1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 1rem;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.preview-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.preview-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
margin-left: auto;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-dot.status-waiting {
|
||||
background: #6b7280;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.status-building {
|
||||
background: #3b82f6;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-dot.status-done {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.status-dot.status-error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
padding: 0.3rem 0.6rem;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preview-btn:hover {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--text);
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
flex: 1;
|
||||
width: calc(100% - 24px);
|
||||
height: calc(100% - 24px);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
margin: 12px;
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.15);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ---- Animation ---- */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
|
|
|
|||
Loading…
Reference in New Issue