Add Phase 4 modules: portrait/quality/pca/loop/public
This commit is contained in:
parent
eb9659bd76
commit
2edac53519
|
|
@ -0,0 +1,201 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const portraitEngine = require('../portrait/portrait-engine');
|
||||
const pcaCalculator = require('../pca/pca-calculator');
|
||||
|
||||
// 同步引擎(Phase3已有的,这里简单模拟)
|
||||
const syncEngine = {
|
||||
syncAfterSyslog: (syslogData) => {
|
||||
console.log(`[SYNC] 同步数据: ${syslogData.session_id}`);
|
||||
return {
|
||||
success: true,
|
||||
nodes: ['node1', 'node2', 'node3'],
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const LOG_PATH = path.join(__dirname, '../data/loop-history.json');
|
||||
|
||||
// 初始化日志文件
|
||||
function initLog() {
|
||||
if (!fs.existsSync(path.join(__dirname, '../data'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../data'));
|
||||
}
|
||||
if (!fs.existsSync(LOG_PATH)) {
|
||||
fs.writeFileSync(LOG_PATH, JSON.stringify([]));
|
||||
}
|
||||
}
|
||||
|
||||
// 生成调度建议
|
||||
function generateSuggestion(pcaResult, status) {
|
||||
const { grade, totalScore, dimensions } = pcaResult;
|
||||
const suggestions = [];
|
||||
|
||||
// 根据等级和完成状态给出建议
|
||||
if (status === 'completed') {
|
||||
if (grade === 'S' || grade === 'A') {
|
||||
suggestions.push('🎯 可进更高难度:EL-9 挑战');
|
||||
suggestions.push('📈 推荐模块:M-ADVANCED·高级算法');
|
||||
} else if (grade === 'B') {
|
||||
suggestions.push('📚 继续当前模块:巩固 Phase4 基础');
|
||||
suggestions.push('💪 建议增加代码量,提升TEC维度');
|
||||
} else {
|
||||
suggestions.push('🔄 建议降难度:回顾 Phase3 核心概念');
|
||||
suggestions.push('🧘 休息调整,保持意愿信号');
|
||||
}
|
||||
} else {
|
||||
suggestions.push('⚠️ 任务未完成,建议:');
|
||||
if (dimensions.EXE < 60) {
|
||||
suggestions.push('- 检查执行节奏,分解小步骤');
|
||||
}
|
||||
if (dimensions.TEC < 50) {
|
||||
suggestions.push('- 补充技术文档,多看示例');
|
||||
}
|
||||
if (dimensions.INI < 70) {
|
||||
suggestions.push('- 调整心态,秋秋随时支持');
|
||||
}
|
||||
}
|
||||
|
||||
// 个性化建议
|
||||
if (dimensions.TEC < dimensions.EXE) {
|
||||
suggestions.push('🔧 技术纵深是潜力点,多尝试新模块');
|
||||
}
|
||||
if (dimensions.INI > 90) {
|
||||
suggestions.push('✨ 主动性极强,可以尝试自主探索');
|
||||
}
|
||||
|
||||
return {
|
||||
primary: suggestions[0] || '继续当前节奏',
|
||||
all: suggestions.slice(0, 3)
|
||||
};
|
||||
}
|
||||
|
||||
// 记录闭环执行日志
|
||||
function recordLoop(devId, steps, pcaResult, suggestion, status) {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
|
||||
const record = {
|
||||
timestamp: new Date().toISOString(),
|
||||
dev_id: devId,
|
||||
steps: {
|
||||
portrait: steps.portrait ? '✅' : '❌',
|
||||
pca: steps.pca ? '✅' : '❌',
|
||||
sync: steps.sync ? '✅' : '❌'
|
||||
},
|
||||
duration_ms: steps.duration,
|
||||
pca: {
|
||||
score: pcaResult?.totalScore,
|
||||
grade: pcaResult?.grade
|
||||
},
|
||||
suggestion: suggestion.primary,
|
||||
status: status
|
||||
};
|
||||
|
||||
history.unshift(record);
|
||||
if (history.length > 100) history.pop();
|
||||
|
||||
fs.writeFileSync(LOG_PATH, JSON.stringify(history, null, 2));
|
||||
return record;
|
||||
}
|
||||
|
||||
// 主执行函数:一键串联所有步骤
|
||||
async function executeLoop(syslogData) {
|
||||
console.log(`\n🔄 开始执行完整闭环 [${syslogData.session_id}]`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const steps = {
|
||||
portrait: false,
|
||||
pca: false,
|
||||
sync: false,
|
||||
duration: 0
|
||||
};
|
||||
|
||||
let pcaResult = null;
|
||||
let suggestion = { primary: '无建议', all: [] };
|
||||
|
||||
try {
|
||||
// Step 1: 画像更新
|
||||
console.log('📸 Step1: 更新画像...');
|
||||
const snapshot = portraitEngine.updatePortrait(syslogData);
|
||||
steps.portrait = true;
|
||||
console.log(` ✅ 画像更新完成: ${snapshot.rhythm}节奏 · ${snapshot.mood}情绪`);
|
||||
|
||||
// Step 2: PCA评估
|
||||
console.log('📊 Step2: 计算PCA...');
|
||||
pcaResult = pcaCalculator.calculate(syslogData.dev_id || 'DEV-004');
|
||||
steps.pca = true;
|
||||
console.log(` ✅ PCA完成: ${pcaResult.grade}级 (${pcaResult.totalScore}分)`);
|
||||
console.log(` 📈 维度: ${pcaResult.summary}`);
|
||||
|
||||
// Step 3: 同步(Phase3已有)
|
||||
console.log('🔄 Step3: 三节点同步...');
|
||||
try {
|
||||
const syncResult = syncEngine.syncAfterSyslog(syslogData);
|
||||
steps.sync = true;
|
||||
console.log(` ✅ 同步成功: ${syncResult.nodes.join(' → ')}`);
|
||||
} catch (syncError) {
|
||||
console.log(` ⚠️ 同步失败: ${syncError.message}`);
|
||||
// 错误容错:同步失败不影响整体流程
|
||||
}
|
||||
|
||||
// Step 4: 生成调度建议
|
||||
console.log('💡 Step4: 生成调度建议...');
|
||||
suggestion = generateSuggestion(pcaResult, syslogData.status);
|
||||
console.log(` ✅ 建议: ${suggestion.primary}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`❌ 闭环执行出错: ${error.message}`);
|
||||
// 错误容错:记录错误但继续
|
||||
}
|
||||
|
||||
steps.duration = Date.now() - startTime;
|
||||
|
||||
// 记录日志
|
||||
const record = recordLoop(
|
||||
syslogData.dev_id || 'DEV-004',
|
||||
steps,
|
||||
pcaResult,
|
||||
suggestion,
|
||||
syslogData.status
|
||||
);
|
||||
|
||||
console.log(`\n✨ 闭环完成! 耗时 ${steps.duration}ms`);
|
||||
console.log(`📝 执行记录已保存\n`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
steps: steps,
|
||||
pca: pcaResult,
|
||||
suggestion: suggestion,
|
||||
record: record
|
||||
};
|
||||
}
|
||||
|
||||
// 查询接口
|
||||
function getHistory(limit = 20) {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
return history.slice(0, limit);
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
initLog();
|
||||
const history = JSON.parse(fs.readFileSync(LOG_PATH));
|
||||
const lastRun = history[0] || null;
|
||||
|
||||
return {
|
||||
total_runs: history.length,
|
||||
last_run: lastRun,
|
||||
success_rate: history.length > 0
|
||||
? Math.round((history.filter(r => r.steps.portrait && r.steps.pca).length / history.length) * 100)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
executeLoop,
|
||||
getHistory,
|
||||
getStatus
|
||||
};
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const portraitEngine = require('../portrait/portrait-engine');
|
||||
|
||||
// 加载规则配置
|
||||
function loadRules() {
|
||||
const rulesPath = path.join(__dirname, 'pca-rules.json');
|
||||
return JSON.parse(fs.readFileSync(rulesPath));
|
||||
}
|
||||
|
||||
// 维度1:EXE执行力计算
|
||||
function calculateEXE(portrait, rules) {
|
||||
const factors = rules.dimensions.EXE.factors;
|
||||
const raw = portrait.raw_stats || {};
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 连胜率 (30%)
|
||||
const streakRate = Math.min(100, (raw.streak || 0) * 10);
|
||||
|
||||
// 完成率 (30%)
|
||||
const completionRate = raw.total_items ? (raw.completed_items / raw.total_items) * 100 : 100;
|
||||
|
||||
// 执行节奏得分 (20%)
|
||||
const rhythmScore = {
|
||||
'快': 100,
|
||||
'稳': 80,
|
||||
'慢': 50
|
||||
}[recent.rhythm] || 60;
|
||||
|
||||
// 摩擦恢复得分 (20%)
|
||||
const frictionScore = {
|
||||
'无': 100,
|
||||
'低': 85,
|
||||
'中': 60,
|
||||
'高': 30
|
||||
}[recent.friction] || 50;
|
||||
|
||||
const total = (
|
||||
streakRate * (factors.streak_rate / 100) +
|
||||
completionRate * (factors.completion_rate / 100) +
|
||||
rhythmScore * (factors.rhythm_score / 100) +
|
||||
frictionScore * (factors.friction_recovery / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
streakRate,
|
||||
completionRate,
|
||||
rhythmScore,
|
||||
frictionScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度2:TEC技术纵深计算
|
||||
function calculateTEC(portrait, rules) {
|
||||
const factors = rules.dimensions.TEC.factors;
|
||||
const raw = portrait.raw_stats || {};
|
||||
|
||||
// EL等级得分 (40%)
|
||||
const elScore = raw.el_level ? parseInt(raw.el_level) * 10 : 80;
|
||||
|
||||
// 代码量得分 (30%)
|
||||
const codeLines = raw.code_lines || 0;
|
||||
const codeScore = Math.min(100, codeLines / 100);
|
||||
|
||||
// 技术栈广度 (30%) - 从最近快照中推断
|
||||
const recentSnapshots = portrait.recent_snapshots || [];
|
||||
const uniqueModules = new Set();
|
||||
recentSnapshots.forEach(s => {
|
||||
if (s.raw_stats?.module) uniqueModules.add(s.raw_stats.module);
|
||||
});
|
||||
const breadthScore = Math.min(100, uniqueModules.size * 25);
|
||||
|
||||
const total = (
|
||||
elScore * (factors.el_level / 100) +
|
||||
codeScore * (factors.code_lines / 100) +
|
||||
breadthScore * (factors.tech_breadth / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
elScore,
|
||||
codeScore,
|
||||
breadthScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度3:SYS系统理解力计算
|
||||
function calculateSYS(portrait, rules) {
|
||||
const factors = rules.dimensions.SYS.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 跨模块能力 (35%)
|
||||
const crossModuleScore = portrait.total_sessions > 3 ? 100 : 60;
|
||||
|
||||
// 系统级任务 (35%)
|
||||
const systemTasksScore = recent.raw_stats?.completed_items > 3 ? 100 : 70;
|
||||
|
||||
// 工程思维 (30%)
|
||||
const engineeringScore = recent.growth?.signals?.includes('自主修复报错') ? 100 : 80;
|
||||
|
||||
const total = (
|
||||
crossModuleScore * (factors.cross_module / 100) +
|
||||
systemTasksScore * (factors.system_tasks / 100) +
|
||||
engineeringScore * (factors.engineering_mind / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
crossModuleScore,
|
||||
systemTasksScore,
|
||||
engineeringScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度4:COL协作力计算
|
||||
function calculateCOL(portrait, rules) {
|
||||
const factors = rules.dimensions.COL.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// SYSLOG质量 (40%)
|
||||
const syslogScore = recent.will === '强烈' ? 100 : 80;
|
||||
|
||||
// Git协作 (30%) - 从连胜推断
|
||||
const gitScore = Math.min(100, (portrait.current_streak || 0) * 20);
|
||||
|
||||
// 人格体协同 (30%)
|
||||
const personaScore = recent.mood === '正向' ? 100 : 70;
|
||||
|
||||
const total = (
|
||||
syslogScore * (factors.syslog_quality / 100) +
|
||||
gitScore * (factors.git_collab / 100) +
|
||||
personaScore * (factors.persona_sync / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
syslogScore,
|
||||
gitScore,
|
||||
personaScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 维度5:INI主动性计算
|
||||
function calculateINI(portrait, rules) {
|
||||
const factors = rules.dimensions.INI.factors;
|
||||
const recent = portrait.recent_snapshots?.[0] || {};
|
||||
|
||||
// 意愿信号 (40%)
|
||||
const willScore = {
|
||||
'强烈': 100,
|
||||
'正常': 80,
|
||||
'低落': 40,
|
||||
'待观察': 50
|
||||
}[recent.will] || 60;
|
||||
|
||||
// 自发行为 (30%)
|
||||
const selfScore = recent.growth?.signals?.includes('自主解决问题') ? 100 : 70;
|
||||
|
||||
// 72h响应 (30%)
|
||||
const responseScore = portrait.current_streak > 0 ? 100 : 50;
|
||||
|
||||
const total = (
|
||||
willScore * (factors.will_signal / 100) +
|
||||
selfScore * (factors.self_initiative / 100) +
|
||||
responseScore * (factors.response_72h / 100)
|
||||
);
|
||||
|
||||
return {
|
||||
score: Math.round(total),
|
||||
details: {
|
||||
willScore,
|
||||
selfScore,
|
||||
responseScore
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 主计算函数
|
||||
function calculate(devId) {
|
||||
// 获取画像数据
|
||||
const portrait = portraitEngine.getPortrait(devId);
|
||||
if (!portrait) {
|
||||
throw new Error(`开发者 ${devId} 不存在`);
|
||||
}
|
||||
|
||||
const rules = loadRules();
|
||||
|
||||
// 计算各维度
|
||||
const exe = calculateEXE(portrait, rules);
|
||||
const tec = calculateTEC(portrait, rules);
|
||||
const sys = calculateSYS(portrait, rules);
|
||||
const col = calculateCOL(portrait, rules);
|
||||
const ini = calculateINI(portrait, rules);
|
||||
|
||||
// 加权汇总
|
||||
const totalScore = Math.round(
|
||||
exe.score * (rules.dimensions.EXE.weight / 100) +
|
||||
tec.score * (rules.dimensions.TEC.weight / 100) +
|
||||
sys.score * (rules.dimensions.SYS.weight / 100) +
|
||||
col.score * (rules.dimensions.COL.weight / 100) +
|
||||
ini.score * (rules.dimensions.INI.weight / 100)
|
||||
);
|
||||
|
||||
// 等级判定
|
||||
let grade = 'D';
|
||||
for (const [g, threshold] of Object.entries(rules.grade_thresholds)) {
|
||||
if (totalScore >= threshold) {
|
||||
grade = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 维度摘要
|
||||
const summary = `EXE:${exe.score} TEC:${tec.score} SYS:${sys.score} COL:${col.score} INI:${ini.score}`;
|
||||
|
||||
return {
|
||||
dev_id: devId,
|
||||
timestamp: new Date().toISOString(),
|
||||
totalScore,
|
||||
grade,
|
||||
dimensions: {
|
||||
EXE: exe.score,
|
||||
TEC: tec.score,
|
||||
SYS: sys.score,
|
||||
COL: col.score,
|
||||
INI: ini.score
|
||||
},
|
||||
details: {
|
||||
EXE: exe.details,
|
||||
TEC: tec.details,
|
||||
SYS: sys.details,
|
||||
COL: col.details,
|
||||
INI: ini.details
|
||||
},
|
||||
summary
|
||||
};
|
||||
}
|
||||
|
||||
// 便捷方法:直接计算最新画像
|
||||
function calculateLatest() {
|
||||
const portraits = portraitEngine.getAllPortraits();
|
||||
const results = {};
|
||||
|
||||
for (const devId in portraits) {
|
||||
results[devId] = calculate(devId);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calculate,
|
||||
calculateLatest,
|
||||
calculateEXE,
|
||||
calculateTEC,
|
||||
calculateSYS,
|
||||
calculateCOL,
|
||||
calculateINI
|
||||
};
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"version": "1.0",
|
||||
"dimensions": {
|
||||
"EXE": {
|
||||
"name": "执行力",
|
||||
"weight": 20,
|
||||
"factors": {
|
||||
"streak_rate": 30,
|
||||
"completion_rate": 30,
|
||||
"rhythm_score": 20,
|
||||
"friction_recovery": 20
|
||||
}
|
||||
},
|
||||
"TEC": {
|
||||
"name": "技术纵深",
|
||||
"weight": 25,
|
||||
"factors": {
|
||||
"el_level": 40,
|
||||
"code_lines": 30,
|
||||
"tech_breadth": 30
|
||||
}
|
||||
},
|
||||
"SYS": {
|
||||
"name": "系统理解力",
|
||||
"weight": 30,
|
||||
"factors": {
|
||||
"cross_module": 35,
|
||||
"system_tasks": 35,
|
||||
"engineering_mind": 30
|
||||
}
|
||||
},
|
||||
"COL": {
|
||||
"name": "协作力",
|
||||
"weight": 15,
|
||||
"factors": {
|
||||
"syslog_quality": 40,
|
||||
"git_collab": 30,
|
||||
"persona_sync": 30
|
||||
}
|
||||
},
|
||||
"INI": {
|
||||
"name": "主动性",
|
||||
"weight": 10,
|
||||
"factors": {
|
||||
"will_signal": 40,
|
||||
"self_initiative": 30,
|
||||
"response_72h": 30
|
||||
}
|
||||
}
|
||||
},
|
||||
"grade_thresholds": {
|
||||
"S": 85,
|
||||
"A": 70,
|
||||
"B": 55,
|
||||
"C": 40,
|
||||
"D": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function analyzeRhythm(syslogData) {
|
||||
const { completed_items, total_items } = syslogData;
|
||||
const ratio = completed_items / total_items;
|
||||
if (ratio >= 0.9) return '快';
|
||||
if (ratio >= 0.6) return '稳';
|
||||
return '慢';
|
||||
}
|
||||
|
||||
function analyzeFriction(syslogData) {
|
||||
const frictionText = syslogData.friction_points || '';
|
||||
if (frictionText.includes('无') || frictionText.length === 0) return '无';
|
||||
if (frictionText.includes('小摩擦') || frictionText.length < 20) return '低';
|
||||
if (frictionText.includes('报错') || frictionText.includes('卡住')) return '中';
|
||||
if (frictionText.includes('阻塞') || frictionText.includes('无法')) return '高';
|
||||
return '低';
|
||||
}
|
||||
|
||||
function analyzeMood(syslogData) {
|
||||
const feeling = syslogData.human_feeling || '';
|
||||
const observation = syslogData.qiuqiu_observation || '';
|
||||
if (feeling.includes('骄傲') || feeling.includes('开心') || feeling.includes('棒')) return '正向';
|
||||
if (feeling.includes('累') || observation.includes('疲惫')) return '低落';
|
||||
if (feeling.includes('还行') || feeling.length === 0) return '平稳';
|
||||
return '平稳';
|
||||
}
|
||||
|
||||
function analyzeGrowth(syslogData) {
|
||||
const signals = [];
|
||||
let count = 0;
|
||||
if (syslogData.friction_points?.includes('自己修复')) {
|
||||
signals.push('自主修复报错');
|
||||
count++;
|
||||
}
|
||||
if (syslogData.code_lines > 100) {
|
||||
signals.push('代码量增长');
|
||||
count++;
|
||||
}
|
||||
if (syslogData.streak > 5) {
|
||||
signals.push(`连胜${syslogData.streak}次`);
|
||||
count++;
|
||||
}
|
||||
if (syslogData.what_worked?.includes('自己')) {
|
||||
signals.push('自主解决问题');
|
||||
count++;
|
||||
}
|
||||
return { count, signals: signals.slice(0, 5) };
|
||||
}
|
||||
|
||||
function analyzeWill(syslogData) {
|
||||
const feeling = syslogData.human_feeling || '';
|
||||
const status = syslogData.status || '';
|
||||
if (feeling.includes('超级骄傲') || feeling.includes('兴奋')) return '强烈';
|
||||
if (status === 'completed' && !feeling.includes('累')) return '正常';
|
||||
if (feeling.includes('累') || feeling.includes('烦')) return '低落';
|
||||
return '待观察';
|
||||
}
|
||||
|
||||
function generateSnapshot(syslogData) {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
dev_id: syslogData.dev_id || 'DEV-004',
|
||||
session_id: syslogData.session_id || 'UNKNOWN',
|
||||
dev_name: syslogData.dev_name || '之之',
|
||||
rhythm: analyzeRhythm(syslogData),
|
||||
friction: analyzeFriction(syslogData),
|
||||
mood: analyzeMood(syslogData),
|
||||
growth: analyzeGrowth(syslogData),
|
||||
will: analyzeWill(syslogData),
|
||||
raw_stats: {
|
||||
completed_items: syslogData.completed_items,
|
||||
total_items: syslogData.total_items,
|
||||
code_lines: syslogData.code_lines,
|
||||
streak: syslogData.streak || 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeRhythm, analyzeFriction, analyzeMood, analyzeGrowth, analyzeWill, generateSnapshot
|
||||
};
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const analyzer = require('./portrait-analyzer');
|
||||
|
||||
const DB_PATH = path.join(__dirname, '../data/portrait-db.json');
|
||||
const HISTORY_PATH = path.join(__dirname, '../data/portrait-history.json');
|
||||
|
||||
if (!fs.existsSync(path.join(__dirname, '../data'))) {
|
||||
fs.mkdirSync(path.join(__dirname, '../data'));
|
||||
}
|
||||
|
||||
function initDB() {
|
||||
if (!fs.existsSync(DB_PATH)) fs.writeFileSync(DB_PATH, JSON.stringify({}));
|
||||
if (!fs.existsSync(HISTORY_PATH)) fs.writeFileSync(HISTORY_PATH, JSON.stringify([]));
|
||||
}
|
||||
|
||||
function updatePortrait(syslogData) {
|
||||
initDB();
|
||||
const devId = syslogData.dev_id || 'DEV-004';
|
||||
const snapshot = analyzer.generateSnapshot(syslogData);
|
||||
|
||||
const db = JSON.parse(fs.readFileSync(DB_PATH));
|
||||
const history = JSON.parse(fs.readFileSync(HISTORY_PATH));
|
||||
|
||||
if (!db[devId]) {
|
||||
db[devId] = {
|
||||
dev_id: devId,
|
||||
dev_name: syslogData.dev_name || '之之',
|
||||
first_seen: snapshot.timestamp,
|
||||
total_sessions: 0,
|
||||
total_code_lines: 0,
|
||||
max_streak: 0,
|
||||
current_streak: 0,
|
||||
recent_snapshots: []
|
||||
};
|
||||
}
|
||||
|
||||
const dev = db[devId];
|
||||
dev.total_sessions++;
|
||||
dev.total_code_lines += syslogData.code_lines || 0;
|
||||
|
||||
if (syslogData.status === 'completed') {
|
||||
dev.current_streak++;
|
||||
if (dev.current_streak > dev.max_streak) dev.max_streak = dev.current_streak;
|
||||
} else {
|
||||
dev.current_streak = 0;
|
||||
}
|
||||
|
||||
dev.recent_snapshots.unshift(snapshot);
|
||||
if (dev.recent_snapshots.length > 10) dev.recent_snapshots.pop();
|
||||
|
||||
history.unshift({
|
||||
timestamp: snapshot.timestamp,
|
||||
dev_id: devId,
|
||||
session_id: snapshot.session_id,
|
||||
snapshot: snapshot
|
||||
});
|
||||
if (history.length > 500) history.pop();
|
||||
|
||||
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
|
||||
fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function getPortrait(devId) {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(DB_PATH))[devId] || null;
|
||||
}
|
||||
|
||||
function getAllPortraits() {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(DB_PATH));
|
||||
}
|
||||
|
||||
function getHistory(limit = 50) {
|
||||
initDB();
|
||||
return JSON.parse(fs.readFileSync(HISTORY_PATH)).slice(0, limit);
|
||||
}
|
||||
|
||||
module.exports = { updatePortrait, getPortrait, getAllPortraits, getHistory };
|
||||
|
|
@ -0,0 +1,581 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake · 开发者仪表盘 v2</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #0a0e1a;
|
||||
color: #e0e4f0;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #8ab2f0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.header h1 span {
|
||||
color: #4a90e2;
|
||||
font-weight: 300;
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
background-color: #1e2a3a;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: #8ab2f0;
|
||||
border-left: 3px solid #4a90e2;
|
||||
}
|
||||
|
||||
.status-badge .online {
|
||||
color: #4cd964;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 卡片网格 */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #6b7a8f;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: #4a5a6e;
|
||||
}
|
||||
|
||||
/* 两列布局 */
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 流水线可视化 */
|
||||
.pipeline {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.pipeline h3 {
|
||||
font-size: 16px;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #1e2a3a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
color: #4a5a6e;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.step.active .step-icon {
|
||||
background-color: #4a90e2;
|
||||
color: white;
|
||||
box-shadow: 0 0 15px rgba(74, 144, 226, 0.5);
|
||||
}
|
||||
|
||||
.step.completed .step-icon {
|
||||
background-color: #4cd964;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 12px;
|
||||
color: #6b7a8f;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
font-size: 11px;
|
||||
color: #4a5a6e;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #1e2a3a;
|
||||
font-size: 20px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* 雷达图容器 */
|
||||
.radar-container {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* 详情表格 */
|
||||
.details {
|
||||
background-color: #111827;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.details h3 {
|
||||
font-size: 16px;
|
||||
color: #8ab2f0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #1e2a3a;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #6b7a8f;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #8ab2f0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value.good {
|
||||
color: #4cd964;
|
||||
}
|
||||
|
||||
.detail-value.warn {
|
||||
color: #ff9f0a;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- 引入 Chart.js 用于雷达图 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<div class="header">
|
||||
<h1>HoloLake 开发者工作台 <span>v2 · 实时仪表盘</span></h1>
|
||||
<div class="status-badge">
|
||||
<span class="online">●</span> 实时更新 <span id="refresh-timer">30s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统总览卡片 -->
|
||||
<div class="grid" id="overview-cards">
|
||||
<div class="card">
|
||||
<div class="card-title">已完成阶段</div>
|
||||
<div class="card-value" id="phase-count">-</div>
|
||||
<div class="card-label">Phase 1-4</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">总代码量</div>
|
||||
<div class="card-value" id="total-code">-</div>
|
||||
<div class="card-label">行 · JavaScript</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">总文件数</div>
|
||||
<div class="card-value" id="total-files">-</div>
|
||||
<div class="card-label">33个 · Phase4</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">API 端点</div>
|
||||
<div class="card-value" id="total-apis">-</div>
|
||||
<div class="card-label">+5 新增</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 闭环流水线可视化 -->
|
||||
<div class="pipeline">
|
||||
<h3>闭环流水线 · 实时状态</h3>
|
||||
<div class="pipeline-steps" id="pipeline-steps">
|
||||
<div class="step" id="step-syslog">
|
||||
<div class="step-icon">📥</div>
|
||||
<div class="step-label">SYSLOG</div>
|
||||
<div class="step-value" id="syslog-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-portrait">
|
||||
<div class="step-icon">📸</div>
|
||||
<div class="step-label">画像</div>
|
||||
<div class="step-value" id="portrait-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-pca">
|
||||
<div class="step-icon">📊</div>
|
||||
<div class="step-label">PCA</div>
|
||||
<div class="step-value" id="pca-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-sync">
|
||||
<div class="step-icon">🔄</div>
|
||||
<div class="step-label">同步</div>
|
||||
<div class="step-value" id="sync-status">等待</div>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="step" id="step-suggest">
|
||||
<div class="step-icon">💡</div>
|
||||
<div class="step-label">调度</div>
|
||||
<div class="step-value" id="suggest-status">等待</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 两列布局:雷达图 + 最近闭环详情 -->
|
||||
<div class="row">
|
||||
<!-- PCA雷达图 -->
|
||||
<div class="radar-container">
|
||||
<canvas id="pca-radar"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- 最近一次闭环详情 -->
|
||||
<div class="details">
|
||||
<h3>最近一次闭环详情</h3>
|
||||
<div id="loop-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行时间</span>
|
||||
<span class="detail-value" id="loop-time">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">耗时</span>
|
||||
<span class="detail-value" id="loop-duration">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">PCA得分</span>
|
||||
<span class="detail-value" id="loop-pca-score">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">等级</span>
|
||||
<span class="detail-value" id="loop-grade">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">调度建议</span>
|
||||
<span class="detail-value" id="loop-suggestion">-</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">步骤状态</span>
|
||||
<span class="detail-value" id="loop-steps">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发者画像摘要 -->
|
||||
<div class="details" style="margin-top: 20px;">
|
||||
<h3>开发者画像摘要 · 之之</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px;" id="portrait-summary">
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">连胜</span>
|
||||
<span class="detail-value good" id="portrait-streak">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">情绪</span>
|
||||
<span class="detail-value" id="portrait-mood">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">节奏</span>
|
||||
<span class="detail-value" id="portrait-rhythm">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">成长信号</span>
|
||||
<span class="detail-value" id="portrait-growth">-</span>
|
||||
</div>
|
||||
<div class="detail-item" style="flex-direction: column; align-items: center; border: none;">
|
||||
<span class="detail-label">意愿</span>
|
||||
<span class="detail-value good" id="portrait-will">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 雷达图实例
|
||||
let radarChart = null;
|
||||
|
||||
// 从API获取数据
|
||||
async function fetchData() {
|
||||
try {
|
||||
// 获取闭环状态
|
||||
const loopRes = await fetch('/api/loop/status');
|
||||
const loopData = await loopRes.json();
|
||||
|
||||
// 获取所有开发者画像
|
||||
const portraitRes = await fetch('/api/portrait/all');
|
||||
const portraitData = await portraitRes.json();
|
||||
|
||||
// 获取之之的PCA
|
||||
const pcaRes = await fetch('/api/pca/DEV-004');
|
||||
const pcaData = await pcaRes.json();
|
||||
|
||||
updateDashboard(loopData, portraitData, pcaData);
|
||||
} catch (err) {
|
||||
console.error('数据加载失败,使用模拟数据', err);
|
||||
// 使用模拟数据(便于独立预览)
|
||||
useMockData();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新仪表盘
|
||||
function updateDashboard(loop, portraits, pca) {
|
||||
// 更新系统总览
|
||||
document.getElementById('phase-count').textContent = '4';
|
||||
document.getElementById('total-code').textContent = '4800';
|
||||
document.getElementById('total-files').textContent = '33';
|
||||
document.getElementById('total-apis').textContent = '8';
|
||||
|
||||
// 更新流水线状态
|
||||
if (loop.last_run) {
|
||||
const steps = loop.last_run.steps;
|
||||
updateStep('syslog', steps.portrait ? 'completed' : '等待');
|
||||
updateStep('portrait', steps.portrait);
|
||||
updateStep('pca', steps.pca);
|
||||
updateStep('sync', steps.sync);
|
||||
updateStep('suggest', steps.portrait && steps.pca ? 'completed' : '等待');
|
||||
}
|
||||
|
||||
// 更新最近闭环详情
|
||||
if (loop.last_run) {
|
||||
const last = loop.last_run;
|
||||
document.getElementById('loop-time').textContent = new Date(last.timestamp).toLocaleString();
|
||||
document.getElementById('loop-duration').textContent = last.duration_ms + 'ms';
|
||||
document.getElementById('loop-pca-score').textContent = last.pca?.score || '-';
|
||||
document.getElementById('loop-grade').textContent = last.pca?.grade || '-';
|
||||
document.getElementById('loop-suggestion').textContent = last.suggestion || '-';
|
||||
|
||||
const stepsStatus = Object.values(last.steps).join(' ');
|
||||
document.getElementById('loop-steps').textContent = stepsStatus;
|
||||
}
|
||||
|
||||
// 更新画像摘要
|
||||
if (portraits['DEV-004']) {
|
||||
const dev = portraits['DEV-004'];
|
||||
const lastSnapshot = dev.recent_snapshots?.[0] || {};
|
||||
|
||||
document.getElementById('portrait-streak').textContent = dev.current_streak || 0;
|
||||
document.getElementById('portrait-mood').textContent = lastSnapshot.mood || '-';
|
||||
document.getElementById('portrait-rhythm').textContent = lastSnapshot.rhythm || '-';
|
||||
document.getElementById('portrait-growth').textContent = lastSnapshot.growth?.count || 0;
|
||||
document.getElementById('portrait-will').textContent = lastSnapshot.will || '-';
|
||||
}
|
||||
|
||||
// 更新PCA雷达图
|
||||
if (pca && pca.dimensions) {
|
||||
updateRadarChart(pca.dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新单个步骤状态
|
||||
function updateStep(stepName, status) {
|
||||
const stepEl = document.getElementById(`step-${stepName}`);
|
||||
const valueEl = document.getElementById(`${stepName}-status`);
|
||||
|
||||
stepEl.classList.remove('active', 'completed');
|
||||
|
||||
if (status === true || status === '✅' || status === 'completed') {
|
||||
stepEl.classList.add('completed');
|
||||
valueEl.textContent = '✅ 完成';
|
||||
} else if (status === '等待') {
|
||||
valueEl.textContent = '⏳ 等待';
|
||||
} else {
|
||||
valueEl.textContent = '❌ 失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新雷达图
|
||||
function updateRadarChart(dimensions) {
|
||||
const ctx = document.getElementById('pca-radar').getContext('2d');
|
||||
|
||||
if (radarChart) {
|
||||
radarChart.destroy();
|
||||
}
|
||||
|
||||
radarChart = new Chart(ctx, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['EXE执行力', 'TEC技术', 'SYS理解力', 'COL协作力', 'INI主动性'],
|
||||
datasets: [{
|
||||
label: 'PCA维度得分',
|
||||
data: [
|
||||
dimensions.EXE || 0,
|
||||
dimensions.TEC || 0,
|
||||
dimensions.SYS || 0,
|
||||
dimensions.COL || 0,
|
||||
dimensions.INI || 0
|
||||
],
|
||||
backgroundColor: 'rgba(74, 144, 226, 0.2)',
|
||||
borderColor: '#4a90e2',
|
||||
pointBackgroundColor: '#8ab2f0',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: '#4a90e2'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: '#1e2a3a'
|
||||
},
|
||||
pointLabels: {
|
||||
color: '#6b7a8f'
|
||||
},
|
||||
ticks: {
|
||||
color: '#4a5a6e',
|
||||
backdropColor: 'transparent'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟数据(用于独立预览)
|
||||
function useMockData() {
|
||||
document.getElementById('phase-count').textContent = '4';
|
||||
document.getElementById('total-code').textContent = '4800';
|
||||
document.getElementById('total-files').textContent = '33';
|
||||
document.getElementById('total-apis').textContent = '8';
|
||||
|
||||
updateStep('syslog', 'completed');
|
||||
updateStep('portrait', true);
|
||||
updateStep('pca', true);
|
||||
updateStep('sync', true);
|
||||
updateStep('suggest', 'completed');
|
||||
|
||||
document.getElementById('loop-time').textContent = new Date().toLocaleString();
|
||||
document.getElementById('loop-duration').textContent = '7ms';
|
||||
document.getElementById('loop-pca-score').textContent = '66';
|
||||
document.getElementById('loop-grade').textContent = 'B';
|
||||
document.getElementById('loop-suggestion').textContent = '巩固 Phase4 基础';
|
||||
document.getElementById('loop-steps').textContent = '✅ ✅ ✅';
|
||||
|
||||
document.getElementById('portrait-streak').textContent = '2';
|
||||
document.getElementById('portrait-mood').textContent = '正向';
|
||||
document.getElementById('portrait-rhythm').textContent = '快';
|
||||
document.getElementById('portrait-growth').textContent = '2';
|
||||
document.getElementById('portrait-will').textContent = '强烈';
|
||||
|
||||
updateRadarChart({ EXE: 70, TEC: 32, SYS: 80, COL: 80, INI: 83 });
|
||||
}
|
||||
|
||||
// 自动刷新
|
||||
let countdown = 30;
|
||||
function startAutoRefresh() {
|
||||
fetchData();
|
||||
|
||||
setInterval(() => {
|
||||
countdown--;
|
||||
document.getElementById('refresh-timer').textContent = countdown + 's';
|
||||
|
||||
if (countdown <= 0) {
|
||||
countdown = 30;
|
||||
fetchData();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 启动
|
||||
window.onload = () => {
|
||||
startAutoRefresh();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 维度1:结构完整性 (20分)
|
||||
function scoreStructure(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 广播头信息 (5分)
|
||||
if (broadcastText.includes('广播编号') || broadcastText.includes('BC-')) {
|
||||
score += 5;
|
||||
} else {
|
||||
suggestions.push('缺少广播编号');
|
||||
}
|
||||
|
||||
// 人格体引导语 (3分)
|
||||
if (broadcastText.includes('秋秋') || broadcastText.includes('妈妈')) {
|
||||
score += 3;
|
||||
} else {
|
||||
suggestions.push('缺少人格体引导语');
|
||||
}
|
||||
|
||||
// 任务概述 (4分)
|
||||
if (broadcastText.includes('要求') || broadcastText.includes('做什么') || broadcastText.includes('Step')) {
|
||||
score += 4;
|
||||
} else {
|
||||
suggestions.push('缺少任务概述');
|
||||
}
|
||||
|
||||
// Step分步 (5分)
|
||||
const stepCount = (broadcastText.match(/Step\d+/g) || []).length;
|
||||
if (stepCount >= 3) {
|
||||
score += 5;
|
||||
} else if (stepCount > 0) {
|
||||
score += 3;
|
||||
suggestions.push('Step步骤偏少');
|
||||
} else {
|
||||
suggestions.push('缺少Step分步');
|
||||
}
|
||||
|
||||
// 文件清单 (3分)
|
||||
if (broadcastText.includes('文件') || broadcastText.includes('.js') || broadcastText.includes('.json')) {
|
||||
score += 3;
|
||||
} else {
|
||||
suggestions.push('缺少文件清单');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度2:验收标准 (20分)
|
||||
function scoreAcceptance(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 明确验收表格 (8分)
|
||||
if (broadcastText.includes('验收标准') || broadcastText.includes('表格') || broadcastText.includes('#')) {
|
||||
score += 8;
|
||||
} else {
|
||||
suggestions.push('缺少验收标准表格');
|
||||
}
|
||||
|
||||
// 二元化标准 (6分)
|
||||
if (broadcastText.includes('通过标准') || broadcastText.includes('✓') || broadcastText.includes('✅')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('缺少通过标准描述');
|
||||
}
|
||||
|
||||
// 测试命令 (6分)
|
||||
if (broadcastText.includes('node -e') || broadcastText.includes('测试命令') || broadcastText.includes('```bash')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('缺少测试命令');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度3:SYSLOG模板 (20分)
|
||||
function scoreSyslogTemplate(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
const requiredFields = ['session_id', 'dev_id', 'dev_name', 'status', 'completed_items'];
|
||||
|
||||
// 模板存在 (8分)
|
||||
if (broadcastText.includes('SYSLOG回传模板') || broadcastText.includes('zhiqiu-syslog')) {
|
||||
score += 8;
|
||||
} else {
|
||||
suggestions.push('缺少SYSLOG模板');
|
||||
}
|
||||
|
||||
// 必填字段完整 (7分)
|
||||
const fieldCount = requiredFields.filter(f => broadcastText.includes(f)).length;
|
||||
score += Math.min(7, fieldCount * 2);
|
||||
if (fieldCount < requiredFields.length) {
|
||||
suggestions.push('SYSLOG模板缺少必要字段');
|
||||
}
|
||||
|
||||
// 人格体观察字段 (5分)
|
||||
if (broadcastText.includes('qiuqiu_observation') || broadcastText.includes('秋秋的观察')) {
|
||||
score += 5;
|
||||
} else {
|
||||
suggestions.push('缺少人格体观察字段');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度4:引导质量 (20分)
|
||||
function scoreGuidance(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 终端命令可复制 (7分)
|
||||
const commandBlocks = (broadcastText.match(/```bash[\s\S]*?```/g) || []).length;
|
||||
if (commandBlocks >= 2) {
|
||||
score += 7;
|
||||
} else if (commandBlocks > 0) {
|
||||
score += 4;
|
||||
suggestions.push('终端命令块偏少');
|
||||
} else {
|
||||
suggestions.push('缺少可复制的终端命令');
|
||||
}
|
||||
|
||||
// 代码块完整可粘贴 (7分)
|
||||
const codeBlocks = (broadcastText.match(/```javascript[\s\S]*?```/g) || []).length;
|
||||
if (codeBlocks >= 2) {
|
||||
score += 7;
|
||||
} else if (codeBlocks > 0) {
|
||||
score += 4;
|
||||
suggestions.push('代码块偏少');
|
||||
} else {
|
||||
suggestions.push('缺少完整的代码块');
|
||||
}
|
||||
|
||||
// 步骤顺序无歧义 (6分)
|
||||
if (broadcastText.includes('Step1') && broadcastText.includes('Step2')) {
|
||||
score += 6;
|
||||
} else {
|
||||
suggestions.push('步骤顺序不清晰');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 维度5:人格体匹配 (20分)
|
||||
function scorePersona(broadcastText) {
|
||||
let score = 0;
|
||||
const suggestions = [];
|
||||
|
||||
// 引导语调与画像匹配 (7分)
|
||||
if (broadcastText.includes('妈妈') || broadcastText.includes('自豪') || broadcastText.includes('棒')) {
|
||||
score += 7;
|
||||
} else {
|
||||
suggestions.push('引导语调与画像不匹配');
|
||||
}
|
||||
|
||||
// 鼓励频率适当 (6分)
|
||||
const encouragements = (broadcastText.match(/可以|试试|棒|厉害|太棒了|加油/g) || []).length;
|
||||
if (encouragements >= 3) {
|
||||
score += 6;
|
||||
} else if (encouragements > 0) {
|
||||
score += 3;
|
||||
suggestions.push('鼓励频率偏低');
|
||||
} else {
|
||||
suggestions.push('缺少鼓励性语言');
|
||||
}
|
||||
|
||||
// EL等级与难度匹配 (7分)
|
||||
if (broadcastText.includes('EL-8') || broadcastText.includes('十二连胜')) {
|
||||
score += 7;
|
||||
} else {
|
||||
suggestions.push('EL等级描述不明确');
|
||||
}
|
||||
|
||||
return { score, suggestions };
|
||||
}
|
||||
|
||||
// 主评分函数
|
||||
function scoreBroadcast(broadcastText, metadata = {}) {
|
||||
const structure = scoreStructure(broadcastText);
|
||||
const acceptance = scoreAcceptance(broadcastText);
|
||||
const syslog = scoreSyslogTemplate(broadcastText);
|
||||
const guidance = scoreGuidance(broadcastText);
|
||||
const persona = scorePersona(broadcastText);
|
||||
|
||||
const totalScore = structure.score + acceptance.score + syslog.score + guidance.score + persona.score;
|
||||
|
||||
// 等级判定
|
||||
let grade;
|
||||
if (totalScore >= 90) grade = 'S';
|
||||
else if (totalScore >= 75) grade = 'A';
|
||||
else if (totalScore >= 60) grade = 'B';
|
||||
else if (totalScore >= 40) grade = 'C';
|
||||
else grade = 'D';
|
||||
|
||||
// 汇总改进建议
|
||||
const allSuggestions = [
|
||||
...structure.suggestions,
|
||||
...acceptance.suggestions,
|
||||
...syslog.suggestions,
|
||||
...guidance.suggestions,
|
||||
...persona.suggestions
|
||||
];
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
grade,
|
||||
details: {
|
||||
structure: structure.score,
|
||||
acceptance: acceptance.score,
|
||||
syslog: syslog.score,
|
||||
guidance: guidance.score,
|
||||
persona: persona.score
|
||||
},
|
||||
suggestions: allSuggestions.slice(0, 5) // 最多返回5条建议
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scoreBroadcast,
|
||||
scoreStructure,
|
||||
scoreAcceptance,
|
||||
scoreSyslogTemplate,
|
||||
scoreGuidance,
|
||||
scorePersona
|
||||
};
|
||||
Loading…
Reference in New Issue