diff --git a/dingtalk-bot/loop/loop-engine.js b/dingtalk-bot/loop/loop-engine.js new file mode 100644 index 00000000..104b521c --- /dev/null +++ b/dingtalk-bot/loop/loop-engine.js @@ -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 +}; diff --git a/dingtalk-bot/pca/pca-calculator.js b/dingtalk-bot/pca/pca-calculator.js new file mode 100644 index 00000000..b17c2ede --- /dev/null +++ b/dingtalk-bot/pca/pca-calculator.js @@ -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 +}; diff --git a/dingtalk-bot/pca/pca-rules.json b/dingtalk-bot/pca/pca-rules.json new file mode 100644 index 00000000..ad80ae59 --- /dev/null +++ b/dingtalk-bot/pca/pca-rules.json @@ -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 + } +} diff --git a/dingtalk-bot/portrait/portrait-analyzer.js b/dingtalk-bot/portrait/portrait-analyzer.js new file mode 100644 index 00000000..bcf5a482 --- /dev/null +++ b/dingtalk-bot/portrait/portrait-analyzer.js @@ -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 +}; diff --git a/dingtalk-bot/portrait/portrait-engine.js b/dingtalk-bot/portrait/portrait-engine.js new file mode 100644 index 00000000..14cf61bb --- /dev/null +++ b/dingtalk-bot/portrait/portrait-engine.js @@ -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 }; diff --git a/dingtalk-bot/public/dashboard-v2.html b/dingtalk-bot/public/dashboard-v2.html new file mode 100644 index 00000000..33a423e3 --- /dev/null +++ b/dingtalk-bot/public/dashboard-v2.html @@ -0,0 +1,581 @@ + + +
+ + +