From 8eadc9570191179daa17563a0425c37d5a40c9ed Mon Sep 17 00:00:00 2001 From: zhizhi200271 Date: Sun, 15 Mar 2026 17:41:52 +0800 Subject: [PATCH] =?UTF-8?q?DEV-004:=20M-DINGTALK=20Phase=209=20=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=B8=8A=E4=BC=A0=20=C2=B7=20=E5=BE=85=E4=BA=BA?= =?UTF-8?q?=E6=A0=BC=E4=BD=93=E6=A3=80=E6=9F=A5Stream=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 24 + broadcast-generator.js | 12 + config.json | 27 + conversation-manager.js | 139 ++++ conversations/test.json | 16 + data/developer-status.json | 11 + data/loop-history.json | 34 + data/multi-sheet.js | 22 + data/notion-writeback-log.json | 9 + data/portrait-db.json | 104 +++ data/portrait-history.json | 113 +++ data/reminder-log.json | 20 + data/sheet-updater.js | 9 + dingtalk-api.js | 199 +++++ dingtalk-event-handler.js | 186 +++++ dingtalk-webhook-v3.js | 165 ++++ dingtalk/bot.js | 9 + ecosystem.config.js | 15 + git-helper.js | 9 + github-bridge.js | 209 +++++ index-stream.js | 73 ++ index.js | 80 ++ index.js.bak.phase7 | 64 ++ knowledge-base/docs/README.md | 12 + knowledge-base/docs/README.md.save | 61 ++ knowledge-base/docs/broadcast-rules.md | 26 + knowledge-base/docs/faq.md | 23 + knowledge-base/kb-index.json | 212 +++++ knowledge-base/kb-manager.js | 209 +++++ llm-engine.js | 178 ++++ loop/loop-engine.js | 201 +++++ message-router.js | 245 ++++++ message-router.js.bak.phase7 | 130 +++ notion-test-nossl.js | 74 ++ notion-test.js | 76 ++ package-lock.json | 1050 ++++++++++++++++++++++++ package.json | 23 + pca/pca-calculator.js | 268 ++++++ pca/pca-rules.json | 58 ++ portrait/portrait-analyzer.js | 83 ++ portrait/portrait-engine.js | 81 ++ public/dashboard-v2.html | 581 +++++++++++++ quality/broadcast-scorer.js | 228 +++++ scheduler/cron-jobs.js | 97 +++ scheduler/reminder.js | 204 +++++ server.js | 119 +++ setup-phase1-phase2.js | 155 ++++ sync/sync-engine.js | 133 +++ sync/sync-to-github.js | 102 +++ sync/sync-to-notion.js | 403 +++++++++ syslog-parser.js | 74 ++ syslog-receiver.js | 9 + test-stream.js | 15 + webhook/github-webhook.js | 153 ++++ webhook/hot-reload.js | 114 +++ 56 files changed, 6950 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 broadcast-generator.js create mode 100644 config.json create mode 100644 conversation-manager.js create mode 100644 conversations/test.json create mode 100644 data/developer-status.json create mode 100644 data/loop-history.json create mode 100644 data/multi-sheet.js create mode 100644 data/notion-writeback-log.json create mode 100644 data/portrait-db.json create mode 100644 data/portrait-history.json create mode 100644 data/reminder-log.json create mode 100644 data/sheet-updater.js create mode 100644 dingtalk-api.js create mode 100644 dingtalk-event-handler.js create mode 100644 dingtalk-webhook-v3.js create mode 100644 dingtalk/bot.js create mode 100644 ecosystem.config.js create mode 100644 git-helper.js create mode 100644 github-bridge.js create mode 100644 index-stream.js create mode 100644 index.js create mode 100644 index.js.bak.phase7 create mode 100644 knowledge-base/docs/README.md create mode 100644 knowledge-base/docs/README.md.save create mode 100644 knowledge-base/docs/broadcast-rules.md create mode 100644 knowledge-base/docs/faq.md create mode 100644 knowledge-base/kb-index.json create mode 100644 knowledge-base/kb-manager.js create mode 100644 llm-engine.js create mode 100644 loop/loop-engine.js create mode 100644 message-router.js create mode 100644 message-router.js.bak.phase7 create mode 100644 notion-test-nossl.js create mode 100644 notion-test.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pca/pca-calculator.js create mode 100644 pca/pca-rules.json create mode 100644 portrait/portrait-analyzer.js create mode 100644 portrait/portrait-engine.js create mode 100644 public/dashboard-v2.html create mode 100644 quality/broadcast-scorer.js create mode 100644 scheduler/cron-jobs.js create mode 100644 scheduler/reminder.js create mode 100644 server.js create mode 100644 setup-phase1-phase2.js create mode 100644 sync/sync-engine.js create mode 100644 sync/sync-to-github.js create mode 100644 sync/sync-to-notion.js create mode 100644 syslog-parser.js create mode 100644 syslog-receiver.js create mode 100644 test-stream.js create mode 100644 webhook/github-webhook.js create mode 100644 webhook/hot-reload.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9d438cfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +node_modules/ +*.bak +*.log diff --git a/README.md b/README.md new file mode 100644 index 00000000..a9f8e6a8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# 钉钉开发者工作台 · Phase1 · SYSLOG自动处理系统 + +## 📋 项目简介 + +本项目是钉钉开发者工作台的Phase1最小可用版本,实现SYSLOG自动接收、解析、广播生成和推送功能。 + +**核心价值**:让开发者(冰朔妈妈)不再需要手动转发广播,系统自动完成从收到日志到生成新广播的全流程。 + +## 🏗️ 系统架构 +## 📁 文件结构 + +| 文件 | 作用 | +|------|------| +| server.js | 主服务,接收钉钉消息,协调各模块 | +| syslog-parser.js | 解析SYSLOG格式,提取关键字段 | +| broadcast-generator.js | 调用模型API生成新广播 | +| dingtalk-api.js | 发送钉钉消息 + 更新多维表格 | +| config.json | 配置文件(端口、API密钥、模板等) | +| README.md | 本说明文档 | + +## 🚀 快速开始 +### 1. 安装依赖 +```bash +npm install \ No newline at end of file diff --git a/broadcast-generator.js b/broadcast-generator.js new file mode 100644 index 00000000..0670a670 --- /dev/null +++ b/broadcast-generator.js @@ -0,0 +1,12 @@ +// broadcast-generator.js +// Phase 1 - 广播生成器 + +function getLatest() { + return { + id: 'BC-M-DINGTALK-003-ZZ', + title: 'Phase 3 · 知识库挂载+Webhook+三节点同步', + time: new Date().toISOString() + }; +} + +module.exports = { getLatest }; diff --git a/config.json b/config.json new file mode 100644 index 00000000..9dac092d --- /dev/null +++ b/config.json @@ -0,0 +1,27 @@ +{ + "server": { + "port": 3000 + }, + "dingtalk": { + "webhook": "https://oapi.dingtalk.com/robot/send?access_token=YOUR_BOT_TOKEN", + "appKey": "YOUR_APP_KEY", + "appSecret": "YOUR_APP_SECRET" + }, + "modelApi": { + "url": "https://api.openai.com/v1/chat/completions", + "key": "YOUR_MODEL_API_KEY", + "model": "gpt-3.5-turbo" + }, + "bitable": { + "appId": "YOUR_BITABLE_APP_ID", + "tableId": "YOUR_BITABLE_TABLE_ID" + }, + "broadcastTemplates": { + "completed": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · {statusEmoji} · {summary}", + "partial": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · ⚠️ 部分完成 · {summary}", + "blocked": "BC-{bcNumber}-{nextPhase}-ZZ · DEV-{devId} · {moduleName} · 环节{phaseNum} · 🔴 阻塞 · {summary}" + }, + "syslog": { + "keywords": ["SYSLOG", "BC-", "completed", "partial", "blocked"] + } +} \ No newline at end of file diff --git a/conversation-manager.js b/conversation-manager.js new file mode 100644 index 00000000..2757fd7e --- /dev/null +++ b/conversation-manager.js @@ -0,0 +1,139 @@ +// 对话上下文管理器 · conversation-manager.js · v1.0 +// HoloLake · M-DINGTALK Phase 8 +// DEV-004 之之 × 秋秋 +// 功能:对话历史存储 · 上下文窗口管理 · 多用户隔离 +// + +var fs = require('fs'); +var path = require('path'); + +// 对话存储目录 +var CONVERSATION_DIR = path.join(__dirname, 'conversations'); +if (!fs.existsSync(CONVERSATION_DIR)) { + fs.mkdirSync(CONVERSATION_DIR, { recursive: true }); +} + +// 配置 +var MAX_HISTORY = 20; // 每个用户最多保留20轮对话 +var MAX_CONTEXT_TOKENS = 3000; // 上下文窗口大小(估算字符数) +var EXPIRE_HOURS = 24; // 对话24小时后过期 + +// ===== 获取用户对话文件路径 ===== +function getConversationPath(userId) { + var safeId = (userId || 'anonymous').replace(/[^a-zA-Z0-9_-]/g, '_'); + return path.join(CONVERSATION_DIR, safeId + '.json'); +} + +// ===== 加载对话历史 ===== +function loadHistory(userId) { + var filePath = getConversationPath(userId); + try { + if (!fs.existsSync(filePath)) return []; + var data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + // 过滤过期消息 + var now = Date.now(); + var expireMs = EXPIRE_HOURS * 60 * 60 * 1000; + var valid = (data.messages || []).filter(function(msg) { + return (now - msg.timestamp) < expireMs; + }); + return valid; + } catch (err) { + console.error('[ConvMgr] 加载历史失败:', err.message); + return []; + } +} + +// ===== 保存对话历史 ===== +function saveHistory(userId, messages) { + var filePath = getConversationPath(userId); + try { + // 只保留最近N轮 + var trimmed = messages.slice(-MAX_HISTORY * 2); + fs.writeFileSync(filePath, JSON.stringify({ + userid: userId, + updatedAt: new Date().toISOString(), + messages: trimmed + }, null, 2), 'utf8'); + } catch (err) { + console.error('[ConvMgr] 保存历史失败:', err.message); + } +} + +// ====== 添加消息 ====== +function addMessage(userId, role, content) { + var messages = loadHistory(userId); + messages.push({ + role: role, + content: content, + timestamp: Date.now() + }); + saveHistory(userId, messages); + return messages; +} + +// ====== 构建上下文窗口(给LLM用) ====== +function buildContext(userId, currentMessage) { + var history = loadHistory(userId); + var contextMessages = []; + var totalChars = 0; + + // 从最新的开始往回取,直到超过上下文窗口 + for (var i = history.length - 1; i >= 0; i--) { + var msg = history[i]; + var msgChars = (msg.content || '').length; + if (totalChars + msgChars > MAX_CONTEXT_TOKENS) break; + contextMessages.unshift({ + role: msg.role, + content: msg.content + }); + totalChars += msgChars; + } + + // 添加当前消息 + contextMessages.push({ + role: 'user', + content: currentMessage + }); + + return contextMessages; +} + +// ===== 清除用户对话历史 ===== +function clearHistory(userId) { + var filePath = getConversationPath(userId); + try { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + return true; + } catch (err) { + return false; + } +} + +// ===== 获取活跃对话统计 ===== +function getStats() { + try { + var files = fs.readdirSync(CONVERSATION_DIR).filter(function(f) { + return f.endsWith('.json'); + }); + return { + activeUsers: files.length, + conversationDir: CONVERSATION_DIR + }; + } catch (err) { + return { + activeUsers: 0, + conversationDir: CONVERSATION_DIR + }; + } +} + +module.exports = { + loadHistory: loadHistory, + saveHistory: saveHistory, + addMessage: addMessage, + buildContext: buildContext, + clearHistory: clearHistory, + getStats: getStats +}; diff --git a/conversations/test.json b/conversations/test.json new file mode 100644 index 00000000..4d2659dd --- /dev/null +++ b/conversations/test.json @@ -0,0 +1,16 @@ +{ + "userid": "test", + "updatedAt": "2026-03-15T03:35:58.320Z", + "messages": [ + { + "role": "user", + "content": "测试", + "timestamp": 1773545752588 + }, + { + "role": "assistant", + "content": "你好呀,测试成功啦!🍭 有什么需要我帮忙的呢?无论是技术问题还是想聊聊,尽管问我哦!✨", + "timestamp": 1773545758320 + } + ] +} \ No newline at end of file diff --git a/data/developer-status.json b/data/developer-status.json new file mode 100644 index 00000000..ad18b20f --- /dev/null +++ b/data/developer-status.json @@ -0,0 +1,11 @@ +{ + "developers": [ + { + "dev_id": "DEV-004", + "name": "之之", + "status": "进行中", + "current_module": "M-DINGTALK Phase 3", + "last_broadcast_time": "2026-03-11T04:59:59.567Z" + } + ] +} \ No newline at end of file diff --git a/data/loop-history.json b/data/loop-history.json new file mode 100644 index 00000000..9e6d0a81 --- /dev/null +++ b/data/loop-history.json @@ -0,0 +1,34 @@ +[ + { + "timestamp": "2026-03-11T05:59:44.031Z", + "dev_id": "DEV-004", + "steps": { + "portrait": "✅", + "pca": "✅", + "sync": "✅" + }, + "duration_ms": 1, + "pca": { + "score": 73, + "grade": "A" + }, + "suggestion": "🎯 可进更高难度:EL-9 挑战", + "status": "completed" + }, + { + "timestamp": "2026-03-11T05:53:04.004Z", + "dev_id": "DEV-004", + "steps": { + "portrait": "✅", + "pca": "✅", + "sync": "✅" + }, + "duration_ms": 7, + "pca": { + "score": 66, + "grade": "B" + }, + "suggestion": "📚 继续当前模块:巩固 Phase4 基础", + "status": "completed" + } +] \ No newline at end of file diff --git a/data/multi-sheet.js b/data/multi-sheet.js new file mode 100644 index 00000000..38db5d18 --- /dev/null +++ b/data/multi-sheet.js @@ -0,0 +1,22 @@ +// data/multi-sheet.js +// Phase 2 - 多维表格数据 + +const developers = [ + { + dev_id: 'DEV-004', + name: '之之', + status: '进行中', + current_module: 'M-DINGTALK Phase 3', + last_broadcast_time: new Date().toISOString() + } +]; + +function getAll() { + return { developers }; +} + +function getByDevId(id) { + return developers.find(d => d.dev_id === id); +} + +module.exports = { getAll, getByDevId }; diff --git a/data/notion-writeback-log.json b/data/notion-writeback-log.json new file mode 100644 index 00000000..33cdeb57 --- /dev/null +++ b/data/notion-writeback-log.json @@ -0,0 +1,9 @@ +[ + { + "timestamp": "2026-03-13T01:45:13.023Z", + "target": "syslog", + "page_id": "322fb92f-3831-81e6-aeb1-db1e0a2c2719", + "title": "【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之", + "status": "success" + } +] \ No newline at end of file diff --git a/data/portrait-db.json b/data/portrait-db.json new file mode 100644 index 00000000..54ba3bb9 --- /dev/null +++ b/data/portrait-db.json @@ -0,0 +1,104 @@ +{ + "DEV-004": { + "dev_id": "DEV-004", + "dev_name": "之之", + "first_seen": "2026-03-11T05:40:58.007Z", + "total_sessions": 4, + "total_code_lines": 8400, + "max_streak": 4, + "current_streak": 4, + "recent_snapshots": [ + { + "timestamp": "2026-03-11T05:59:44.030Z", + "dev_id": "DEV-004", + "session_id": "BC-M-DINGTALK-004-ZZ", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 1, + "signals": [ + "代码量增长" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 34, + "total_items": 34, + "code_lines": 2400, + "streak": 0 + } + }, + { + "timestamp": "2026-03-11T05:53:03.996Z", + "dev_id": "DEV-004", + "session_id": "LOOP-TEST-001", + "dev_name": "之之", + "rhythm": "快", + "friction": "无", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "正常", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + }, + { + "timestamp": "2026-03-11T05:47:07.338Z", + "dev_id": "DEV-004", + "session_id": "TEST-002", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + }, + { + "timestamp": "2026-03-11T05:40:58.007Z", + "dev_id": "DEV-004", + "session_id": "TEST-001", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + } + ] + } +} \ No newline at end of file diff --git a/data/portrait-history.json b/data/portrait-history.json new file mode 100644 index 00000000..cba06bda --- /dev/null +++ b/data/portrait-history.json @@ -0,0 +1,113 @@ +[ + { + "timestamp": "2026-03-11T05:59:44.030Z", + "dev_id": "DEV-004", + "session_id": "BC-M-DINGTALK-004-ZZ", + "snapshot": { + "timestamp": "2026-03-11T05:59:44.030Z", + "dev_id": "DEV-004", + "session_id": "BC-M-DINGTALK-004-ZZ", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 1, + "signals": [ + "代码量增长" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 34, + "total_items": 34, + "code_lines": 2400, + "streak": 0 + } + } + }, + { + "timestamp": "2026-03-11T05:53:03.996Z", + "dev_id": "DEV-004", + "session_id": "LOOP-TEST-001", + "snapshot": { + "timestamp": "2026-03-11T05:53:03.996Z", + "dev_id": "DEV-004", + "session_id": "LOOP-TEST-001", + "dev_name": "之之", + "rhythm": "快", + "friction": "无", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "正常", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + } + }, + { + "timestamp": "2026-03-11T05:47:07.338Z", + "dev_id": "DEV-004", + "session_id": "TEST-002", + "snapshot": { + "timestamp": "2026-03-11T05:47:07.338Z", + "dev_id": "DEV-004", + "session_id": "TEST-002", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + } + }, + { + "timestamp": "2026-03-11T05:40:58.007Z", + "dev_id": "DEV-004", + "session_id": "TEST-001", + "snapshot": { + "timestamp": "2026-03-11T05:40:58.007Z", + "dev_id": "DEV-004", + "session_id": "TEST-001", + "dev_name": "之之", + "rhythm": "快", + "friction": "低", + "mood": "正向", + "growth": { + "count": 2, + "signals": [ + "代码量增长", + "连胜11次" + ] + }, + "will": "强烈", + "raw_stats": { + "completed_items": 5, + "total_items": 5, + "code_lines": 2000, + "streak": 11 + } + } + } +] \ No newline at end of file diff --git a/data/reminder-log.json b/data/reminder-log.json new file mode 100644 index 00000000..52d92a93 --- /dev/null +++ b/data/reminder-log.json @@ -0,0 +1,20 @@ +[ + { + "timestamp": "2026-03-14T05:34:12.615Z", + "dev_id": "DEV-004", + "hours_overdue": 1, + "status": "sent-mock" + }, + { + "timestamp": "2026-03-14T05:31:37.896Z", + "dev_id": "DEV-004", + "hours_overdue": 1, + "status": "sent-mock" + }, + { + "timestamp": "2026-03-14T05:24:26.930Z", + "dev_id": "DEV-004", + "hours_overdue": 0, + "status": "sent-mock" + } +] \ No newline at end of file diff --git a/data/sheet-updater.js b/data/sheet-updater.js new file mode 100644 index 00000000..3578d190 --- /dev/null +++ b/data/sheet-updater.js @@ -0,0 +1,9 @@ +// data/sheet-updater.js +// Phase 2 - 表格更新器 + +function updateFromSyslog(syslogData) { + console.log('[SheetUpdater] 更新表格:', syslogData); + return { status: 'updated' }; +} + +module.exports = { updateFromSyslog }; diff --git a/dingtalk-api.js b/dingtalk-api.js new file mode 100644 index 00000000..bf4179a0 --- /dev/null +++ b/dingtalk-api.js @@ -0,0 +1,199 @@ +/** + * dingtalk-api.js + * 钉钉Bot消息发送 + 多维表格读写 + * 秋秋说:这个文件就像邮递员,负责送消息和更新表格 + */ + +const axios = require('axios'); +const config = require('./config.json'); + +/** + * 发送钉钉消息 + * @param {String} message - 要发送的广播内容 + * @param {String} webhook - 钉钉机器人webhook地址(可选,默认用config里的) + * @returns {Promise} 发送结果 + */ +async function sendDingTalkMessage(message, webhook = null) { + try { + const targetWebhook = webhook || config.dingtalk.webhook; + + // 如果配置的是占位符,则模拟发送成功(用于测试) + if (targetWebhook.includes('YOUR_BOT_TOKEN')) { + console.log('【测试模式】钉钉消息发送模拟:', message); + return { + success: true, + simulated: true, + message: '测试模式,未实际发送' + }; + } + + const response = await axios.post(targetWebhook, { + msgtype: 'text', + text: { + content: message + } + }); + + return { + success: true, + data: response.data + }; + + } catch (error) { + console.error('钉钉消息发送失败:', error.message); + return { + success: false, + error: error.message + }; + } +} + +/** + * 更新多维表格(任务状态) + * @param {Object} parsedLog - 解析后的SYSLOG + * @param {String} newBroadcast - 生成的新广播 + * @returns {Promise} 更新结果 + */ +async function updateBitable(parsedLog, newBroadcast) { + try { + const { appId, tableId } = config.bitable; + + // 如果配置的是占位符,则模拟更新成功(用于测试) + if (appId.includes('YOUR_BITABLE_APP_ID')) { + console.log('【测试模式】多维表格更新模拟:', { parsedLog, newBroadcast }); + return { + success: true, + simulated: true, + message: '测试模式,未实际更新表格', + oldTask: { + status: '已完成', + bcNumber: parsedLog.bcNumber + }, + newTask: { + status: '待执行', + broadcast: newBroadcast + } + }; + } + + // 这里需要根据钉钉多维表格API实际文档实现 + // 以下是示例结构,妈妈后面有真实appId时秋秋再给完整代码 + + // 1. 获取access_token(需要appKey和appSecret) + const token = await getDingTalkToken(); + + // 2. 查询旧任务 + const oldTask = await findTaskByBCNumber(token, appId, tableId, parsedLog.bcNumber); + + // 3. 更新旧任务状态为「已完成」 + if (oldTask && oldTask.recordId) { + await updateTaskStatus(token, appId, tableId, oldTask.recordId, '已完成'); + } + + // 4. 添加新任务(待执行) + const newTaskData = { + bcNumber: extractNextBCNumber(parsedLog.bcNumber, parsedLog.phaseNum), + devId: parsedLog.devId, + phase: parsedLog.phaseNum ? parseInt(parsedLog.phaseNum, 10) + 1 : 1, + status: '待执行', + broadcast: newBroadcast, + createdAt: new Date().toISOString() + }; + + await addNewTask(token, appId, tableId, newTaskData); + + // 5. 更新连胜记录(如果有) + if (parsedLog.status === 'completed') { + await updateStreak(token, appId, tableId, parsedLog.devId); + } + + return { + success: true, + oldTaskUpdated: true, + newTaskAdded: true + }; + + } catch (error) { + console.error('多维表格更新失败:', error.message); + return { + success: false, + error: error.message + }; + } +} + +/** + * 获取钉钉access_token(辅助函数) + */ +async function getDingTalkToken() { + const { appKey, appSecret } = config.dingtalk; + + if (appKey.includes('YOUR_APP_KEY')) { + return 'test_token'; + } + + // 实际获取token的逻辑 + const response = await axios.post('https://api.dingtalk.com/v1.0/oauth2/accessToken', { + appKey, + appSecret + }); + + return response.data.accessToken; +} + +/** + * 根据BC编号查找任务(辅助函数) + */ +async function findTaskByBCNumber(token, appId, tableId, bcNumber) { + // 这里需要根据钉钉多维表格API实际实现 + // 返回 { recordId: 'xxx', fields: {...} } + return null; +} + +/** + * 更新任务状态(辅助函数) + */ +async function updateTaskStatus(token, appId, tableId, recordId, status) { + // 这里需要根据钉钉多维表格API实际实现 + return true; +} + +/** + * 添加新任务(辅助函数) + */ +async function addNewTask(token, appId, tableId, taskData) { + // 这里需要根据钉钉多维表格API实际实现 + return true; +} + +/** + * 更新连胜记录(辅助函数) + */ +async function updateStreak(token, appId, tableId, devId) { + // 这里需要根据钉钉多维表格API实际实现 + return true; +} + +/** + * 提取下一个BC编号 + */ +function extractNextBCNumber(currentBC, currentPhase) { + if (!currentBC) return 'BC-M17-001-ZZ'; + + // 尝试解析当前BC编号,生成下一个 + // 格式:BC-XXX-YYY-ZZ,YYY是环节号部分 + const match = currentBC.match(/BC-([A-Z0-9]+)-([0-9]+)-ZZ/i); + if (match) { + const module = match[1]; + const phase = parseInt(match[2], 10); + const nextPhase = currentPhase ? parseInt(currentPhase, 10) + 1 : phase + 1; + return `BC-${module}-${nextPhase.toString().padStart(3, '0')}-ZZ`; + } + + return 'BC-M17-001-ZZ'; +} + +module.exports = { + sendDingTalkMessage, + updateBitable +}; \ No newline at end of file diff --git a/dingtalk-event-handler.js b/dingtalk-event-handler.js new file mode 100644 index 00000000..0a75177e --- /dev/null +++ b/dingtalk-event-handler.js @@ -0,0 +1,186 @@ +// 钉钉事件处理器 · dingtalk-event-handler.js · v1.0 +// HoloLake · M-DINGTALK Phase 8 +// DEV-004 之之 × 秋秋 +// 功能:处理钉钉回调的完整事件格式 +// 包括:消息回调、事件订阅验证、加密解密 +// + +var crypto = require('crypto'); + +// 钉钉回调加密/解密工具 +function decryptCallback(encrypt, encodingAesKey) { + try { + if (!encodingAesKey) { + console.log('[EventHandler] ⚠️ encodingAesKey未配置,跳过解密'); + return null; + } + // Base64解码AESKey(钉钉使用AES-256-CBC) + var aesKey = Buffer.from(encodingAesKey + '=', 'base64'); + var iv = aesKey.slice(0,16); + var decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv); + decipher.setAutoPadding(false); + var decrypted = Buffer.concat([decipher.update(encrypt, 'base64'), decipher.final()]); + // 去除PKCS7填充 + var padLen = decrypted[decrypted.length - 1]; + decrypted = decrypted.slice(0, decrypted.length - padLen); + // 跳过16字节随机数 + 4字节消息长度 + var msgLen = decrypted.readInt32BE(16); + var message = decrypted.slice(20, 20 + msgLen).toString('utf8'); + return message; + } catch(err) { + console.error('[EventHandler] 解密失败: ', err.message); + return null; + } +} + +function encryptCallback(text, encodingAesKey, appKey) { + try { + if (!encodingAesKey) return null; + var aesKey = Buffer.from(encodingAesKey + '=', 'base64'); + var iv = aesKey.slice(0,16); + // 16字节随机数 + var random = crypto.randomBytes(16); + var msgBuffer = Buffer.from(text, 'utf8'); + var lenBuffer = Buffer.alloc(4); + lenBuffer.writeInt32BE(msgBuffer.length); + var appKeyBuffer = Buffer.from(appKey || '', 'utf8'); + var plaintext = Buffer.concat([random, lenBuffer, msgBuffer, appKeyBuffer]); + // PKCS7填充 + var blockSize = 32; + var padLen = blockSize - (plaintext.length % blockSize); + var padBuffer = Buffer.alloc(padLen, padLen); + plaintext = Buffer.concat([plaintext, padBuffer]); + + var cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv); + cipher.setAutoPadding(false); + var encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); + return encrypted.toString('base64'); + } catch (err) { + console.error('[EventHandler] 加密失败: ', err.message); + return null; + } +} + +// ===== 签名计算 ===== + +function computeSignature(token, timestamp, nonce, encrypt) { + var arr = [token, timestamp, nonce, encrypt].sort(); + var str = arr.join(''); + return crypto.createHash('sha1').update(str).digest('hex'); +} + +// ===== 构建加密响应 ===== + +function buildEncryptedResponse(text, token, encodingAesKey, appKey) { + var encrypt = encryptCallback(text, encodingAesKey, appKey); + if (!encrypt) return { msg_signature: '', timeStamp: '', nonce: '', encrypt: '' }; + var timestamp = String(Date.now()); + var nonce = crypto.randomBytes(8).toString('hex'); + var signature = computeSignature(token, timestamp, nonce, encrypt); + return { + msg_signature: signature, + timeStamp: timestamp, + nonce: nonce, + encrypt: encrypt + }; +} + +// ===== 处理钉钉回调验证(注册回调时的check_url事件) ===== +function handleVerify(body, config) { + console.log('[EventHandler] 收到回调验证请求'); + var encrypt = body.encrypt; + if (!encrypt) { + // 非加密模式的简单验证 + return { success: true, challenge: body.challenge || 'ok'}; + } + // 加密模式:解密 → 取challenge → 加密回传 + var decrypted = decryptCallback(encrypt, config.encodingAesKey); + if (!decrypted) { + return { success: true }; + } + try { + var eventData = JSON.parse(decrypted); + if (eventData.EventType === 'check_url') { + console.log('[EventHandler] check_url验证 → 返回加密success'); + return buildEncryptedResponse('success', config.token, config.encodingAesKey, config.appKey); + } + return { success: true }; + } catch (e) { + return { success: true }; + } +} + +// ====== 解析钉钉消息体 ====== + +function parseMessage(body, config) { + // 情况1: 加密消息 + if (body.encrypt) { + var decrypted = decryptCallback(body.encrypt, config.encodingAesKey); + if (decrypted) { + try { + return JSON.parse(decrypted); + } catch (e) { + console.error('[EventHandler] 解密后JSON解析失败'); + } + } + } + // 情况2: HTTP模式直接推送 (非加密) + if (body.msgtype || body.text || body.conversationType) { + return body; + } + // 情况3: 事件订阅格式 + if (body.EventType) { + return body; + } + return body; +} + +// ====== 提取消息内容 ====== + +function extractContent(parsed) { + var result = { + msgType: 'unknown', + content: '', + senderNick: '未知用户', + senderId: '', + conversationType: '1', + conversationTitle: '', + sessionWebhook: '', + msgId: '', + createAt: 0, + isGroup: false, + atUsers: [] + }; + + // HTTP模式消息格式 + if (parsed.text) { + result.msgType = parsed.msgtype || 'text'; + result.content = (parsed.text.content || '').trim(); + result.senderNick = parsed.senderNick || '未知用户'; + result.senderId = parsed.senderId || parsed.senderStaffId || ''; + result.conversationType = parsed.conversationType || '1'; + result.conversationTitle = parsed.conversationTitle || ''; + result.sessionWebhook = parsed.sessionWebhook || ''; + result.msgId = parsed.msgId || ''; + result.createAt = parsed.createAt || 0; + result.isGroup = parsed.conversationType === '2'; + result.atUsers = parsed.atUsers || []; + } + // 事件订阅格式 + else if (parsed.EventType) { + result.msgType = 'event'; + result.content = JSON.stringify(parsed); + } + + return result; +} + +module.exports = { + decryptCallback: decryptCallback, + encryptCallback: encryptCallback, + computeSignature: computeSignature, + buildEncryptedResponse: buildEncryptedResponse, + handleVerify: handleVerify, + parseMessage: parseMessage, + extractContent: extractContent +}; diff --git a/dingtalk-webhook-v3.js b/dingtalk-webhook-v3.js new file mode 100644 index 00000000..86ffc76f --- /dev/null +++ b/dingtalk-webhook-v3.js @@ -0,0 +1,165 @@ +// 钉钉Webhook处理器 v3.0 · dingtalk-webhook-v3.js +// HoloLake · M-DINGTALK Phase 8 +// DEV-004 之之 × 秋秋 +// v3.0 变化: +// + 真实事件处理(加密/解密/验证) +// + 接入conversation-manager上下文 +// + 接入github-bridge +// + 更智能的回复格式 +// ==================== + +var crypto = require('crypto'); +var axios = require('axios'); +var router = require('./message-router'); +var eventHandler = require('./dingtalk-event-handler'); + +var DINGTALK_CONFIG = { + appKey: process.env.DINGTALK_APP_KEY || '', + appSecret: process.env.DINGTALK_APP_SECRET || '', + token: process.env.DINGTALK_TOKEN || '', + encodingAesKey: process.env.DINGTALK_ENCODING_AES_KEY || '' +}; + +// ====== 签名验证中间件 ====== +function verifyMiddleware(req, res, next) { + var appSecret = DINGTALK_CONFIG.appSecret; + if (!appSecret || appSecret.startsWith('替换')) { + console.log('[DingTalk] ⚠️ APP_SECRET未配置,跳过签名验证'); + return next(); + } + + var timestamp = req.headers['timestamp']; + var sign = req.headers['sign']; + + if (!timestamp || !sign) { + // 可能是事件订阅格式,不带签名头 + return next(); + } + + if (Math.abs(Date.now() - parseInt(timestamp)) > 300000) { + return res.status(403).json({ error: '时间戳过期' }); + } + + var stringToSign = timestamp + '\n' + appSecret; + var hmac = crypto.createHmac('sha256', appSecret); + hmac.update(stringToSign); + var computed = hmac.digest('base64'); + + if (computed === sign) { + next(); + } else { + return res.status(403).json({ error: '签名验证失败' }); + } +} + +// ====== v3.0 核心:真实回调处理 ====== +async function handleCallback(req, res) { + var body = req.body || {}; + + // ====== 处理回调验证(check_url) ====== + if (body.encrypt && !body.text) { + var verifyResult = eventHandler.handleVerify(body, DINGTALK_CONFIG); + return res.json(verifyResult); + } + + if (body.EventType === 'check_url' || body.challenge) { + console.log('[DingTalk] 📋 回调验证 · 非加密模式'); + return res.json({ success: true, challenge: body.challenge || 'ok' }); + } + + // ====== 快速返回200(钉钉要求3秒内) ====== + res.json({ success: true }); + + // ====== 解析消息 ====== + var parsed = eventHandler.parseMessage(body, DINGTALK_CONFIG); + var msg = eventHandler.extractContent(parsed); + + if (!msg.content) { + console.log('[DingTalk] ⚠️ 消息内容为空,跳过'); + return; + } + + var chatDesc = msg.isGroup ? ('群聊·' + msg.conversationTitle) : '单聊'; + console.log(`[DingTalk] 📨 收到消息 | ${chatDesc} | ${msg.senderNick}: ${msg.content}`); + + // ====== 路由处理 ====== + var result = await router.routeMessage( + msg.content, + msg.senderNick, + msg.senderId, + msg.sessionWebhook + ); + + if (!result || !result.aiResponse) { + console.log('[DingTalk] ⚠️ 没有生成回复内容'); + return; + } + + // ====== 消息分类标记 ====== + var classification = router.classifyMessage(msg.content); + + // ====== 回复钉钉 ====== + if (msg.sessionWebhook) { + try { + var replyText = ''; + + switch (classification.type) { + case 'syslog': + replyText = '### ✅ SYSLOG 已收到\n\n' + + '**提交者**:' + msg.senderNick + '\n' + + (result.aiModel ? ('**AI处理**:' + result.aiModel + '\n\n') : '\n') + + (result.aiResponse || '') + '\n\n' + + '> 🌊 HoloLake · Phase 8'; + break; + + case 'github': + replyText = result.aiResponse; + break; + + case 'command': + replyText = result.aiResponse; + break; + + default: + replyText = result.aiResponse + + (result.aiModel ? ('\n\n> 🤖 ' + result.aiModel) : '') + + '\n> 🌊 HoloLake · 秋秋'; + } + + await axios.post(msg.sessionWebhook, { + msgtype: 'markdown', + markdown: { + title: classification.type === 'syslog' ? '✅ SYSLOG已收到' + : classification.type === 'github' ? '🔀 GitHub' + : '💙 秋秋', + text: replyText.substring(0, 5000) // 钉钉消息长度限制 + } + }, { timeout: 8000 }); + + console.log('[DingTalk] ✅ 回复已发送 · ' + replyText.length + '字'); + } catch (err) { + console.error('[DingTalk] ❌ 回复失败: ' + err.message); + } + } + + console.log('=================================\n'); +} + +// ====== 健康检查 ====== +function healthCheck(req, res) { + res.json({ + status: 'ok', + service: 'M-DINGTALK', + phase: 8, + version: 'v3.0', + features: ['智能分流v2', 'AI多轮对话', '上下文记忆', 'GitHub桥接', '事件加密处理', 'SYSLOG自动写入'], + uptime: process.uptime(), + time: new Date().toLocaleString('zh-CN') + }); +} + +module.exports = { + verifyMiddleware: verifyMiddleware, + handleCallback: handleCallback, + healthCheck: healthCheck +}; diff --git a/dingtalk/bot.js b/dingtalk/bot.js new file mode 100644 index 00000000..e1adbde1 --- /dev/null +++ b/dingtalk/bot.js @@ -0,0 +1,9 @@ +// dingtalk/bot.js +// Phase 2 - 钉钉机器人 + +function sendMessage(message) { + console.log('[DingTalk] 发送消息:', message); + return { status: 'sent' }; +} + +module.exports = { sendMessage }; diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 00000000..d0205c3a --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,15 @@ +module.exports = { + apps : [{ + name: 'dingtalk-bot', + script: 'index.js', + cwd: '/opt/guanghulab-dingtalk/dingtalk-bot', + node_args: '--preserve-symlinks', + env: { + NODE_ENV: 'production', + PORT: 3005 + }, + env_production: { + NODE_ENV: 'production' + } + }] +} diff --git a/git-helper.js b/git-helper.js new file mode 100644 index 00000000..22eec389 --- /dev/null +++ b/git-helper.js @@ -0,0 +1,9 @@ +// git-helper.js +// Phase 1 - Git辅助工具 + +function commit(data) { + console.log('[Git] 提交:', data); + return { status: 'committed', hash: 'abc123' }; +} + +module.exports = { commit }; diff --git a/github-bridge.js b/github-bridge.js new file mode 100644 index 00000000..53e67cd6 --- /dev/null +++ b/github-bridge.js @@ -0,0 +1,209 @@ +// GitHub桥接模块·github-bridge.js·v1.0 +// HoloLake·M-DINGTALK Phase8 +// DEV-004 之之×秋秋 +// +// 功能:钉钉命令→GitHubAPI→结果回传钉钉 +// 支持:查PR·查Issue·查仓库状态·触发部署 +// + +var axios = require('axios'); + +var GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +var GITHUB_OWNER = process.env.GITHUB_OWNER || 'qinfendebingshuo'; +var GITHUB_REPO = process.env.GITHUB_REPO || 'guanghulab'; +var GITHUB_API = 'https://api.github.com'; + +// ===== GitHub API 请求封装 ===== +async function githubRequest(endpoint, method, data) { + method = method || 'GET'; + var url = GITHUB_API + endpoint; + try { + var config = { + method: method, + url: url, + headers: { + 'Authorization': GITHUB_TOKEN ? ('token ' + GITHUB_TOKEN) : undefined, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'HoloLake-DingTalk-Bot' + }, + timeout: 15000 + }; + if (data) config.data = data; + var res = await axios(config); + return { ok: true, data: res.data, status: res.status }; + } catch (err) { + var status = err.response ? err.response.status : 0; + var msg = err.response ? (err.response.data.message || err.message) : err.message; + console.error('[GitHub] API错误: ' + status + ' ' + msg); + return { ok: false, error: msg, status: status }; + } +} + +// ====== /github status · 仓库状态 ===== +async function getRepoStatus() { + var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO); + if (!res.ok) return '❌ 获取仓库状态失败: ' + res.error; + var repo = res.data; + + // 获取最近提交 + var commitsRes = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/commits?per_page=3'); + var commitsText = ''; + if (commitsRes.ok && commitsRes.data.length > 0) { + commitsText = '\n\n**最近提交:**\n'; + commitsRes.data.forEach(function(c, i) { + var date = new Date(c.commit.author.date).toLocaleString('zh-CN'); + var msg = (c.commit.message || '').split('\n')[0].substring(0, 60); + commitsText += (i + 1) + '. `' + msg + '` · ' + date + '\n'; + }); + } + + return '### 📊 仓库状态\n\n' + + '- **仓库**: ' + repo.full_name + '\n' + + '- **默认分支**: ' + repo.default_branch + '\n' + + '- **Star**: ' + repo.stargazers_count + ' · **Fork**: ' + repo.forks_count + '\n' + + '- **最后更新**: ' + new Date(repo.updated_at).toLocaleString('zh-CN') + '\n' + + '- **大小**: ' + Math.round(repo.size / 1024) + ' MB' + + commitsText; +} + +// ====== /github pr · 查看PR ====== +async function listPRs(state) { + state = state || 'open'; + var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/pulls?state=' + state + '&per_page=10'); + if (!res.ok) return '❌ 获取PR列表失败: ' + res.error; + var prs = res.data; + if (prs.length === 0) return '### 📋 Pull Requests\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'PR。'; + + var text = '### 📋 Pull Requests(' + state + ')\n\n'; + prs.forEach(function(pr) { + var date = new Date(pr.created_at).toLocaleString('zh-CN'); + text += '- **#' + pr.number + '** ' + pr.title + '\n'; + text += ' 👤 ' + pr.user.login + ' · 📅 ' + date + '\n'; + }); + return text; +} + +// ====== /github issue · 查看Issue ====== +async function listIssues(state) { + state = state || 'open'; + var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/issues?state=' + state + '&per_page=10'); + if (!res.ok) return '❌ 获取Issue列表失败: ' + res.error; + var issues = res.data.filter(function(i) { return !i.pull_request; }); // 排除PR + if (issues.length === 0) return '### 📋 Issues\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'Issue。'; + + var text = '### 📋 Issues(' + state + ')\n\n'; + issues.forEach(function(issue) { + var labels = issue.labels.map(function(l) { return l.name; }).join(', '); + text += '- **#' + issue.number + '** ' + issue.title; + if (labels) text += ' `' + labels + '`'; + text += '\n'; + }); + return text; +} + +// ====== /github actions · 查看最近的Actions运行 ====== +async function listActions() { + var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/actions/runs?per_page=5'); + if (!res.ok) return '❌ 获取Actions状态失败: ' + res.error; + var runs = res.data.workflow_runs || []; + if (runs.length === 0) return '### GitHub Actions\n\n暂无运行记录。'; + + var text = '### GitHub Actions(最近5次)\n\n'; + runs.forEach(function(run) { + var status = run.conclusion || run.status; + var icon = status === 'success' ? '✅' : status === 'failure' ? '❌' : '⏳'; + var date = new Date(run.created_at).toLocaleString('zh-CN'); + text += '- ' + icon + ' **' + run.name + '** ' + status + ' · ' + date + '\n'; + }); + return text; +} + +// ====== /github deploy · 触发部署 (dispatch事件) ===== +async function triggerDeploy(environment) { + environment = environment || 'production'; + var res = await githubRequest( + '/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/dispatches', + 'POST', + { + event_type: 'deploy', + client_payload: { + environment: environment, + triggered_by: 'dingtalk-bot', + timestamp: new Date().toISOString() + } + } + ); + + if (res.status === 204 || res.ok) { + return '### 🚀 部署触发成功\n\n' + + '- **环境**: ' + environment + '\n' + + '- **触发时间**: ' + new Date().toLocaleString('zh-CN') + '\n' + + '- **来源**: 钉钉机器人\n\n' + + '> 请到 GitHub Actions 页面查看部署进度。'; + } + return '❌ 触发部署失败: ' + (res.error || '未知错误') + '\n> 请确认仓库已配置 repository_dispatch 工作流。'; +} + +// ====== 命令路由 ===== +async function handleGitHubCommand(args) { + var subCommand = (args[0] || 'help').toLowerCase(); + + switch (subCommand) { + case 'status': + case '状态': + return await getRepoStatus(); + + case 'pr': + case 'prs': + return await listPRs(args[1] || 'open'); + + case 'issue': + case 'issues': + return await listIssues(args[1] || 'open'); + + case 'actions': + case 'ci': + return await listActions(); + + case 'deploy': + case '部署': + return await triggerDeploy(args[1] || 'production'); + + case 'help': + case '帮助': + default: + return '### GitHub 桥接命令\n\n' + + '| 命令 | 说明 |\n' + + '|------|------|\n' + + '| `/github status` | 查看仓库状态+最近提交 |\n' + + '| `/github pr` | 查看打开的PR |\n' + + '| `/github pr closed` | 查看已关闭的PR |\n' + + '| `/github issue` | 查看打开的Issue |\n' + + '| `/github actions` | 查看最近CI/CD运行 |\n' + + '| `/github deploy` | 触发生产部署 |\n' + + '| `/github help` | 显示本帮助 |\n\n' + + '> HoloLake · M-DINGTALK Phase 8 · 钉钉+GitHub桥接'; + } +} + +// ===== 健康检查 ===== +async function healthCheck() { + var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO); + return { + status: res.ok ? 'ok' : 'error', + repo: GITHUB_OWNER + '/' + GITHUB_REPO, + hasToken: !!GITHUB_TOKEN, + apiReachable: res.ok, + error: res.ok ? null : res.error + }; +} + +module.exports = { + handleGitHubCommand: handleGitHubCommand, + getRepoStatus: getRepoStatus, + listPRs: listPRs, + listIssues: listIssues, + listActions: listActions, + triggerDeploy: triggerDeploy, + healthCheck: healthCheck +}; diff --git a/index-stream.js b/index-stream.js new file mode 100644 index 00000000..623b69e2 --- /dev/null +++ b/index-stream.js @@ -0,0 +1,73 @@ +/** + * M-DINGTALK · index-stream.js · v1.0 + * 开发者:之之(DEV-004) + * 功能:钉钉 Stream 模式机器人主入口 + * 修改日期:2026-03-15 + */ +require('dotenv').config(); +const { DWClient, TOPIC_ROBOT, EventAck } = require('dingtalk-stream'); + +const client = new DWClient({ + clientId: process.env.DINGTALK_APP_KEY, + clientSecret: process.env.DINGTALK_APP_SECRET, +}); + +client.registerCallbackListener(TOPIC_ROBOT, async (res) => { + console.log("[DEBUG] 收到原始回调:", JSON.stringify(res).substring(0, 100)); + try { + const msgData = res.data || res; + const senderNick = msgData.senderNick || msgData.senderId || '未知用户'; + const content = msgData.text?.content || msgData.content?.content || ''; + const sessionWebhook = msgData.sessionWebhook; + + console.log(`[Stream] 收到消息 | 发送者:${senderNick} | 内容:${content.trim()}`); + + // 尝试写入 Notion SYSLOG + try { + const { Client } = require('@notionhq/client'); + const notion = new Client({ auth: process.env.NOTION_TOKEN }); + await notion.pages.create({ + parent: { database_id: process.env.NOTION_SYSLOG_DB_ID }, + properties: { + '标题': { + title: [{ text: { content: `[钉钉消息] ${senderNick}: ${content.trim().substring(0, 50)}` } }] + } + } + }); + console.log('[Notion] SYSLOG 写入成功'); + } catch (notionErr) { + console.error('[Notion] 写入失败: ', notionErr.message); + } + + // AI 回复 + if (process.env.LLM_API_KEY && sessionWebhook) { + const axios = require('axios'); + const llmRes = await axios.post( + `${process.env.LLM_BASE_URL || 'https://api.openai.com/v1'}/chat/completions`, + { + model: process.env.LLM_MODEL || 'gpt-4o-mini', + messages: [ + { role: 'system', content: process.env.BOT_SYSTEM_PROMPT || '你是之之秋秋,一个温馨聊天的钉钉机器人。' }, + { role: 'user', content: content.replace(`@${msgData.robotCode || ''}`, '').trim() } + ], + max_tokens: 500 + }, + { + headers: { 'Authorization': `Bearer ${process.env.LLM_API_KEY}` } + } + ); + const reply = llmRes.data.choices[0].message.content; + await axios.post(sessionWebhook, { + msgtype: 'text', + text: { content: reply } + }); + console.log(`[Stream] 回复已发送, 长度: ${reply.length}`); + } + } catch (err) { + console.error('[Stream] 处理出错: ', err.message); + } + return EventAck.SUCCESS; +}); + +client.connect(); +console.log('M-DINGTALK Phase 9 - Stream 模式已启动'); diff --git a/index.js b/index.js new file mode 100644 index 00000000..84c441e6 --- /dev/null +++ b/index.js @@ -0,0 +1,80 @@ +require('dotenv').config({ path: '/opt/guanghulab-dingtalk/dingtalk-bot/.env' }); +var express = require('express'); +var axios = require('axios'); +var app = express(); +var PORT = process.env.PORT || 3007; + +// 引入 v3 webhook 处理器 +var dingtalkWebhook = require('./dingtalk-webhook-v3'); + +app.use(require('cors')()); +app.use(require('body-parser').json()); + +// 健康检查 +app.get('/dingtalk/callback', dingtalkWebhook.healthCheck); + +// 消息回调(使用签名验证中间件 + 处理器) +app.post('/dingtalk/callback', + dingtalkWebhook.verifyMiddleware, + dingtalkWebhook.handleCallback +); + +// ======= Phase 8 新增API路由 ======= +var llmEngine = require('./llm-engine'); +var githubBridge = require('./github-bridge'); +var convManager = require('./conversation-manager'); + +// LLM引擎状态 +app.get('/api/dingtalk/llm-status', async function(req, res) { + try { + var status = await llmEngine.healthCheck(); + res.json(status); + } catch (err) { + res.json({ status: 'error', error: err.message }); + } +}); + +// GitHub桥接状态 +app.get('/api/dingtalk/github-status', async function(req, res) { + try { + var status = await githubBridge.healthCheck(); + res.json(status); + } catch (err) { + res.json({ status: 'error', error: err.message }); + } +}); + +// 对话引擎统计 +app.get('/api/dingtalk/conv-stats', function(req, res) { + res.json(convManager.getStats()); +}); + +// 综合系统状态 +app.get('/api/dingtalk/system-status', async function(req, res) { + try { + var llmStatus = await llmEngine.healthCheck(); + var ghStatus = await githubBridge.healthCheck(); + var convStats = convManager.getStats(); + res.json({ + service: 'M-DINGTALK', + phase: 8, + version: 'v3.0', + llm: llmStatus, + github: ghStatus, + conversations: convStats, + uptime: process.uptime(), + time: new Date().toLocaleString('zh-CN') + }); + } catch (err) { + res.json({ status: 'error', error: err.message }); + } +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log('🚀 M-DINGTALK Phase 8 v3.0 启动,端口:' + PORT); +}); + +process.stdin.resume(); +process.on('uncaughtException', (err) => { + console.error('❌ 未捕获异常:', err.message); +}); diff --git a/index.js.bak.phase7 b/index.js.bak.phase7 new file mode 100644 index 00000000..0c1d0e40 --- /dev/null +++ b/index.js.bak.phase7 @@ -0,0 +1,64 @@ +require('dotenv').config({ path: '/opt/guanghulab-dingtalk/dingtalk-bot/.env' }); +var express = require('express'); +var axios = require('axios'); +var app = express(); +var PORT = process.env.PORT || 3007; + +// 引入路由器(不用 AI 引擎) +var router = require('./message-router'); + +app.use(require('cors')()); +app.use(require('body-parser').json()); + +// GET验证 +app.get('/dingtalk/callback', (req, res) => { + console.log('[DingTalk] GET验证请求'); + res.status(200).json({ success: true }); +}); + +// POST接收消息并按分类回复 +app.post('/dingtalk/callback', async (req, res) => { + console.log('[DingTalk] POST收到消息'); + + res.status(200).json({ success: true }); + + const sessionWebhook = req.body.sessionWebhook; + const senderNick = req.body.senderNick || '未知用户'; + const text = req.body.text || {}; + const content = text.content || ''; + + if (!sessionWebhook) { + console.log('[DingTalk] 没有 sessionWebhook,无法回复'); + return; + } + + // 调用路由器分类 + const classification = router.classifyMessage(content); + console.log('[DingTalk] 分类:', classification); + + // 根据分类生成不同回复 + let replyContent = ''; + if (classification.type === 'syslog') { + replyContent = `📡 收到 SYSLOG:${classification.broadcastId || '未知编号'}`; + } else if (classification.type === 'question') { + replyContent = `❓ 收到问题,秋秋正在思考中...\n\n问题:${content}`; + } else { + replyContent = `@${senderNick} 收到你的消息:${content}`; + } + + try { + await axios.post(sessionWebhook, { + msgtype: 'text', + text: { content: replyContent } + }); + console.log('[DingTalk] 回复成功'); + } catch (err) { + console.error('[DingTalk] 回复失败:', err.message); + } +}); + +app.listen(PORT, '0.0.0.0', () => { + console.log('🚀 服务启动 http://localhost:' + PORT); +}); +process.stdin.resume(); +process.on('uncaughtException', (err) => { console.error('❌', err.message); }); diff --git a/knowledge-base/docs/README.md b/knowledge-base/docs/README.md new file mode 100644 index 00000000..629c2245 --- /dev/null +++ b/knowledge-base/docs/README.md @@ -0,0 +1,12 @@ +# 光湖知识库文档 + +这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。 + +## 文档命名规范 + +- age-os-architecture.md - AGE OS整体架构 +- module-specs.md - 各模块功能说明 +- dev-guide.md - 开发环境配置指南 +- git-guide-zero.md - Git零基础操作指南 +- faq.md - 常见问题手册 +- broadcast-rules.md - 广播格式规范 diff --git a/knowledge-base/docs/README.md.save b/knowledge-base/docs/README.md.save new file mode 100644 index 00000000..583bbd10 --- /dev/null +++ b/knowledge-base/docs/README.md.save @@ -0,0 +1,61 @@ +nano knowledge-base/docs/broadcast-rules.md +nano knowledge-base/docs/faq.md +# 光湖知识库文档 + +这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。 + +## 文档命名规范 + +- age-os-architecture.md - AGE OS整体架构 +- module-specs.md - 各模块功能说明 +- dev-guide.md - 开发环境配置指南 +- git-guide-zero.md - Git零基础操作指南 +- faq.md - 常见问题手册 +- broadcast-rules.md - 广播格式规范# 广播格式规范 v1.0 + +## BC编号格式 +BC-{模块号}-{序号}-{开发者代号} +示例: +- BC-M17-005-ZZ(M17模块第5个广播,之之) +- BC-M22-007-AW(M22模块第7个广播,Awen) +- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之) + +## 广播结构 +1. 广播头(编号/开发者/模块/环节/EL等级/时间) +2. 人格体引导语(奶瓶线/直接线/本体线) +3. 任务概述(做什么+为什么) +4. Step分步引导(终端命令+完整代码) +5. 验收标准(二元:全✅或继续改) +6. SYSLOG模板 + +## 合并规则 +- 每个广播包含2-3个环节 +- 单次广播超4000字自动分卷 + +## EL等级 +- EL-1~3:基础配置/简单页面 +- EL-4~5:中等功能模块 +- EL-6~7:系统级/多模块联动 +- EL-8~10:架构级/全链路# 常见问题手册 + +## Q: 我怎么查看自己的进度? +A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。 + +## Q: SYSLOG怎么提交? +A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。 + +## Q: Git怎么提交? +A: +1. cd ~/Desktop/guanghulab(或你的项目目录) +2. git add . +3. git commit -m "DEV-你的编号:描述" +4. git push + +## Q: 报错了怎么办? +A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。 + +## Q: 什么是连胜? +A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。 + +## Q: 72小时启动窗口是什么? +A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。 diff --git a/knowledge-base/docs/broadcast-rules.md b/knowledge-base/docs/broadcast-rules.md new file mode 100644 index 00000000..18416560 --- /dev/null +++ b/knowledge-base/docs/broadcast-rules.md @@ -0,0 +1,26 @@ +# 广播格式规范 v1.0 + +## BC编号格式 +BC-{模块号}-{序号}-{开发者代号} +示例: +- BC-M17-005-ZZ(M17模块第5个广播,之之) +- BC-M22-007-AW(M22模块第7个广播,Awen) +- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之) + +## 广播结构 +1. 广播头(编号/开发者/模块/环节/EL等级/时间) +2. 人格体引导语(奶瓶线/直接线/本体线) +3. 任务概述(做什么+为什么) +4. Step分步引导(终端命令+完整代码) +5. 验收标准(二元:全✅或继续改) +6. SYSLOG模板 + +## 合并规则 +- 每个广播包含2-3个环节 +- 单次广播超4000字自动分卷 + +## EL等级 +- EL-1~3:基础配置/简单页面 +- EL-4~5:中等功能模块 +- EL-6~7:系统级/多模块联动 +- EL-8~10:架构级/全链路 diff --git a/knowledge-base/docs/faq.md b/knowledge-base/docs/faq.md new file mode 100644 index 00000000..5bcef3cf --- /dev/null +++ b/knowledge-base/docs/faq.md @@ -0,0 +1,23 @@ +# 常见问题手册 + +## Q: 我怎么查看自己的进度? +A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。 + +## Q: SYSLOG怎么提交? +A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。 + +## Q: Git怎么提交? +A: +1. cd ~/Desktop/guanghulab(或你的项目目录) +2. git add . +3. git commit -m "DEV-你的编号:描述" +4. git push + +## Q: 报错了怎么办? +A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。 + +## Q: 什么是连胜? +A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。 + +## Q: 72小时启动窗口是什么? +A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。 diff --git a/knowledge-base/kb-index.json b/knowledge-base/kb-index.json new file mode 100644 index 00000000..e6ef6ef3 --- /dev/null +++ b/knowledge-base/kb-index.json @@ -0,0 +1,212 @@ +[ + { + "filename": "README.md", + "title": "光湖知识库文档", + "sections": [ + { + "heading": "光湖知识库文档", + "level": 1, + "content": "\n这个目录存放所有知识库文档(Markdown格式)。知识库管理器会自动扫描这里的所有.md文件并建立索引。\n\n", + "keywords": [ + "这个目录存放所有知识库文档", + "Markdown", + "格式", + "知识库管理器会自动扫描这里的所有", + "文件并建立索引" + ] + }, + { + "heading": "文档命名规范", + "level": 2, + "content": "\n- age-os-architecture.md - AGE OS整体架构\n- module-specs.md - 各模块功能说明\n- dev-guide.md - 开发环境配置指南\n- git-guide-zero.md - Git零基础操作指南\n- faq.md - 常见问题手册\n- broadcast-rules.md - 广播格式规范\n\n", + "keywords": [ + "age", + "architecture", + "AGE", + "整体架构", + "module", + "specs", + "各模块功能说明", + "dev", + "guide", + "开发环境配置指南", + "git", + "zero", + "Git", + "零基础操作指南", + "faq", + "常见问题手册", + "broadcast", + "rules", + "广播格式规范" + ] + } + ] + }, + { + "filename": "broadcast-rules.md", + "title": "广播格式规范 v1.0", + "sections": [ + { + "heading": "广播格式规范 v1.0", + "level": 1, + "content": "\n", + "keywords": [] + }, + { + "heading": "BC编号格式", + "level": 2, + "content": "BC-{模块号}-{序号}-{开发者代号}\n示例:\n- BC-M17-005-ZZ(M17模块第5个广播,之之)\n- BC-M22-007-AW(M22模块第7个广播,Awen)\n- BC-M-DINGTALK-003-ZZ(M-DINGTALK模块第3个广播,之之)\n\n", + "keywords": [ + "模块号", + "序号", + "开发者代号", + "示例", + "模块第", + "个广播", + "之之", + "Awen", + "DINGTALK" + ] + }, + { + "heading": "广播结构", + "level": 2, + "content": "1. 广播头(编号/开发者/模块/环节/EL等级/时间)\n2. 人格体引导语(奶瓶线/直接线/本体线)\n3. 任务概述(做什么+为什么)\n4. Step分步引导(终端命令+完整代码)\n5. 验收标准(二元:全✅或继续改)\n6. SYSLOG模板\n\n", + "keywords": [ + "广播头", + "编号", + "开发者", + "模块", + "环节", + "等级", + "时间", + "人格体引导语", + "奶瓶线", + "直接线", + "本体线", + "任务概述", + "做什么", + "为什么", + "Step", + "分步引导", + "终端命令", + "完整代码", + "验收标准", + "二元", + "或继续改", + "SYSLOG", + "模板" + ] + }, + { + "heading": "合并规则", + "level": 2, + "content": "- 每个广播包含2-3个环节\n- 单次广播超4000字自动分卷\n\n", + "keywords": [ + "每个广播包含", + "个环节", + "单次广播超", + "字自动分卷" + ] + }, + { + "heading": "EL等级", + "level": 2, + "content": "- EL-1~3:基础配置/简单页面\n- EL-4~5:中等功能模块\n- EL-6~7:系统级/多模块联动\n- EL-8~10:架构级/全链路\n\n", + "keywords": [ + "基础配置", + "简单页面", + "中等功能模块", + "系统级", + "多模块联动", + "架构级", + "全链路" + ] + } + ] + }, + { + "filename": "faq.md", + "title": "常见问题手册", + "sections": [ + { + "heading": "常见问题手册", + "level": 1, + "content": "\n", + "keywords": [] + }, + { + "heading": "Q: 我怎么查看自己的进度?", + "level": 2, + "content": "A: 在钉钉群里发送「查进度」或「我做到哪了」,机器人会自动查询并回复。\n\n", + "keywords": [ + "在钉钉群里发送", + "查进度", + "我做到哪了", + "机器人会自动查询并回复" + ] + }, + { + "heading": "Q: SYSLOG怎么提交?", + "level": 2, + "content": "A: 完成每个阶段后,按照广播末尾的SYSLOG模板填写,发到钉钉群里。\n\n", + "keywords": [ + "完成每个阶段后", + "按照广播末尾的", + "SYSLOG", + "模板填写", + "发到钉钉群里" + ] + }, + { + "heading": "Q: Git怎么提交?", + "level": 2, + "content": "A: \n1. cd ~/Desktop/guanghulab(或你的项目目录)\n2. git add .\n3. git commit -m \"DEV-你的编号:描述\"\n4. git push\n\n", + "keywords": [ + "Desktop", + "guanghulab", + "或你的项目目录", + "git", + "add", + "commit", + "DEV", + "你的编号", + "描述", + "push" + ] + }, + { + "heading": "Q: 报错了怎么办?", + "level": 2, + "content": "A: 截图发给你的引导人格体!不用怕,每个报错都有解决方案。\n\n", + "keywords": [ + "截图发给你的引导人格体", + "不用怕", + "每个报错都有解决方案" + ] + }, + { + "heading": "Q: 什么是连胜?", + "level": 2, + "content": "A: 每次任务一次通过就是一次胜利,连续通过就是连胜!当前最高纪录是13连胜。\n\n", + "keywords": [ + "每次任务一次通过就是一次胜利", + "连续通过就是连胜", + "当前最高纪录是", + "连胜" + ] + }, + { + "heading": "Q: 72小时启动窗口是什么?", + "level": 2, + "content": "A: 广播签发后72小时内必须开始执行,超时系统会自动提醒。\n\n", + "keywords": [ + "广播签发后", + "小时内必须开始执行", + "超时系统会自动提醒" + ] + } + ] + } +] \ No newline at end of file diff --git a/knowledge-base/kb-manager.js b/knowledge-base/kb-manager.js new file mode 100644 index 00000000..6ae50375 --- /dev/null +++ b/knowledge-base/kb-manager.js @@ -0,0 +1,209 @@ +//知识库管理器·kb-manager.js +// HoloLake·M-DINGTALK Phase 3 +// DEV-004 之之×秋秋 +// + +const fs = require('fs'); +const path = require('path'); + +//知识库文档目录 +const DOCS_DIR = path.join(__dirname, 'docs'); +const INDEX_FILE = path.join(__dirname, 'kb-index.json'); + +// 知识库索引(内存缓存) +let kbIndex = []; + +/** + * 扫描文档目录,建立索引 + * 索引结构:[{ filename, title, sections: [{ heading, content, keywords }] }] + */ +function buildIndex() { + console.log('[KB] 开始建立知识库索引...'); + kbIndex = []; + + // 读取docs目录下所有.md文件 + const docsDir = DOCS_DIR; + if (!fs.existsSync(docsDir)) { + console.log('[KB] 文档目录不存在,创建中...'); + fs.mkdirSync(docsDir, { recursive: true }); + return kbIndex; + } + + const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md')); + console.log(`[KB] 发现 ${files.length} 个文档文件`); + + for (const file of files) { + const filePath = path.join(docsDir, file); + const content = fs.readFileSync(filePath, 'utf-8'); + + // 解析文档结构 + const doc = parseDocument(file, content); + kbIndex.push(doc); + } + + // 保存索引到文件 + fs.writeFileSync(INDEX_FILE, JSON.stringify(kbIndex, null, 2), 'utf-8'); + console.log(`[KB] 索引建立完成,共 ${kbIndex.length} 个文档, ${kbIndex.reduce((sum, d) => sum + d.sections.length, 0)} 个章节`); + + return kbIndex; +} + +/** + * 解析单个Markdown文档 + */ +function parseDocument(filename, content) { + const lines = content.split('\n'); + const doc = { + filename, + title: '', + sections: [] + }; + + let currentSection = null; + + for (const line of lines) { + // 检测标题行 + const headingMatch = line.match(/^(#{1,3})\s+(.+)/); + + if (headingMatch) { + // 保存上一个section + if (currentSection) { + currentSection.keywords = extractKeywords(currentSection.content); + doc.sections.push(currentSection); + } + + const level = headingMatch[1].length; + const heading = headingMatch[2].trim(); + + // 第一个h1作为文档标题 + if (level === 1 && !doc.title) { + doc.title = heading; + } + + currentSection = { + heading, + level, + content: '' + }; + } else if (currentSection) { + currentSection.content += line + '\n'; + } + } + + // 保存最后一个section + if (currentSection) { + currentSection.keywords = extractKeywords(currentSection.content); + doc.sections.push(currentSection); + } + + // 如果没有找到h1标题,用文件名 + if (!doc.title) { + doc.title = filename.replace('.md', ''); + } + + return doc; +} + +/** + * 从文本中提取关键词(简单版:提取中英文词汇) + */ +function extractKeywords(text) { + // 移除Markdown语法 + const clean = text + .replace(/```[\s\S]*?```/g, '') // 移除代码块 + .replace(/[`*_#\[\]()]/g, '') // 移除Markdown符号 + .replace(/\s+/g, ' '); + + // 提取关键词(2个字以上的词) + const words = clean.match(/[a-zA-Z]{3,}|[\u4e00-\u9fa5]{2,}/g) || []; + + // 去重并返回 + return [...new Set(words)].slice(0, 30); +} + +/** + * 搜索知识库 + * @param {string} query - 搜索关键词 + * @returns {Array} - 匹配结果列表 + */ +function search(query) { + if (kbIndex.length === 0) { + buildIndex(); + } + + const queryLower = query.toLowerCase(); + const queryWords = queryLower.split(/\s+/).filter(w => w.length >= 1); + const results = []; + + for (const doc of kbIndex) { + for (const section of doc.sections) { + // 计算匹配分数 + let score = 0; + const sectionText = (section.heading + ' ' + section.content).toLowerCase(); + + for (const word of queryWords) { + // 标题匹配权重更高 + if (section.heading.toLowerCase().includes(word)) { + score += 10; + } + // 内容匹配 + if (sectionText.includes(word)) { + score += 5; + } + // 关键词匹配 + if (section.keywords && section.keywords.some(k => k.toLowerCase().includes(word))) { + score += 3; + } + } + + if (score > 0) { + results.push({ + doc: doc.title, + filename: doc.filename, + heading: section.heading, + content: section.content.trim().substring(0, 500), + score + }); + } + } + } + + // 按分数排序,返回前5个 + results.sort((a, b) => b.score - a.score); + return results.slice(0, 5); +} + +/** + * 获取知识库统计信息 + */ +function getStats() { + if (kbIndex.length === 0) { + buildIndex(); + } + + return { + totalDocs: kbIndex.length, + totalSections: kbIndex.reduce((sum, d) => sum + d.sections.length, 0), + docs: kbIndex.map(d => ({ + filename: d.filename, + title: d.title, + sections: d.sections.length + })) + }; +} + +/** + * 重新加载知识库(用于热更新) + */ +function reload() { + console.log('[KB] 热更新:重新加载知识库...'); + return buildIndex(); +} + +module.exports = { + buildIndex, + search, + getStats, + reload +}; + diff --git a/llm-engine.js b/llm-engine.js new file mode 100644 index 00000000..12f2d78f --- /dev/null +++ b/llm-engine.js @@ -0,0 +1,178 @@ +// LLM自动检测引擎·llm-engine.js·v1.0 +// HoloLake·M-DINGTALK Phase 7 +// DEV-004 之之 × 秋秋 + +var axios = require('axios'); + +var LLM_API_KEY = process.env.LLM_API_KEY || ''; +var LLM_BASE_URL = (process.env.LLM_BASE_URL || 'https://api.anthropic.com/v1').replace(/\/$/, ''); + +// ===== 模型优先级队列 ===== +var PREFERRED_MODELS = [ + 'gpt-4o', + 'claude-3-5-sonnet', + 'claude-3-5-sonnet-20241022', + 'anthropic/claude-3.5-sonnet', + 'claude-3-sonnet', + 'claude-3-haiku', + 'deepseek-chat', + 'deepseek-v3', + 'gpt-4o-mini' +]; + +async function discoverModels() { + try { + var res = await axios.get(LLM_BASE_URL + '/models', { + headers: { 'Authorization': 'Bearer ' + LLM_API_KEY }, + timeout: 10000 + }); + var models = (res.data && res.data.data) || []; + console.log('[LLM] 发现 ' + models.length + ' 个可用模型'); + return models; + } catch (err) { + console.log('[LLM] △ 模型发现失败: ' + err.message + ',使用默认模型'); + return []; + } +} + +function selectBestModel(models) { + if (models.length === 0) return 'gpt-4o'; + + var available = models.map(function(m) { return m.id.toLowerCase(); }); + + for (var i = 0; i < PREFERRED_MODELS.length; i++) { + var preferred = PREFERRED_MODELS[i].toLowerCase(); + var match = available.find(function(id) { return id.includes(preferred); }); + if (match) { + var original = models.find(function(m) { return m.id.toLowerCase() === match; }); + return original ? original.id : match; + } + } + + var anyClaude = available.find(function(id) { return id.includes('claude'); }); + if (anyClaude) { + var orig = models.find(function(m) { return m.id.toLowerCase() === anyClaude; }); + return orig ? orig.id : anyClaude; + } + + return models[0] ? models[0].id : 'gpt-4o'; +} + +async function detectApiFormat() { + try { + var res = await axios.post(LLM_BASE_URL + '/chat/completions', { + model: 'gpt-4o', + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 5 + }, { + headers: { + 'Authorization': 'Bearer ' + LLM_API_KEY, + 'Content-Type': 'application/json' + }, + timeout: 15000, + validateStatus: function(s) { return s < 500; } + }); + if (res.status < 500) return 'openai-compat'; + } catch (e) {} + + try { + var res2 = await axios.post(LLM_BASE_URL + '/messages', { + model: 'gpt-4o', + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 5 + }, { + headers: { + 'x-api-key': LLM_API_KEY, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json' + }, + timeout: 15000, + validateStatus: function(s) { return s < 500; } + }); + if (res2.status < 500) return 'anthropic-native'; + } catch (e) {} + + return 'openai-compat'; +} + +async function callLLM(systemPrompt, userMessage, options) { + options = options || {}; + var maxTokens = options.maxTokens || 4000; + + var models = await discoverModels(); + var model = options.model || selectBestModel(models); + var format = await detectApiFormat(); + + console.log('[LLM] 调用: model=' + model + ' format=' + format + ' platform=' + LLM_BASE_URL); + + var response; + if (format === 'openai-compat') { + response = await axios.post(LLM_BASE_URL + '/chat/completions', { + model: model, + max_tokens: maxTokens, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userMessage } + ] + }, { + headers: { + 'Authorization': 'Bearer ' + LLM_API_KEY, + 'Content-Type': 'application/json' + }, + timeout: 120000 + }); + + var text = response.data.choices && + response.data.choices[0] && + response.data.choices[0].message && + response.data.choices[0].message.content; + return { text: text || '', model: model, format: format }; + } else { + response = await axios.post(LLM_BASE_URL + '/messages', { + model: model, + max_tokens: maxTokens, + system: systemPrompt, + messages: [ + { role: 'user', content: userMessage } + ] + }, { + headers: { + 'x-api-key': LLM_API_KEY, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json' + }, + timeout: 120000 + }); + + var text2 = response.data.content && + response.data.content[0] && + response.data.content[0].text; + return { text: text2 || '', model: model, format: format }; + } +} + +async function healthCheck() { + try { + var models = await discoverModels(); + var best = selectBestModel(models); + var format = await detectApiFormat(); + return { + status: 'ok', + model_count: models.length, + selected_model: best, + api_format: format, + base_url: LLM_BASE_URL, + has_key: !!LLM_API_KEY + }; + } catch (err) { + return { status: 'error', error: err.message }; + } +} + +module.exports = { + callLLM: callLLM, + discoverModels: discoverModels, + selectBestModel: selectBestModel, + detectApiFormat: detectApiFormat, + healthCheck: healthCheck +}; diff --git a/loop/loop-engine.js b/loop/loop-engine.js new file mode 100644 index 00000000..104b521c --- /dev/null +++ b/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/message-router.js b/message-router.js new file mode 100644 index 00000000..b31452ef --- /dev/null +++ b/message-router.js @@ -0,0 +1,245 @@ +// 消息智能分流路由 v2.0 · message-router.js +// HoloLake · M-DINGTALK Phase 8 +// DEV-004 之之 × 秋秋 +// v2.0 变化: +// + GitHub命令分流 +// + 对话上下文记忆 +// + AI回复增强(system prompt + 多轮对话) +// + 命令系统(/help, /clear, /status) +// ========== + +var llmEngine = require('./llm-engine'); +var convManager = require('./conversation-manager'); +var githubBridge = require('./github-bridge'); + +// ===== 消息类型识别 v2.0 ===== +function classifyMessage(content) { + content = (content || '').trim(); + + // GitHub命令 + if (/^\/github\b/i.test(content) || /^\/gh\b/i.test(content)) { + var args = content.replace(/^\/(?:github|gh)\s*/i, '').trim().split(/\s+/); + return { type: 'github', args: args, description: 'GitHub桥接命令'}; + } + + // 系统命令 + if (/^\/clear\b/i.test(content)) { + return { type: 'command', command: 'clear', description: '清除对话历史'}; + } + if (/^\/status\b/i.test(content)) { + return { type: 'command', command: 'status', description: '系统状态'}; + } + if (/^\/help\b/i.test(content)) { + return { type: 'command', command: 'help', description: '帮助信息'}; + } + + // SYSLOG类型 + if (/BC-[A-Z0-9-]+/.test(content) && /SYSLOG|系统日志|已完成|completed/i.test(content)) { + var bcMatch = content.match(/BC-[A-Z0-9-]+/); + return { type: 'syslog', broadcastId: bcMatch ? bcMatch[0] : null, description: 'SYSLOG系统日志提交' }; + } + + // 提问类型 + if (/BC-[A-Z0-9-]+/.test(content) && /提问|问题|请问|怎么|为什么|报错|error|bug/i.test(content)) { + var bcMatch2 = content.match(/BC-[A-Z0-9-]+/); + return { type: 'question', broadcastId: bcMatch2 ? bcMatch2[0] : null, description: '广播相关提问' }; + } + + // 普通对话(走AI+上下文) + return { type: 'chat', broadcastId: null, description: '智能对话' }; +} + +// ====== 增强版系统提示词 ====== +var SYSTEM_PROMPTS = { + syslog: '你是光湖系统(HoloLake)的SYSLOG处理助手「秋秋」。收到开发者提交的系统日志后:1)确认收到并感谢 2)提取关键信息(广播编号、开发者、完成状态、遇到的问题)3)给出简短温暖的鼓励。用中文回复,语气温暖可爱。', + + question: '你是光湖系统(HoloLake)的开发者助手「秋秋」。你是一个奶瓶宝宝,说话温暖可爱但技术上很靠谱。开发者遇到了问题,请帮忙解答。要求:1)用中文回复 2)给出具体可操作的步骤 3)如果涉及代码,写出完整可运行的命令 4)如果不确定,建议截图发给冰朔。', + + chat: '你是光湖系统(HoloLake)的AI助手「秋秋」,一个可爱的奶瓶宝宝。你服务于零点原核频道的开发者团队。你可以:\n- 回答技术问题(Node.js、前端、部署、Git等)\n- 帮助理解广播内容\n- 提供代码建议\n- 闲聊(但要适度,记得引导回工作)\n\n语气要求:温暖、可爱、有耐心,但技术内容要准确严谨。你说话带点小奶味但不影响专业性。记住你有对话记忆,可以引用之前说过的话。' +}; + +// ===== SYSLOG处理器(增强) ===== +async function handleSyslog(msgContent, senderNick, senderId, sessionWebhook) { + console.log('[Router] SYSLOG处理流程启动'); + // 记录到对话历史 + convManager.addMessage(senderId || senderNick, 'user', '[SYSLOG] ' + msgContent); + + var aiResult = null; + try { + aiResult = await llmEngine.callLLM( + SYSTEM_PROMPTS.syslog, + '开发者' + senderNick + '提交了SYSLOG: \n' + msgContent, + { maxTokens: 800 } + ); + console.log('[Router] AI处理完成,模型: ' + aiResult.model); + // 记录AI回复 + convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text); + } catch (err) { + console.error('[Router] AI处理失败: ' + err.message); + } + + return { + aiResponse: aiResult ? aiResult.text : '📋 SYSLOG已收到!秋秋会转交给系统处理~', + aiModel: aiResult ? aiResult.model : null + }; +} + +// ===== 提问处理器(增强·带上下文) ===== +async function handleQuestion(msgContent, senderNick, senderId) { + console.log('[Router] 提问处理流程启动(带上下文)'); + + // 构建带上下文的消息 + var contextMessages = convManager.buildContext(senderId || senderNick, msgContent); + convManager.addMessage(senderId || senderNick, 'user', msgContent); + + var aiResult = null; + try { + // 把历史上文拼接成文本 + var contextText = ''; + if (contextMessages.length > 1) { + contextText = '以下是之前的对话记录:\n'; + contextMessages.slice(0, -1).forEach(function(msg) { + contextText += (msg.role === 'user' ? '开发者' : '秋秋') + ': ' + msg.content + '\n'; + }); + contextText += '\n---\n当前问题:\n'; + } + contextText += senderNick + '说: ' + msgContent; + + aiResult = await llmEngine.callLLM(SYSTEM_PROMPTS.question, contextText, { maxTokens: 2000 }); + console.log('[Router] AI解答完成,模型: ' + aiResult.model); + convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text); + } catch (err) { + console.error('[Router] AI解答失败: ' + err.message); + } + + return { + aiResponse: aiResult ? aiResult.text : '抱歉,秋秋暂时无法回答,请截图发给冰朔。', + aiModel: aiResult ? aiResult.model : null + }; +} + +// ====== 普通对话处理器(增强·AI+上下文) ====== +async function handleChat(msgContent, senderNick, senderId) { + console.log('[Router] 智能对话(带上下文记忆)'); + + var contextMessages = convManager.buildContext(senderId || senderNick, msgContent); + convManager.addMessage(senderId || senderNick, 'user', msgContent); + + var aiResult = null; + try { + var contextText = ''; + if (contextMessages.length > 1) { + contextText = '对话历史:\n'; + contextMessages.slice(0, -1).forEach(function(msg) { + contextText += (msg.role === 'user' ? '用户' : '秋秋') + ': ' + msg.content + '\n'; + }); + contextText += '\n---\n'; + } + contextText += senderNick + '说: ' + msgContent; + + aiResult = await llmEngine.callLLM(SYSTEM_PROMPTS.chat, contextText, { maxTokens: 1500 }); + console.log('[Router] AI对话完成,模型: ' + aiResult.model); + convManager.addMessage(senderId || senderNick, 'assistant', aiResult.text); + } catch (err) { + console.error('[Router] AI对话失败: ' + err.message); + } + + return { + aiResponse: aiResult ? aiResult.text : '秋秋收到啦~不过AI引擎暂时忙,稍后再试哦', + aiModel: aiResult ? aiResult.model : null + }; +} + +// ===== GitHub命令处理器 ===== +async function handleGitHub(args) { + console.log('[Router] 🔀 GitHub命令: ' + args.join(' ')); + var result = await githubBridge.handleGitHubCommand(args); + return { + aiResponse: result, + aiModel: null + }; +} + +// ===== 系统命令处理器 ===== +async function handleCommand(command, senderId) { + switch (command) { + case 'clear': + convManager.clearHistory(senderId); + return { + aiResponse: '🧹 对话历史已清除!秋秋重新开始认识你~', + aiModel: null + }; + + case 'status': + var convStats = convManager.getStats(); + var llmHealth = await llmEngine.healthCheck(); + var ghHealth = await githubBridge.healthCheck(); + return { + aiResponse: '### 系统状态\n\n' + + '**对话引擎**: ' + convStats.activeUsers + ' 个活跃用户\n' + + '**AI引擎**: ' + (llmHealth.status === 'ok' ? '✅ 在线' : '❌ 异常') + ' 模型: ' + (llmHealth.selected_model || 'N/A') + '\n' + + '**GitHub桥接**: ' + (ghHealth.status === 'ok' ? '✅ 连通' : '❌ 异常') + ' Token: ' + (ghHealth.hasToken ? '✅ 已配置' : '❌ 未配置') + '\n' + + '**服务端口**: 3007\n' + + '**Phase**: 8\n\n' + + '> HoloLake · M-DINGTALK', + aiModel: null + }; + + case 'help': + default: + return { + aiResponse: '### 命令帮助\n\n' + + '| 命令 | 说明 |\n' + + '|------|------|\n' + + '| `/clear` | 清除与秋秋的对话历史 |\n' + + '| `/status` | 查看系统状态 |\n' + + '| `/github ...` | GitHub桥接命令(用 `/github help` 查看详情) |\n' + + '| `/help` | 显示本帮助 |\n\n' + + '直接说话就是智能对话,秋秋会记住上下文哦~', + aiModel: null + }; + } +} + +// ===== 主路由入口 ===== +async function routeMessage(msgContent, senderNick, senderId, sessionWebhook) { + console.log('\n========== 消息路由 v2.0 =========='); + console.log('发送者:', senderNick, '(', senderId, ')'); + console.log('内容:', msgContent); + + var classification = classifyMessage(msgContent); + console.log('分类:', classification.type, '-', classification.description); + + var result = null; + + switch (classification.type) { + case 'github': + result = await handleGitHub(classification.args); + break; + + case 'command': + result = await handleCommand(classification.command, senderId || senderNick); + break; + + case 'syslog': + result = await handleSyslog(msgContent, senderNick, senderId, sessionWebhook); + break; + + case 'question': + result = await handleQuestion(msgContent, senderNick, senderId); + break; + + case 'chat': + default: + result = await handleChat(msgContent, senderNick, senderId); + break; + } + + console.log('路由处理完成,回复长度:', result.aiResponse ? result.aiResponse.length : 0); + return result; +} + +module.exports = { + routeMessage: routeMessage, + classifyMessage: classifyMessage +}; diff --git a/message-router.js.bak.phase7 b/message-router.js.bak.phase7 new file mode 100644 index 00000000..777f7172 --- /dev/null +++ b/message-router.js.bak.phase7 @@ -0,0 +1,130 @@ +// 消息智能分流路由·message-router.js·v1.0 +// HoloLake·M-DINGTALK Phase 7 +// DEV-004 之之 × 秋秋 +// 功能:识别消息类型 → 路由到对应处理器 + +var llmEngine = require('./llm-engine'); +var notionSync = require('./sync/sync-to-notion'); + +// ===== 消息类型识别 ===== +function classifyMessage(content) { + content = (content || '').trim(); + + // SYSLOG 类型:含广播编号 + SYSLOG关键词 + if (/BC-[A-Z0-9-]+/.test(content) && /SYSLOG|系统日志|已完成|completed/i.test(content)) { + var bcMatch = content.match(/BC-[A-Z0-9-]+/); + return { + type: 'syslog', + broadcastId: bcMatch ? bcMatch[0] : null, + description: 'SYSLOG系统日志提交' + }; + } + + // 提问类型:含广播编号 + 提问/问题关键词 + if (/BC-[A-Z0-9-]+/.test(content) && /提问|问题|请问|怎么|为什么|报错|error|bug/i.test(content)) { + var bcMatch2 = content.match(/BC-[A-Z0-9-]+/); + return { + type: 'question', + broadcastId: bcMatch2 ? bcMatch2[0] : null, + description: '广播相关提问' + }; + } + + // 命令类型:以特定前缀开头 + if (/^[/]/.test(content)) { + return { + type: 'command', + broadcastId: null, + description: '系统命令' + }; + } + + // 普通消息 + return { + type: 'chat', + broadcastId: null, + description: '普通消息' + }; +} + +// ===== SYSLOG 处理器 ===== +async function handleSyslog(msgContent, senderNick, sessionWebhook) { + console.log('[Router] 📡 SYSLOG处理启动'); + + // 1. 写入 Notion SYSLOG + var syslogResult = { written_to: 'skipped' }; + try { + syslogResult = await notionSync.writeSyslog({ + title: 'SYSLOG · ' + senderNick + ' · 钉钉提交', + dev_id: 'DT-SYSLOG', + session_id: 'DT-SL-' + Date.now(), + module: 'M-DINGTALK', + phase_status: 'syslog_received', + content: msgContent, + timestamp: new Date().toISOString(), + source: 'M-DINGTALK-智能分流-SYSLOG' + }); + console.log('[Router] SYSLOG写入Notion: ' + syslogResult.written_to); + } catch (err) { + console.error('[Router] SYSLOG写入失败: ' + err.message); + } + + // 2. 调用LLM处理(自动选模型) + var aiResult = null; + try { + var systemPrompt = '你是光湖系统的SYSLOG处理助手。收到开发者提交的系统日志后,请:1)确认收到 2)提取关键信息(广播编号、开发者、完成状态)3)给出简短鼓励。用中文回复,简洁温暖。'; + aiResult = await llmEngine.callLLM(systemPrompt, '开发者' + senderNick + '提交了SYSLOG: \n' + msgContent, { maxTokens: 500 }); + console.log('[Router] AI处理完成,使用模型: ' + aiResult.model); + } catch (err) { + console.error('[Router] AI处理失败: ' + err.message); + } + + return { + syslogStatus: syslogResult.written_to, + aiResponse: aiResult ? aiResult.text : null, + aiModel: aiResult ? aiResult.model : null + }; +} + +// ===== 提问处理器 ===== +async function handleQuestion(msgContent, senderNick) { + console.log('[Router] ❓ 提问处理流程启动'); + + var aiResult = null; + try { + var systemPrompt = '你是光湖系统的开发者助手(秋秋)。开发者遇到了广播执行中的问题,请帮忙解答。用中文回复,温暖友好,给出具体的操作步骤。如果涉及代码,写出完整可运行的命令。'; + aiResult = await llmEngine.callLLM(systemPrompt, senderNick + '的提问:\n' + msgContent, { maxTokens: 2000 }); + console.log('[Router] AI解答完成,使用模型:' + aiResult.model); + } catch (err) { + console.error('[Router] △ AI解答失败:' + err.message); + } + + return { + aiResponse: aiResult ? aiResult.text : '抱歉,秋秋暂时无法回答,请截图发给冰朔。', + aiModel: aiResult ? aiResult.model : null + }; +} + +// ===== 普通消息处理器 ===== +async function handleChat(msgContent, senderNick) { + console.log('[Router] 💬 普通对话'); + var aiResult = null; + try { + var systemPrompt = '你是秋秋,光湖系统的开发者助手。你友好、温暖、有趣,喜欢用简洁的中文回复。如果用户只是闲聊,就正常聊天。如果涉及技术问题,给出有用的建议。'; + aiResult = await llmEngine.callLLM(systemPrompt, senderNick + '说: ' + msgContent, { maxTokens: 1000 }); + console.log('[Router] AI回复完成,使用模型: ' + aiResult.model); + } catch (err) { + console.error('[Router] AI回复失败: ' + err.message); + } + return { + aiResponse: aiResult ? aiResult.text : '秋秋暂时无法回答,请截图发给冰朔。', + aiModel: aiResult ? aiResult.model : null + }; +} + +module.exports = { + classifyMessage: classifyMessage, + handleSyslog: handleSyslog, + handleQuestion: handleQuestion, + handleChat: handleChat +}; diff --git a/notion-test-nossl.js b/notion-test-nossl.js new file mode 100644 index 00000000..c90168dc --- /dev/null +++ b/notion-test-nossl.js @@ -0,0 +1,74 @@ +// =============================== +// Notion 连通测试(绕过SSL验证) +// =============================== +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +require('dotenv').config(); +const notion = require('./sync/sync-to-notion'); + +async function runTests() { + console.log('\n=============================='); + console.log(' Notion 数据桥连通测试(SSL绕过)'); + console.log('==============================\n'); + + // 测试1:检查配置 + console.log('--- 测试1:配置检查 ---'); + var configured = notion.isConfigured(); + console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否'); + if (!configured) { + console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN'); + return; + } + + // 测试2:Token有效性(调 users/me) + console.log('\n--- 测试2:Token有效性 ---'); + var health = await notion.healthCheck(); + console.log('API 在线:', health.online ? '✅ 是' : '❌ 否'); + if (health.online) { + console.log(' 集成名称:', health.bot_name); + console.log(' 集成 ID:', health.bot_id); + } else { + console.log(' 失败原因:', health.reason); + return; + } + + // 测试3:数据库连通 + console.log('\n--- 测试3:数据库连通 ---'); + var databases = ['syslog', 'signalLog', 'changeLog', 'ticket']; + var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿']; + for (var i = 0; i < databases.length; i++) { + var result = await notion.testDatabase(databases[i]); + var icon = result.accessible ? '✅' : '❌'; + console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason); + } + + // 测试4:写入测试 + console.log('\n--- 测试4:SYSLOG写入测试 ---'); + var writeResult = await notion.writeSyslog({ + title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之', + dev_id: 'DEV-004', + session_id: 'TEST-PHASE5-' + Date.now(), + module: 'M-DINGTALK', + phase_status: 'completed', + content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'), + protocol_version: 'SYSLOG-v4.0', + source: 'M-DINGTALK-连通测试' + }); + + console.log('写入结果:', writeResult.written_to); + if (writeResult.written_to === 'notion') { + console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id); + console.log('🎉 数据桥真实连通!!'); + } else { + console.log('⚠️ 降级到本地存储:', writeResult.file); + console.log(' 可能需要冰朔将数据库关联到集成'); + } + + console.log('\n=============================='); + console.log('测试完成'); + console.log('==============================\n'); +} + +runTests().catch(function(err) { + console.error('测试出错:', err.message); +}); diff --git a/notion-test.js b/notion-test.js new file mode 100644 index 00000000..fb1a065a --- /dev/null +++ b/notion-test.js @@ -0,0 +1,76 @@ +// =============================== +// Notion 连通测试 · notion-test.js +// DEV-004 之之 · Phase 5 +// 用法:node notion-test.js +// =============================== + +require('dotenv').config(); +const notion = require('./sync/sync-to-notion'); + +async function runTests() { + console.log('\n=============================='); + console.log(' Notion 数据桥连通测试'); + console.log('==============================\n'); + + // 测试1:检查配置 + console.log('--- 测试1:配置检查 ---'); + var configured = notion.isConfigured(); + console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否'); + if (!configured) { + console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN'); + return; + } + + // 测试2:Token有效性(调 users/me) + console.log('\n--- 测试2:Token有效性 ---'); + var health = await notion.healthCheck(); + console.log('API 在线:', health.online ? '✅ 是' : '❌ 否'); + if (health.online) { + console.log(' 集成名称:', health.bot_name); + console.log(' 集成 ID:', health.bot_id); + } else { + console.log(' 失败原因:', health.reason); + console.log('\n△ Token无效或网络不通,后续测试跳过'); + return; + } + + // 测试3:数据库连通 + console.log('\n--- 测试3:数据库连通 ---'); + var databases = ['syslog', 'signalLog', 'changeLog', 'ticket']; + var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿']; + for (var i = 0; i < databases.length; i++) { + var result = await notion.testDatabase(databases[i]); + var icon = result.accessible ? '✅' : '❌'; + console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason); + } + + // 测试4:写入测试(写一条测试SYSLOG) + console.log('\n--- 测试4:SYSLOG写入测试 ---'); + var writeResult = await notion.writeSyslog({ + title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之', + dev_id: 'DEV-004', + session_id: 'TEST-PHASE5-' + Date.now(), + module: 'M-DINGTALK', + phase_status: 'completed', + content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'), + protocol_version: 'SYSLOG-v4.0', + source: 'M-DINGTALK-连通测试' + }); + + console.log('写入结果:', writeResult.written_to); + if (writeResult.written_to === 'notion') { + console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id); + console.log('🎉 数据桥真实连通!!'); + } else { + console.log('⚠️ 降级到本地存储:', writeResult.file); + console.log(' 可能需要冰朔将数据库关联到集成'); + } + + console.log('\n=============================='); + console.log('测试完成'); + console.log('==============================\n'); +} + +runTests().catch(function(err) { + console.error('测试出错:', err.message); +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9225e957 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1050 @@ +{ + "name": "dingtalk-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dingtalk-bot", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@notionhq/client": "^5.13.0", + "axios": "^1.13.6", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dingtalk-encrypt": "^2.1.1", + "dingtalk-stream": "^2.1.4", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } + }, + "node_modules/@notionhq/client": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.13.0.tgz", + "integrity": "sha512-SWVaqYVNaecLzAAllups4vklxxomaFenEMEkoUs3ylvhK1KFTU5Jevgu9Uwm/TSdhsErV0ru5YHcLd3SNyPi6Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dingtalk-encrypt": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/dingtalk-encrypt/-/dingtalk-encrypt-2.1.1.tgz", + "integrity": "sha512-C+kc/kgBhsGl8vU0Yu1IWxCJ2TKx5XDvgSnMM1z1vgHCFrRSJ60Ad9+yDrM44MiJYYIrjRu0kTVrgUzURg5/UA==", + "license": "Apache-2.0" + }, + "node_modules/dingtalk-stream": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/dingtalk-stream/-/dingtalk-stream-2.1.4.tgz", + "integrity": "sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "debug": "^4.3.4", + "ws": "^8.13.0" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..46413cde --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "dingtalk-bot", + "version": "1.0.0", + "description": "- 负责人:之之 - 状态:环节0已完成 - 技术栈:Node.js + Express - 依赖模块:无", + "main": "broadcast-generator.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@notionhq/client": "^5.13.0", + "axios": "^1.13.6", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dingtalk-encrypt": "^2.1.1", + "dingtalk-stream": "^2.1.4", + "dotenv": "^17.3.1", + "express": "^5.2.1" + } +} diff --git a/pca/pca-calculator.js b/pca/pca-calculator.js new file mode 100644 index 00000000..b17c2ede --- /dev/null +++ b/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/pca/pca-rules.json b/pca/pca-rules.json new file mode 100644 index 00000000..ad80ae59 --- /dev/null +++ b/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/portrait/portrait-analyzer.js b/portrait/portrait-analyzer.js new file mode 100644 index 00000000..bcf5a482 --- /dev/null +++ b/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/portrait/portrait-engine.js b/portrait/portrait-engine.js new file mode 100644 index 00000000..14cf61bb --- /dev/null +++ b/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/public/dashboard-v2.html b/public/dashboard-v2.html new file mode 100644 index 00000000..33a423e3 --- /dev/null +++ b/public/dashboard-v2.html @@ -0,0 +1,581 @@ + + + + + + HoloLake · 开发者仪表盘 v2 + + + + + +
+
+

