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:
冰朔 2026-03-11 20:15:37 +08:00 committed by GitHub
commit 5d117847af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2171 additions and 48 deletions

View File

@ -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);
}
}

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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
};

View File

@ -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,

View File

@ -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']
};
}),

View File

@ -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
};

View File

@ -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',

View File

@ -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
};

View File

@ -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
}
}
}
}
}

View File

@ -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"
}
}

View File

@ -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
});
}
})();
});

View File

@ -50,6 +50,9 @@ router.post('/message', async (req, res) => {
// 更新最后话题
memoryManager.updateLastTopic(dev_id, message);
// 自动更新用户画像
memoryManager.updateProfile(dev_id, message, result.reply);
}
res.json({

View File

@ -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;

View File

@ -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}`);
});

View File

@ -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');

View File

@ -0,0 +1,7 @@
{
"schema_version": "1.0",
"description": "系统进化日志 · 记录每次自进化事件",
"last_updated": null,
"total_events": 0,
"events": []
}

View File

@ -0,0 +1,7 @@
{
"schema_version": "1.0",
"description": "系统知识库 · 自动积累 · 每次代码生成/方案确认/技术问题解决时更新",
"last_updated": null,
"total_entries": 0,
"entries": []
}

View File

@ -0,0 +1,7 @@
{
"schema_version": "1.0",
"description": "系统模式库 · 自动识别 · 每完成一个项目+每日聚合更新",
"last_updated": null,
"total_patterns": 0,
"patterns": []
}

View File

@ -0,0 +1,7 @@
{
"schema_version": "1.0",
"description": "质量评分 · 反馈驱动 · 项目完成后自动评分",
"last_updated": null,
"total_scores": 0,
"scores": []
}

View File

@ -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>

View File

@ -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 ---- */

View File

@ -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); }