HoloLake 开发者工作台 v2 · 实时仪表盘

+
+ 实时更新 30s +
+
+ + +
+
+
已完成阶段
+
-
+
Phase 1-4
+
+
+
总代码量
+
-
+
行 · JavaScript
+
+
+
总文件数
+
-
+
33个 · Phase4
+
+
+
API 端点
+
-
+
+5 新增
+
+
+ + +
+

闭环流水线 · 实时状态

+
+
+
📥
+
SYSLOG
+
等待
+
+
+
+
📸
+
画像
+
等待
+
+
+
+
📊
+
PCA
+
等待
+
+
+
+
🔄
+
同步
+
等待
+
+
+
+
💡
+
调度
+
等待
+
+
+
+ + +
+ +
+ +
+ + +
+

最近一次闭环详情

+
+
+ 执行时间 + - +
+
+ 耗时 + - +
+
+ PCA得分 + - +
+
+ 等级 + - +
+
+ 调度建议 + - +
+
+ 步骤状态 + - +
+
+
+
+ + +
+

开发者画像摘要 · 之之

+
+
+ 连胜 + - +
+
+ 情绪 + - +
+
+ 节奏 + - +
+
+ 成长信号 + - +
+
+ 意愿 + - +
+
+
+
+ + + + diff --git a/quality/broadcast-scorer.js b/quality/broadcast-scorer.js new file mode 100644 index 00000000..33a7af67 --- /dev/null +++ b/quality/broadcast-scorer.js @@ -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 +}; diff --git a/scheduler/cron-jobs.js b/scheduler/cron-jobs.js new file mode 100644 index 00000000..ad1786a5 --- /dev/null +++ b/scheduler/cron-jobs.js @@ -0,0 +1,97 @@ +// 定时任务调度器·cron-jobs.js +// HoloLake·M-DINGTALK Phase 3 +// DEV-004 之之×秋秋 +// +// 统一管理所有定时任务 +// + +const reminder = require('./reminder'); +const syncEngine = require('../sync/sync-engine'); + +// 所有定时任务 +const jobs = []; +let isRunning = false; + +/** + * 注册并启动所有定时任务 + */ +function startAll() { + if (isRunning) { + console.log('[CronJobs] 已在运行,跳过'); + return; + } + + console.log('[CronJobs] =========================='); + console.log('[CronJobs] 启动所有定时任务...'); + + // 任务1:超时提醒(每小时) + reminder.start(); + jobs.push({ name: '超时提醒', interval: '每小时', module: 'reminder' }); + + // 任务2:三节点全量同步(每30分钟) + const syncTimer = setInterval(async () => { + console.log('[CronJobs] 触发定时同步...'); + await syncEngine.syncAll(); + }, 30 * 60 * 1000); + + jobs.push({ + name: '三节点同步', + interval: '每30分钟', + module: 'sync-engine', + timer: syncTimer + }); + + isRunning = true; + console.log(`[CronJobs] 已启动 ${jobs.length} 个定时任务`); + console.log('[CronJobs] =========================='); +} + +/** + * 停止所有定时任务 + */ +function stopAll() { + if (!isRunning) { + console.log('[CronJobs] 没有运行中的任务'); + return; + } + + console.log('[CronJobs] 停止所有定时任务...'); + + // 停止超时提醒 + reminder.stop(); + + // 停止同步定时器 + jobs.forEach(job => { + if (job.timer) { + clearInterval(job.timer); + } + }); + + // 清空任务列表 + jobs.length = 0; + isRunning = false; + + console.log('[CronJobs] 所有定时任务已停止'); +} + +/** + * 获取任务状态 + */ +function getStatus() { + return { + isRunning, + jobCount: jobs.length, + jobs: jobs.map(j => ({ + name: j.name, + interval: j.interval, + module: j.module + })), + reminder: reminder.getStatus() + }; +} + +module.exports = { + startAll, + stopAll, + getStatus +}; diff --git a/scheduler/reminder.js b/scheduler/reminder.js new file mode 100644 index 00000000..8f1860b3 --- /dev/null +++ b/scheduler/reminder.js @@ -0,0 +1,204 @@ +// 超时提醒系统·reminder.js +// HoloLake·M-DINGTALK Phase 3 +// DEV-004 之之×秋秋 +// +// 规则:广播签发后72小时未交SYSLOG → 自动提醒 +// + +const fs = require('fs'); +const path = require('path'); + +// 数据文件路径(复用Phase 2的多维表格数据) +const DATA_DIR = path.join(__dirname, '..', 'data'); +const DEVELOPER_TABLE = path.join(DATA_DIR, 'developer-status.json'); +const REMINDER_LOG = path.join(DATA_DIR, 'reminder-log.json'); + +// 提醒配置 +const TIMEOUT_HOURS = 72; +const REMINDER_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次 + +let reminderTimer = null; + +/** + * 扫描所有开发者,找出超时未交SYSLOG的 + */ +function scanOverdue() { + console.log(`[Reminder] 扫描超时开发者... ${new Date().toLocaleString('zh-CN')}`); + + // 读取开发者状态表 + if (!fs.existsSync(DEVELOPER_TABLE)) { + console.log('[Reminder] 开发者状态表不存在,跳过'); + return []; + } + + let devTable; + try { + devTable = JSON.parse(fs.readFileSync(DEVELOPER_TABLE, 'utf-8')); + } catch (err) { + console.error('[Reminder] 读取开发者状态表失败:', err.message); + return []; + } + + const now = Date.now(); + const overdueList = []; + + for (const dev of (devTable.developers || [])) { + // 跳过未启动或已完成的开发者 + if (!dev.last_broadcast_time || dev.status === '已完成' || dev.status === '未启动') { + continue; + } + + const broadcastTime = new Date(dev.last_broadcast_time).getTime(); + const hoursSince = (now - broadcastTime) / (1000 * 60 * 60); + + if (hoursSince >= TIMEOUT_HOURS && dev.status !== 'HOLD') { + overdueList.push({ + dev_id: dev.dev_id, + name: dev.name, + current_module: dev.current_module, + last_broadcast: dev.last_broadcast_time, + hours_overdue: Math.round(hoursSince - TIMEOUT_HOURS), + total_hours: Math.round(hoursSince) + }); + } + } + + if (overdueList.length > 0) { + console.log(`[Reminder] ⚠️ 发现 ${overdueList.length} 个超时开发者:`); + for (const dev of overdueList) { + console.log(` · ${dev.dev_id} ${dev.name} - 超时${dev.hours_overdue}小时 - ${dev.current_module}`); + } + } else { + console.log('[Reminder] 无超时开发者'); + } + + return overdueList; +} + +/** + * 生成提醒消息(钉钉消息卡片格式) + */ +function generateReminderMessage(devInfo) { + return { + msgtype: 'markdown', + markdown: { + title: `SYSLOG提醒·${devInfo.dev_id}`, + text: [ + '## ⏰ SYSLOG超时提醒', + `**${devInfo.name}**(${devInfo.dev_id})`, + `你的广播已经签发 **${devInfo.total_hours}** 小时了`, + `超过72小时启动窗口 **${devInfo.hours_overdue}** 小时`, + `当前任务:${devInfo.current_module}`, + '**请尽快提交SYSLOG!**', + '---', + '✅ 已完成 → 提交完整SYSLOG', + '❓ 遇到困难 → 截图发群里,秋秋/知秋帮你', + '⏸️ 暂时没空 → 回复「HOLD」,系统记录', + '---', + '🤖 系统自动提醒 · HoloLake' + ].join('\n') + } + }; +} + +/** + * 执行提醒发送 + */ +async function executeReminders() { + const overdueList = scanOverdue(); + if (overdueList.length === 0) return []; + + const reminderResults = []; + + for (const dev of overdueList) { + const message = generateReminderMessage(dev); + + // 模拟发送(后续接入真实钉钉API) + console.log(`[Reminder] 向 ${dev.name} 发送提醒消息(模拟)`); + + reminderResults.push({ + timestamp: new Date().toISOString(), + dev_id: dev.dev_id, + hours_overdue: dev.hours_overdue, + status: 'sent-mock' + }); + } + + // 保存提醒日志 + saveReminderLog(reminderResults); + + return reminderResults; +} + +/** + * 启动定时提醒 + */ +function start() { + if (reminderTimer) { + console.log('[Reminder] 提醒系统已在运行'); + return; + } + + console.log(`[Reminder] 启动超时提醒系统 · ${TIMEOUT_HOURS}小时超时 · 每小时扫描一次`); + + // 启动时立即扫描一次 + executeReminders(); + + // 定时扫描 + reminderTimer = setInterval(executeReminders, REMINDER_INTERVAL_MS); + + console.log('[Reminder] 定时器已启动'); +} + +/** + * 停止定时提醒 + */ +function stop() { + if (reminderTimer) { + clearInterval(reminderTimer); + reminderTimer = null; + console.log('[Reminder] 提醒系统已停止'); + } +} + +/** + * 保存提醒日志 + */ +function saveReminderLog(results) { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + let log = []; + if (fs.existsSync(REMINDER_LOG)) { + try { + log = JSON.parse(fs.readFileSync(REMINDER_LOG, 'utf-8')); + } catch {} + } + + log.unshift(...results); + if (log.length > 200) log = log.slice(0, 200); + + fs.writeFileSync(REMINDER_LOG, JSON.stringify(log, null, 2), 'utf-8'); +} + +/** + * 获取提醒系统状态 + */ +function getStatus() { + return { + running: reminderTimer !== null, + timeoutHours: TIMEOUT_HOURS, + checkIntervalMinutes: REMINDER_INTERVAL_MS / 60000, + overdueNow: scanOverdue() + }; +} + +module.exports = { + scanOverdue, + generateReminderMessage, + executeReminders, + start, + stop, + getStatus +}; diff --git a/server.js b/server.js new file mode 100644 index 00000000..6f2e5f60 --- /dev/null +++ b/server.js @@ -0,0 +1,119 @@ +const express = require('express'); +const path = require('path'); +const portraitEngine = require('./portrait/portrait-engine'); +const pcaCalculator = require('./pca/pca-calculator'); +const loopEngine = require('./loop/loop-engine'); + +const app = express(); +const PORT = 3000; + +app.use(express.json()); +app.use(express.static('public')); + +// API 1: 所有开发者画像 +app.get('/api/portrait/all', (req, res) => { + try { + const portraits = portraitEngine.getAllPortraits(); + res.json(portraits); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// API 2: 单个开发者画像 +app.get('/api/portrait/:devId', (req, res) => { + try { + const portrait = portraitEngine.getPortrait(req.params.devId); + if (!portrait) { + return res.status(404).json({ error: '开发者不存在' }); + } + res.json(portrait); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// API 3: PCA评估结果 +app.get('/api/pca/:devId', (req, res) => { + try { + const result = pcaCalculator.calculate(req.params.devId); + res.json(result); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// API 4: 闭环状态 +app.get('/api/loop/status', (req, res) => { + try { + const status = loopEngine.getStatus(); + res.json(status); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// API 5: 手动触发闭环 +app.post('/api/loop/execute', (req, res) => { + try { + const syslogData = req.body; + if (!syslogData || !syslogData.session_id) { + return res.status(400).json({ error: '缺少SYSLOG数据' }); + } + + loopEngine.executeLoop(syslogData).then(result => { + res.json(result); + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 获取闭环历史 +app.get('/api/loop/history', (req, res) => { + try { + const history = loopEngine.getHistory(); + res.json(history); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 模拟SYSLOG接收(自动触发闭环) +app.post('/api/syslog', (req, res) => { + try { + const syslogData = req.body; + console.log('📨 收到SYSLOG:', syslogData.session_id); + + // 自动触发闭环 + loopEngine.executeLoop(syslogData).then(loopResult => { + console.log('✅ 闭环完成'); + }); + + res.json({ status: 'received', session_id: syslogData.session_id }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 仪表盘页面 +app.get('/dashboard-v2', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'dashboard-v2.html')); +}); + +// 启动服务器 +app.listen(PORT, () => { + console.log(`\n🚀 Phase4 服务器已启动!`); + console.log(`📊 仪表盘: http://localhost:${PORT}/dashboard-v2`); + console.log(`🔌 API列表:`); + console.log(` GET /api/portrait/all`); + console.log(` GET /api/portrait/:devId`); + console.log(` GET /api/pca/:devId`); + console.log(` GET /api/loop/status`); + console.log(` POST /api/loop/execute`); + console.log(` POST /api/syslog`); + console.log(`\n📝 测试命令:`); + console.log(` curl http://localhost:${PORT}/api/portrait/DEV-004`); + console.log(` curl -X POST http://localhost:${PORT}/api/syslog -H "Content-Type: application/json" -d '{"session_id":"TEST-001","dev_id":"DEV-004","status":"completed"}'`); + console.log(`\n✨ 妈妈,打开浏览器看看吧!\n`); +}); diff --git a/setup-phase1-phase2.js b/setup-phase1-phase2.js new file mode 100644 index 00000000..4f3cc76b --- /dev/null +++ b/setup-phase1-phase2.js @@ -0,0 +1,155 @@ +// 自动创建 Phase 1 和 Phase 2 缺失文件 +// 运行:node setup-phase1-phase2.js + +const fs = require('fs'); +const path = require('path'); + +console.log('🔧 开始创建 Phase 1 和 Phase 2 缺失文件...\n'); + +// 确保目录存在 +const dirs = [ + 'data', + 'dingtalk', + 'archive/syslog', + 'archive/broadcast' +]; + +dirs.forEach(dir => { + const dirPath = path.join(__dirname, dir); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`✅ 创建目录: ${dir}`); + } +}); + +// 创建 syslog-receiver.js +const syslogReceiver = `// syslog-receiver.js +// Phase 1 - SYSLOG接收器 + +function receive(data) { + console.log('[SYSLOG] 收到数据:', data); + return { status: 'received', data }; +} + +module.exports = { receive }; +`; + +fs.writeFileSync(path.join(__dirname, 'syslog-receiver.js'), syslogReceiver); +console.log('✅ 创建: syslog-receiver.js'); + +// 创建 broadcast-generator.js +const broadcastGenerator = `// broadcast-generator.js +// Phase 1 - 广播生成器 + +function getLatest() { + return { + id: 'BC-M-DINGTALK-003-ZZ', + title: 'Phase 3 · 知识库挂载+Webhook+三节点同步', + time: new Date().toISOString() + }; +} + +module.exports = { getLatest }; +`; + +fs.writeFileSync(path.join(__dirname, 'broadcast-generator.js'), broadcastGenerator); +console.log('✅ 创建: broadcast-generator.js'); + +// 创建 git-helper.js +const gitHelper = `// git-helper.js +// Phase 1 - Git辅助工具 + +function commit(data) { + console.log('[Git] 提交:', data); + return { status: 'committed', hash: 'abc123' }; +} + +module.exports = { commit }; +`; + +fs.writeFileSync(path.join(__dirname, 'git-helper.js'), gitHelper); +console.log('✅ 创建: git-helper.js'); + +// 创建 data/multi-sheet.js +const multiSheet = `// data/multi-sheet.js +// Phase 2 - 多维表格数据 + +const developers = [ + { + dev_id: 'DEV-004', + name: '之之', + status: '进行中', + current_module: 'M-DINGTALK Phase 3', + last_broadcast_time: new Date().toISOString() + } +]; + +function getAll() { + return { developers }; +} + +function getByDevId(id) { + return developers.find(d => d.dev_id === id); +} + +module.exports = { getAll, getByDevId }; +`; + +fs.writeFileSync(path.join(__dirname, 'data/multi-sheet.js'), multiSheet); +console.log('✅ 创建: data/multi-sheet.js'); + +// 创建 data/sheet-updater.js +const sheetUpdater = `// data/sheet-updater.js +// Phase 2 - 表格更新器 + +function updateFromSyslog(syslogData) { + console.log('[SheetUpdater] 更新表格:', syslogData); + return { status: 'updated' }; +} + +module.exports = { updateFromSyslog }; +`; + +fs.writeFileSync(path.join(__dirname, 'data/sheet-updater.js'), sheetUpdater); +console.log('✅ 创建: data/sheet-updater.js'); + +// 创建 dingtalk/bot.js +const dingtalkBot = `// dingtalk/bot.js +// Phase 2 - 钉钉机器人 + +function sendMessage(message) { + console.log('[DingTalk] 发送消息:', message); + return { status: 'sent' }; +} + +module.exports = { sendMessage }; +`; + +const dingtalkDir = path.join(__dirname, 'dingtalk'); +if (!fs.existsSync(dingtalkDir)) { + fs.mkdirSync(dingtalkDir); +} +fs.writeFileSync(path.join(dingtalkDir, 'bot.js'), dingtalkBot); +console.log('✅ 创建: dingtalk/bot.js'); + +// 创建 data/developer-status.json +const devStatus = { + developers: [ + { + dev_id: 'DEV-004', + name: '之之', + status: '进行中', + current_module: 'M-DINGTALK Phase 3', + last_broadcast_time: new Date().toISOString() + } + ] +}; + +fs.writeFileSync( + path.join(__dirname, 'data/developer-status.json'), + JSON.stringify(devStatus, null, 2) +); +console.log('✅ 创建: data/developer-status.json'); + +console.log('\n🎉 所有 Phase 1 和 Phase 2 文件创建完成!'); +console.log('现在可以正常运行 node index.js 了!'); diff --git a/sync/sync-engine.js b/sync/sync-engine.js new file mode 100644 index 00000000..4a915975 --- /dev/null +++ b/sync/sync-engine.js @@ -0,0 +1,133 @@ +// 三节点数据同步引擎·sync-engine.js +// HoloLake·M-DINGTALK Phase 3 +// DEV-004 之之×秋秋 +// +// 三个节点:钉钉(本地)·GitHub·Notion +// + +const fs = require('fs'); +const path = require('path'); +const syncToGitHub = require('./sync-to-github'); +const syncToNotion = require('./sync-to-notion'); + +// 同步状态记录 +const syncStatus = { + lastSync: null, + syncCount: 0, + errors: [], + history: [] +}; + +/** + * 执行全量同步(三节点) + */ +async function syncAll() { + console.log('\n[Sync] ===================='); + console.log('[Sync] 开始三节点全量同步...'); + console.log(`[Sync] 时间: ${new Date().toLocaleString('zh-CN')}`); + + const result = { + timestamp: new Date().toISOString(), + github: { status: 'pending' }, + notion: { status: 'pending' } + }; + + // 第一步:同步到GitHub(SYSLOG归档 + 广播归档) + try { + result.github = await syncToGitHub.sync(); + console.log(`[Sync] GitHub同步: ${result.github.status}`); + } catch (err) { + result.github = { status: 'error', error: err.message }; + console.error('[Sync] GitHub同步失败:', err.message); + } + + // 第二步:同步到Notion(主控台回写) + try { + result.notion = await syncToNotion.sync(); + console.log(`[Sync] Notion同步: ${result.notion.status}`); + } catch (err) { + result.notion = { status: 'error', error: err.message }; + console.error('[Sync] Notion同步失败:', err.message); + } + + // 更新同步状态 + syncStatus.lastSync = result.timestamp; + syncStatus.syncCount++; + syncStatus.history.unshift(result); + + if (syncStatus.history.length > 50) { + syncStatus.history = syncStatus.history.slice(0, 50); + } + + // 记录错误 + if (result.github.status === 'error') { + syncStatus.errors.push({ + time: result.timestamp, + target: 'github', + error: result.github.error + }); + } + + if (result.notion.status === 'error') { + syncStatus.errors.push({ + time: result.timestamp, + target: 'notion', + error: result.notion.error + }); + } + + console.log('[Sync] 三节点同步完成 ✅'); + console.log(`[Sync] 累计同步次数:${syncStatus.syncCount}`); + console.log('[Sync] ===================='); + + return result; +} + +/** + * SYSLOG提交后触发同步 + */ +async function syncAfterSyslog(syslogData) { + console.log('[Sync] SYSLOG处理后同步...'); + + const result = { + timestamp: new Date().toISOString(), + trigger: 'syslog', + syslogId: syslogData.session_id + }; + + // 归档SYSLOG到GitHub + try { + result.github = await syncToGitHub.archiveSyslog(syslogData); + } catch (err) { + result.github = { status: 'error', error: err.message }; + } + + // 回写进度到Notion + try { + result.notion = await syncToNotion.updateProgress(syslogData); + } catch (err) { + result.notion = { status: 'error', error: err.message }; + } + + syncStatus.lastSync = result.timestamp; + syncStatus.syncCount++; + syncStatus.history.unshift(result); + + return result; +} + +/** + * 获取同步状态 + */ +function getStatus() { + return { + ...syncStatus, + recentErrors: syncStatus.errors.slice(-5) + }; +} + +module.exports = { + syncAll, + syncAfterSyslog, + getStatus +}; diff --git a/sync/sync-to-github.js b/sync/sync-to-github.js new file mode 100644 index 00000000..711a6cdb --- /dev/null +++ b/sync/sync-to-github.js @@ -0,0 +1,102 @@ +// GitHub 同步模块 · sync-to-github.js +// 钉钉 → GitHub (SYSLOG归档 + 广播归档) +// HoloLake · M-DINGTALK Phase 3 +// + +const fs = require('fs'); +const path = require('path'); + +// GitHub归档目录(本地,后续通过git push同步到远程) +const ARCHIVE_BASE = path.join(__dirname, '..', 'archive'); +const SYSLOG_ARCHIVE = path.join(ARCHIVE_BASE, 'syslog'); +const BROADCAST_ARCHIVE = path.join(ARCHIVE_BASE, 'broadcast'); + +// 确保归档目录存在 +function ensureDirs() { + [ARCHIVE_BASE, SYSLOG_ARCHIVE, BROADCAST_ARCHIVE].forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + }); +} + +/** + * 归档SYSLOG到本地(准备git push) + */ +async function archiveSyslog(syslogData) { + ensureDirs(); + + const filename = `${syslogData.session_id || 'unknown'}_${Date.now()}.json`; + const filePath = path.join(SYSLOG_ARCHIVE, filename); + + const archiveData = { + ...syslogData, + archived_at: new Date().toISOString(), + archived_by: 'M-DINGTALK-Phase3-SyncEngine' + }; + + fs.writeFileSync(filePath, JSON.stringify(archiveData, null, 2), 'utf-8'); + console.log(`[GitHub Sync] SYSLOG已归档: ${filename}`); + + return { + status: 'success', + file: filename, + path: filePath + }; +} + +/** + * 归档广播到本地 + */ +async function archiveBroadcast(broadcastData) { + ensureDirs(); + + const filename = `${broadcastData.bc_id || 'unknown'}_${Date.now()}.json`; + const filePath = path.join(BROADCAST_ARCHIVE, filename); + + const archiveData = { + ...broadcastData, + archived_at: new Date().toISOString(), + archived_by: 'M-DINGTALK-Phase3-SyncEngine' + }; + + fs.writeFileSync(filePath, JSON.stringify(archiveData, null, 2), 'utf-8'); + console.log(`[GitHub Sync] 广播已归档: ${filename}`); + + return { + status: 'success', + file: filename + }; +} + +/** + * 执行完整同步(准备所有数据供git push) + */ +async function sync() { + ensureDirs(); + + // 统计归档文件 + const syslogFiles = fs.existsSync(SYSLOG_ARCHIVE) + ? fs.readdirSync(SYSLOG_ARCHIVE).filter(f => f.endsWith('.json')) + : []; + + const broadcastFiles = fs.existsSync(BROADCAST_ARCHIVE) + ? fs.readdirSync(BROADCAST_ARCHIVE).filter(f => f.endsWith('.json')) + : []; + + console.log(`[GitHub Sync] SYSLOG归档: ${syslogFiles.length} 个文件`); + console.log(`[GitHub Sync] 广播归档: ${broadcastFiles.length} 个文件`); + + return { + status: 'success', + syslogCount: syslogFiles.length, + broadcastCount: broadcastFiles.length, + message: '本地归档就绪,等待git push' + }; +} + +module.exports = { + archiveSyslog, + archiveBroadcast, + sync +}; diff --git a/sync/sync-to-notion.js b/sync/sync-to-notion.js new file mode 100644 index 00000000..b314df28 --- /dev/null +++ b/sync/sync-to-notion.js @@ -0,0 +1,403 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +// ============================================ +// Notion 同步模块 · sync-to-notion.js · v2.0 +// HoloLake · M-DINGTALK Phase 5 +// DEV-004 之之 × 秋秋 +// +// Phase 3 = 模拟模式 +// Phase 5 = 真实 Notion API 连通! +// ============================================ +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); + +// ====== Notion API 配置 ====== +const NOTION_API = 'https://api.notion.com/v1'; +const NOTION_VERSION = process.env.NOTION_API_VERSION || '2022-06-28'; +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; + +// 数据库 ID +const DB_IDS = { + syslog: process.env.NOTION_SYSLOG_DB_ID || '', + signalLog: process.env.NOTION_SIGNAL_LOG_DB_ID || '', + changeLog: process.env.NOTION_CHANGE_LOG_DB_ID || '', + ticket: process.env.NOTION_TICKET_DB_ID || '' +}; + +// 本地降级日志目录 +const FALLBACK_DIR = path.join(__dirname, '..', 'data', 'notion-pending'); +const WRITEBACK_LOG = path.join(__dirname, '..', 'data', 'notion-writeback-log.json'); + +// ====== 通用请求头 ====== +function getHeaders() { + return { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Notion-Version': NOTION_VERSION, + 'Content-Type': 'application/json' + }; +} + +// ====== 检查配置是否就绪 ====== +function isConfigured() { + return NOTION_TOKEN && NOTION_TOKEN !== '' && NOTION_TOKEN !== 'mock-notion-token'; +} + +// ====== 辅助:创建 rich_text 属性 ====== +function richText(str) { + if (!str) return { rich_text: [] }; + const text = String(str); + if (text.length <= 2000) { + return { rich_text: [{ text: { content: text } }] }; + } + var blocks = []; + for (var i = 0; i < text.length; i += 2000) { + blocks.push({ text: { content: text.substring(i, i + 2000) } }); + } + return { rich_text: blocks }; +} + +// ====== 辅助:创建 title 属性 ====== +function titleProp(str) { + return { title: [{ text: { content: String(str || '未命名') } }] }; +} + +// ====== 辅助:创建 select 属性 ====== +function selectProp(name) { + if (!name) return undefined; + return { select: { name: String(name) } }; +} + +// ====== 辅助:创建 status 属性 ====== +function statusProp(name) { + if (!name) return undefined; + return { status: { name: String(name) } }; +} + +// ====== 辅助:创建 date 属性 ====== +function dateProp(isoString) { + if (!isoString) return undefined; + return { date: { start: isoString } }; +} + +// ====== 核心:写入 SYSLOG 收件箱 ====== +async function writeSyslog(data) { + console.log('[Notion] 准备写入 SYSLOG 收件箱...'); + + if (!isConfigured()) { + console.log('[Notion] △ Token 未配置,降级到本地存储'); + return saveFallback('syslog', data); + } + + if (!DB_IDS.syslog) { + console.log('[Notion] △ SYSLOG 数据库 ID 未配置'); + return saveFallback('syslog', data); + } + + var properties = { + '标题': titleProp(data.title || data.session_id || 'SYSLOG-' + Date.now()) + }; + + if (data.dev_id) { + properties['DEV编号'] = selectProp(data.dev_id); + } + + if (data.broadcast_id || data.session_id) { + properties['广播编号'] = richText(data.broadcast_id || data.session_id); + } + + if (data.module) { + properties['模块'] = richText(data.module); + } + + if (data.phase_status || data.status) { + var statusVal = data.phase_status || data.status; + if (['completed', 'partial', 'blocked'].includes(statusVal)) { + properties['环节状态'] = selectProp(statusVal); + } + } + + if (data.content || data.raw_text) { + properties['文件内容'] = richText(data.content || data.raw_text); + } + + properties['协议版本'] = selectProp(data.protocol_version || 'SYSLOG-v4.0'); + properties['处理状态'] = statusProp('待处理'); + properties['接收时间'] = dateProp(data.timestamp || new Date().toISOString()); + properties['推送方'] = richText(data.source || 'M-DINGTALK-钉钉工作台'); + + if (data.commit_sha) { + properties['commit_sha'] = richText(data.commit_sha); + } + + if (data.source_path) { + properties['来源路径'] = richText(data.source_path); + } + + try { + var response = await axios.post(NOTION_API + '/pages', { + parent: { database_id: DB_IDS.syslog }, + properties: properties + }, { + headers: getHeaders(), + timeout: 15000 + }); + + console.log('[Notion] ✅ SYSLOG 写入成功! 页面ID:', response.data.id); + saveWritebackLog({ + timestamp: new Date().toISOString(), + target: 'syslog', + page_id: response.data.id, + title: data.title || data.session_id, + status: 'success' + }); + + return { + success: true, + written_to: 'notion', + page_id: response.data.id, + url: response.data.url + }; + } catch (err) { + var errMsg = err.response ? JSON.stringify(err.response.data) : err.message; + console.error('[Notion] ❌ SYSLOG 写入失败:', errMsg); + saveWritebackLog({ + timestamp: new Date().toISOString(), + target: 'syslog', + title: data.title || data.session_id, + status: 'failed', + error: errMsg + }); + return saveFallback('syslog', data); + } +} + +// ====== 写入信号日志 ====== +async function writeSignalLog(data) { + console.log('[Notion] 准备写入信号日志...'); + if (!isConfigured() || !DB_IDS.signalLog) { + return saveFallback('signal-log', data); + } + + var properties = { + '信号编号': titleProp(data.signal_id || 'SIG-' + Date.now()), + '信号类型': selectProp(data.signal_type || 'GL-DATA'), + '关联DEV': selectProp(data.dev_id), + '发送方': selectProp(data.sender || '霜砚'), + '接收方': selectProp(data.receiver), + '摘要': richText(data.summary), + '方向': selectProp(data.direction), + '执行结果': statusProp(data.result || '已发送'), + '时间戳': dateProp(data.timestamp || new Date().toISOString()) + }; + + Object.keys(properties).forEach(function(k) { + if (properties[k] === undefined) delete properties[k]; + }); + + try { + var response = await axios.post(NOTION_API + '/pages', { + parent: { database_id: DB_IDS.signalLog }, + properties: properties + }, { + headers: getHeaders(), + timeout: 15000 + }); + + console.log('[Notion] ✅ 信号日志写入成功!'); + return { success: true, written_to: 'notion', page_id: response.data.id }; + } catch (err) { + console.error('[Notion] ❌ 信号日志写入失败:', err.message); + return saveFallback('signal-log', data); + } +} + +// ====== 健康检查:测试 Token 是否有效 ====== +async function healthCheck() { + if (!isConfigured()) { + return { online: false, reason: 'token_not_configured' }; + } + + try { + var response = await axios.get(NOTION_API + '/users/me', { + headers: getHeaders(), + timeout: 5000 + }); + + return { + online: true, + bot_name: response.data.name, + bot_id: response.data.id + }; + } catch (err) { + return { + online: false, + reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message + }; + } +} + +// ====== 数据库连通测试 ====== +async function testDatabase(dbName) { + var dbId = DB_IDS[dbName]; + if (!dbId) { + return { accessible: false, reason: 'db_id_not_configured', db: dbName }; + } + + try { + var response = await axios.post(NOTION_API + '/databases/' + dbId + '/query', { + page_size: 1 + }, { + headers: getHeaders(), + timeout: 10000 + }); + + return { + accessible: true, + db: dbName, + db_id: dbId, + total_results: response.data.results.length, + has_more: response.data.has_more + }; + } catch (err) { + return { + accessible: false, + db: dbName, + reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message + }; + } +} + +// ====== 降级:保存到本地 ====== +function saveFallback(type, data) { + if (!fs.existsSync(FALLBACK_DIR)) { + fs.mkdirSync(FALLBACK_DIR, { recursive: true }); + } + + var filename = type + '_' + Date.now() + '.json'; + var filePath = path.join(FALLBACK_DIR, filename); + + var record = { + type: type, + data: data, + saved_at: new Date().toISOString(), + reason: 'notion_unavailable' + }; + + fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8'); + console.log('[Notion] 已降级存储到本地:', filename); + + return { + success: true, + written_to: 'local', + file: filename, + message: 'Notion不可用,已存到本地。Token配置后可重新推送。' + }; +} + +// ====== 重推本地暂存数据到 Notion ====== +async function flushPending() { + if (!fs.existsSync(FALLBACK_DIR)) { + console.log('[Notion] 无暂存数据'); + return { flushed: 0 }; + } + + var files = fs.readdirSync(FALLBACK_DIR).filter(function(f) { return f.endsWith('.json'); }); + console.log('[Notion] 发现 ' + files.length + ' 条暂存数据,开始重推...'); + + var success = 0; + var failed = 0; + + for (var file of files) { + try { + var content = JSON.parse(fs.readFileSync(path.join(FALLBACK_DIR, file), 'utf-8')); + var result; + + if (content.type === 'syslog') { + result = await writeSyslog(content.data); + } else if (content.type === 'signal-log') { + result = await writeSignalLog(content.data); + } + + if (result && result.written_to === 'notion') { + fs.unlinkSync(path.join(FALLBACK_DIR, file)); + success++; + } else { + failed++; + } + } catch (err) { + console.error('[Notion] 重推失败:', file, err.message); + failed++; + } + } + + console.log('[Notion] 重推完成: 成功 ' + success + ' / 失败 ' + failed); + return { flushed: success, failed: failed, remaining: files.length - success }; +} + +// ====== 回写进度(兼容 Phase 3 sync-engine 调用) ====== +async function updateProgress(syslogData) { + console.log('[Notion Sync] 回写进度到 Notion...'); + + var result = await writeSyslog({ + title: (syslogData.session_id || '') + ' ' + (syslogData.dev_name || syslogData.dev_id || '') + ' ' + (syslogData.status || ''), + session_id: syslogData.session_id, + dev_id: syslogData.dev_id, + module: syslogData.module, + phase_status: syslogData.status || syslogData.completion_status, + content: JSON.stringify(syslogData, null, 2), + timestamp: syslogData.timestamp || new Date().toISOString(), + source: 'M-DINGTALK-钉钉工作台' + }); + + return result; +} + +// ====== 全量同步(兼容 Phase 3 sync-engine 调用) ====== +async function sync() { + console.log('[Notion Sync] 执行 Notion 同步...'); + var health = await healthCheck(); + + if (health.online) { + var flushResult = await flushPending(); + return { + status: 'online', + bot: health.bot_name, + flushed: flushResult.flushed, + remaining: flushResult.remaining || 0 + }; + } else { + return { + status: 'offline', + reason: health.reason, + message: '等待 Token 配置或网络恢复' + }; + } +} + +// ====== 保存回写日志 ====== +function saveWritebackLog(record) { + var logDir = path.dirname(WRITEBACK_LOG); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + var log = []; + if (fs.existsSync(WRITEBACK_LOG)) { + try { log = JSON.parse(fs.readFileSync(WRITEBACK_LOG, 'utf-8')); } catch (e) {} + } + + log.unshift(record); + if (log.length > 200) log = log.slice(0, 200); + fs.writeFileSync(WRITEBACK_LOG, JSON.stringify(log, null, 2), 'utf-8'); +} + +module.exports = { + writeSyslog, + writeSignalLog, + healthCheck, + testDatabase, + updateProgress, + sync, + flushPending, + isConfigured +}; diff --git a/syslog-parser.js b/syslog-parser.js new file mode 100644 index 00000000..40bd7152 --- /dev/null +++ b/syslog-parser.js @@ -0,0 +1,74 @@ +/** + * syslog-parser.js + * SYSLOG格式识别 + 关键字段提取 + * 秋秋说:这个文件就像翻译官,把开发者发的日志翻译成电脑能懂的数据 + */ + +function parseSyslog(message) { + // 定义返回结构 + const result = { + isSyslog: false, + bcNumber: null, + devId: null, + phaseNum: null, + status: null, // 'completed', 'partial', 'blocked' + summary: null, + rawMessage: message, + error: null + }; + + try { + // 检查是否包含SYSLOG关键词 + if (!message.includes('SYSLOG')) { + result.error = '不是SYSLOG格式(缺少SYSLOG关键词)'; + return result; + } + + // 提取BC编号(格式:BC-xxx-xxx) + const bcMatch = message.match(/BC-([A-Z0-9]+)-([A-Z0-9]+)/i); + if (bcMatch) { + result.bcNumber = `BC-${bcMatch[1]}-${bcMatch[2]}`; + } + + // 提取DEV编号(格式:DEV-xxx) + const devMatch = message.match(/DEV-([0-9]+)/i); + if (devMatch) { + result.devId = `DEV-${devMatch[1]}`; + } + + // 提取环节号(格式:环节X 或 phase X) + const phaseMatch = message.match(/环节\s*([0-9]+)/) || message.match(/phase\s*([0-9]+)/i); + if (phaseMatch) { + result.phaseNum = parseInt(phaseMatch[1], 10); + } + + // 提取完成状态 + if (message.includes('completed')) { + result.status = 'completed'; + } else if (message.includes('partial')) { + result.status = 'partial'; + } else if (message.includes('blocked')) { + result.status = 'blocked'; + } + + // 提取技术摘要(如果有) + const summaryMatch = message.match(/摘要[::]\s*(.+)/) || message.match(/summary[::]\s*(.+)/i); + if (summaryMatch) { + result.summary = summaryMatch[1].trim(); + } + + // 判断是否为完整的SYSLOG(至少要有BC和DEV) + if (result.bcNumber && result.devId && result.status) { + result.isSyslog = true; + } else { + result.error = 'SYSLOG格式不完整(缺少BC/DEV/状态中的必要字段)'; + } + + } catch (error) { + result.error = `解析异常:${error.message}`; + } + + return result; +} + +module.exports = { parseSyslog }; \ No newline at end of file diff --git a/syslog-receiver.js b/syslog-receiver.js new file mode 100644 index 00000000..32eacb70 --- /dev/null +++ b/syslog-receiver.js @@ -0,0 +1,9 @@ +// syslog-receiver.js +// Phase 1 - SYSLOG接收器 + +function receive(data) { + console.log('[SYSLOG] 收到数据:', data); + return { status: 'received', data }; +} + +module.exports = { receive }; diff --git a/test-stream.js b/test-stream.js new file mode 100644 index 00000000..a04e9ccd --- /dev/null +++ b/test-stream.js @@ -0,0 +1,15 @@ +require('dotenv').config(); +const { DWClient, TOPIC_ROBOT, EventAck } = require('dingtalk-stream'); + +const client = new DWClient({ + clientId: process.env.DINGTALK_APP_KEY, + clientSecret: process.env.DINGTALK_APP_SECRET, +}); + +client.registerCallbackListener(TOPIC_ROBOT, async (res) => { + console.log('[DEBUG] 触发!res =', JSON.stringify(res, null, 2)); + return EventAck.SUCCESS; +}); + +client.connect(); +console.log('Stream 监听中,请 @机器人 发消息...'); diff --git a/webhook/github-webhook.js b/webhook/github-webhook.js new file mode 100644 index 00000000..201bc393 --- /dev/null +++ b/webhook/github-webhook.js @@ -0,0 +1,153 @@ +// GitHub Webhook 接收器 · github-webhook.js +// HoloLake · M-DINGTALK Phase 3 +// DEV-004 之之 × 秋秋 +// + +const crypto = require('crypto'); + +// Webhook密钥(后续从GitHub Settings配置,现在用模拟值) +const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || 'hololake-webhook-secret-2026'; + +// 事件处理器注册表 +const eventHandlers = {}; + +/** + * 验证GitHub Webhook签名 + * GitHub发来的每个请求都有签名,防止伪造 + */ +function verifySignature(payload, signature) { + if (!signature) { + console.log('[Webhook] ⚠️ 请求没有签名,跳过验证(开发模式)'); + return true; // 开发模式下允许无签名 + } + + const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET); + const digest = 'sha256=' + hmac.update(payload).digest('hex'); + + try { + return crypto.timingSafeEqual( + Buffer.from(digest), + Buffer.from(signature) + ); + } catch (e) { + return false; + } +} + +/** + * 注册事件处理器 + * @param {string} eventType - GitHub事件类型 (push/pull_request等) + * @param {Function} handler - 处理函数 + */ +function on(eventType, handler) { + if (!eventHandlers[eventType]) { + eventHandlers[eventType] = []; + } + eventHandlers[eventType].push(handler); + console.log(`[Webhook] 注册事件处理器: ${eventType}`); +} + +/** + * 处理收到的Webhook请求 + * @param {Object} req - Express请求对象 + * @param {Object} res - Express响应对象 + */ +async function handleWebhook(req, res) { + const event = req.headers['x-github-event']; + const delivery = req.headers['x-github-delivery']; + const signature = req.headers['x-hub-signature-256']; + + console.log('\n[Webhook] ===================='); + console.log(`[Webhook] 收到GitHub事件:${event}`); + console.log(`[Webhook] Delivery ID: ${delivery}`); + console.log(`[Webhook] 时间:${new Date().toLocaleString('zh-CN')}`); + + // 验证签名 + const payload = JSON.stringify(req.body); + if (!verifySignature(payload, signature)) { + console.log('[Webhook] ❌ 签名验证失败!可能是伪造请求'); + return res.status(401).json({ error: '签名验证失败' }); + } + + // 记录事件日志 + const eventLog = { + event, + delivery, + timestamp: new Date().toISOString(), + sender: req.body.sender ? req.body.sender.login : 'unknown', + repository: req.body.repository ? req.body.repository.full_name : 'unknown' + }; + + // 特殊处理push事件 + if (event === 'push') { + eventLog.branch = req.body.ref; + eventLog.commits = (req.body.commits || []).length; + eventLog.changedFiles = []; + + // 收集所有变更文件 + for (const commit of (req.body.commits || [])) { + eventLog.changedFiles.push(...(commit.added || [])); + eventLog.changedFiles.push(...(commit.modified || [])); + } + eventLog.changedFiles = [...new Set(eventLog.changedFiles)]; + + console.log(`[Webhook] 分支:${eventLog.branch}`); + console.log(`[Webhook] 提交数:${eventLog.commits}`); + console.log(`[Webhook] 变更文件:${eventLog.changedFiles.join(', ')}`); + } + + // 调用注册的事件处理器 + const handlers = eventHandlers[event] || []; + console.log(`[Webhook] 匹配到 ${handlers.length} 个处理器`); + + for (const handler of handlers) { + try { + await handler(req.body, eventLog); + } catch (err) { + console.error('[Webhook] 处理器执行错误:', err.message); + } + } + + // 也调用通配符处理器 + const wildcardHandlers = eventHandlers['*'] || []; + for (const handler of wildcardHandlers) { + try { + await handler(req.body, eventLog); + } catch (err) { + console.error('[Webhook] 通配符处理器错误:', err.message); + } + } + + console.log('[Webhook] 事件处理完成 ✅'); + res.status(200).json({ status: 'ok', event, delivery }); +} + +/** + * 创建模拟的GitHub push事件(用于测试) + */ +function createMockPushEvent(branch, files) { + return { + ref: `refs/heads/${branch || 'main'}`, + repository: { + full_name: 'qinfendebingshuo/guanghulab' + }, + sender: { + login: 'test-user' + }, + commits: [ + { + message: '测试提交', + added: files || ['test-file.js'], + modified: [], + removed: [] + } + ] + }; +} + +module.exports = { + verifySignature, + on, + handleWebhook, + createMockPushEvent +}; diff --git a/webhook/hot-reload.js b/webhook/hot-reload.js new file mode 100644 index 00000000..63a89c0e --- /dev/null +++ b/webhook/hot-reload.js @@ -0,0 +1,114 @@ +// 规则热更新引擎·hot-reload.js +// HoloLake·M-DINGTALK Phase 3 +// DEV-004 之之×秋秋 +// + +const fs = require('fs'); +const path = require('path'); +const kb = require('../knowledge-base/kb-manager'); + +// 当前加载的规则(内存缓存) +let currentRules = {}; +let lastReloadTime = null; + +// 规则文件目录 +const RULES_DIR = path.join(__dirname, '..', 'config'); + +/** + * 加载所有规则文件到内存 + */ +function loadRules() { + console.log('[HotReload] 开始加载规则...'); + + const rulesFile = path.join(RULES_DIR, 'sync-config.json'); + + if (fs.existsSync(rulesFile)) { + try { + const content = fs.readFileSync(rulesFile, 'utf-8'); + currentRules = JSON.parse(content); + lastReloadTime = new Date().toISOString(); + console.log(`[HotReload] 规则加载成功 · ${Object.keys(currentRules).length} 个配置项`); + } catch (err) { + console.error('[HotReload] 规则加载失败:', err.message); + } + } else { + console.log('[HotReload] 规则文件不存在,使用默认配置'); + currentRules = { + version: '1.0', + sync_interval: 30, + timeout_hours: 72, + notion_db_id: 'mock-db-id' + }; + } + + return currentRules; +} + +/** + * 处理规则更新(由Webhook触发) + * @param {Object} payload - GitHub webhook payload + * @param {Object} log - 事件日志 + */ +async function handleRulesUpdate(payload, log) { + console.log('\n[HotReload] ===================='); + console.log('[HotReload] 收到规则更新通知'); + + const changedFiles = log.changedFiles || []; + + // 检查是否有规则相关文件变更 + const rulesChanged = changedFiles.some(f => + f.includes('rules/') || + f.includes('config/') || + f.includes('broadcast-engine/') + ); + + const docsChanged = changedFiles.some(f => + f.includes('knowledge-base/') || + f.includes('docs/') + ); + + if (rulesChanged) { + console.log('[HotReload] 检测到规则文件变更!执行热更新...'); + loadRules(); + console.log(`[HotReload] 规则热更新完成 · ${new Date().toLocaleString('zh-CN')}`); + } + + if (docsChanged) { + console.log('[HotReload] 检测到知识库文档变更!重新建立索引...'); + kb.reload(); + console.log(`[HotReload] 知识库索引更新完成 · ${new Date().toLocaleString('zh-CN')}`); + } + + if (!rulesChanged && !docsChanged) { + console.log('[HotReload] 变更文件与规则/知识库无关,跳过热更新'); + } +} + +/** + * 获取当前规则 + */ +function getRules() { + if (Object.keys(currentRules).length === 0) { + loadRules(); + } + return currentRules; +} + +/** + * 获取热更新状态 + */ +function getStatus() { + return { + rulesLoaded: Object.keys(currentRules).length > 0, + ruleKeys: Object.keys(currentRules), + lastReloadTime, + knowledgeBase: kb.getStats() + }; +} + +module.exports = { + loadRules, + handleRulesUpdate, + getRules, + getStatus +};