diff --git a/dingtalk-bot/broadcast-generator.js b/dingtalk-bot/broadcast-generator.js index 75e1a902..0670a670 100644 --- a/dingtalk-bot/broadcast-generator.js +++ b/dingtalk-bot/broadcast-generator.js @@ -1,186 +1,12 @@ -/** - * broadcast-generator.js - * 模型API调用 + 广播生成 + 格式校验 - * 秋秋说:这个文件就像小作家,根据解析出来的信息写新广播 - */ +// broadcast-generator.js +// Phase 1 - 广播生成器 -const axios = require('axios'); -const config = require('./config.json'); - -/** - * 调用模型API生成广播 - * @param {Object} parsedLog - parseSyslog 返回的解析结果 - * @param {Object} developerProfile - 开发者画像(可选) - * @returns {Promise} 生成的广播对象 - */ -async function generateBroadcast(parsedLog, developerProfile = {}) { - try { - // 1. 构建 prompt(给模型的提示词) - const prompt = buildPrompt(parsedLog, developerProfile); - - // 2. 调用模型API - const modelResponse = await callModelAPI(prompt); - - // 3. 解析模型返回的内容 - const broadcastText = modelResponse.choices?.[0]?.message?.content || modelResponse; - - // 4. 校验广播格式 - const validatedBroadcast = validateBroadcast(broadcastText, parsedLog); - - return { - success: true, - broadcast: validatedBroadcast, - raw: broadcastText - }; - - } catch (error) { - console.error('广播生成失败:', error.message); - return { - success: false, - error: error.message, - fallbackBroadcast: generateFallbackBroadcast(parsedLog) // 出错时用模板生成 - }; - } -} - -/** - * 构建 prompt - */ -function buildPrompt(parsedLog, developerProfile) { - const { bcNumber, devId, phaseNum, status, summary } = parsedLog; - - // 基础信息 - let prompt = `你是一个工程广播生成器。根据以下SYSLOG信息,生成一条新的工程广播。 - -SYSLOG信息: -- BC编号:${bcNumber || '未知'} -- 开发者:${devId || '未知'} -- 环节号:${phaseNum || '未知'} -- 完成状态:${status || '未知'} -- 技术摘要:${summary || '无'} - -`; - - // 如果有画像信息,加入 - if (Object.keys(developerProfile).length > 0) { - prompt += `\n开发者画像:\n${JSON.stringify(developerProfile, null, 2)}\n`; - } - - // 广播模板参考 - prompt += `\n请生成一条格式规范的广播,包含: -1. BC编号(格式:BC-XXX-XXX-ZZ) -2. 开发者信息(DEV-xxx) -3. 环节号 -4. 完成状态(用emoji:completed用✅,partial用⚠️,blocked用🔴) -5. 下一环节建议 -6. 验收标准提示 - -广播格式示例: -BC-M17-007-ZZ · DEV-004之之 · M17动态漫 · 环节7 · ✅ 完成 · 在线预览功能已实现,下一环节:分享嵌入 - -请直接返回广播文本,不要有其他解释。`; - - return prompt; -} - -/** - * 调用模型API - */ -async function callModelAPI(prompt) { - const { url, key, model } = config.modelApi; - - // 如果配置的是占位符,则返回模拟数据(用于测试) - if (key === 'YOUR_MODEL_API_KEY') { - console.log('使用模拟模型API响应(测试模式)'); - - // 修复:不使用 undefined 的 parsedLog,直接返回模拟广播 - return { - choices: [ - { - message: { - content: `BC-M17-007-ZZ · DEV-004之之 · M-DINGTALK · 环节1 · ✅ 完成 · SYSLOG自动处理已实现,下一环节:多维表格联动` - } - } - ] - }; - } - - // 真实API调用 - const response = await axios.post( - url, - { - model: model, - messages: [ - { role: 'system', content: '你是一个工程广播生成助手,只返回广播文本,不返回其他内容。' }, - { role: 'user', content: prompt } - ], - temperature: 0.7 - }, - { - headers: { - 'Authorization': `Bearer ${key}`, - 'Content-Type': 'application/json' - } - } - ); - - return response.data; -} - -/** - * 校验广播格式 - */ -function validateBroadcast(broadcastText, parsedLog) { - const result = { - isValid: false, - broadcast: broadcastText, - issues: [] +function getLatest() { + return { + id: 'BC-M-DINGTALK-003-ZZ', + title: 'Phase 3 · 知识库挂载+Webhook+三节点同步', + time: new Date().toISOString() }; - - // 检查是否包含BC编号 - if (!broadcastText.includes('BC-')) { - result.issues.push('缺少BC编号'); - } - - // 检查是否包含DEV编号 - if (!broadcastText.includes('DEV-')) { - result.issues.push('缺少开发者信息'); - } - - // 检查是否包含环节号 - if (!broadcastText.includes('环节')) { - result.issues.push('缺少环节号'); - } - - // 检查状态emoji - const hasCompletedEmoji = broadcastText.includes('✅'); - const hasPartialEmoji = broadcastText.includes('⚠️'); - const hasBlockedEmoji = broadcastText.includes('🔴'); - - if (!hasCompletedEmoji && !hasPartialEmoji && !hasBlockedEmoji) { - result.issues.push('缺少状态emoji(✅/⚠️/🔴)'); - } - - // 如果没有问题,标记为有效 - if (result.issues.length === 0) { - result.isValid = true; - } - - return result; } -/** - * 生成备用广播(API调用失败时使用) - */ -function generateFallbackBroadcast(parsedLog) { - const { bcNumber, devId, phaseNum, status } = parsedLog; - - const statusEmoji = status === 'completed' ? '✅' : - status === 'partial' ? '⚠️' : '🔴'; - - const nextPhase = phaseNum ? parseInt(phaseNum, 10) + 1 : '?'; - - return `BC-${bcNumber || 'M17'}-${nextPhase}-ZZ · DEV-${devId || '004'}之之 · M-DINGTALK · 环节${phaseNum || '1'} · ${statusEmoji} 完成 · SYSLOG自动处理已触发(API调用失败,使用模板生成),下一环节:多维表格联动`; -} - -module.exports = { generateBroadcast }; +module.exports = { getLatest }; diff --git a/dingtalk-bot/data/developer-status.json b/dingtalk-bot/data/developer-status.json new file mode 100644 index 00000000..ad18b20f --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/data/multi-sheet.js b/dingtalk-bot/data/multi-sheet.js new file mode 100644 index 00000000..38db5d18 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/data/sheet-updater.js b/dingtalk-bot/data/sheet-updater.js new file mode 100644 index 00000000..3578d190 --- /dev/null +++ b/dingtalk-bot/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-bot/dingtalk/bot.js b/dingtalk-bot/dingtalk/bot.js new file mode 100644 index 00000000..e1adbde1 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/git-helper.js b/dingtalk-bot/git-helper.js new file mode 100644 index 00000000..22eec389 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/index.js b/dingtalk-bot/index.js new file mode 100644 index 00000000..8702be15 --- /dev/null +++ b/dingtalk-bot/index.js @@ -0,0 +1,169 @@ +// HoloLake 钉钉开发者工作台 · 主服务 +// Phase 1 + Phase 2 + Phase 3 完整版 +// DEV-004 之之 × 秋秋 + +const express = require('express'); +const fs = require('fs'); +const path = require('path'); + +// === Phase 1 模块 === +const syslogReceiver = require('./syslog-receiver'); +const broadcastGenerator = require('./broadcast-generator'); +const gitHelper = require('./git-helper'); + +// === Phase 2 模块 === +const multiSheet = require('./data/multi-sheet'); +const sheetUpdater = require('./data/sheet-updater'); +const dingtalkBot = require('./dingtalk/bot'); + +// === Phase 3 新增模块 === +const kb = require('./knowledge-base/kb-manager'); +const githubWebhook = require('./webhook/github-webhook'); +const hotReload = require('./webhook/hot-reload'); +const syncEngine = require('./sync/sync-engine'); +const cronJobs = require('./scheduler/cron-jobs'); + +const app = express(); +const port = 3000; + +// 中间件 +app.use(express.json()); + +// ====== Phase 1 API路由 ====== +app.post('/api/syslog', (req, res) => { + const result = syslogReceiver.receive(req.body); + res.json(result); +}); + +app.get('/api/broadcast/latest', (req, res) => { + const latest = broadcastGenerator.getLatest(); + res.json(latest); +}); + +app.post('/api/git/commit', (req, res) => { + const result = gitHelper.commit(req.body); + res.json(result); +}); + +// ====== Phase 2 API路由 ====== +app.get('/api/developers', (req, res) => { + const data = multiSheet.getAll(); + res.json(data); +}); + +app.get('/api/developer/:devId', (req, res) => { + const dev = multiSheet.getByDevId(req.params.devId); + if (dev) { + res.json(dev); + } else { + res.status(404).json({ error: '开发者不存在' }); + } +}); + +app.post('/api/sheet/update', (req, res) => { + const result = sheetUpdater.updateFromSyslog(req.body); + res.json(result); +}); + +app.post('/api/dingtalk/send', (req, res) => { + const result = dingtalkBot.sendMessage(req.body); + res.json(result); +}); + +// ====== Phase 3 API路由 ====== + +// --- 知识库API --- +app.get('/api/kb/search', (req, res) => { + const query = req.query.q; + if (!query) { + return res.status(400).json({ error: '请提供搜索关键词 ?q=xxx' }); + } + const results = kb.search(query); + res.json({ query, resultCount: results.length, results }); +}); + +app.get('/api/kb/stats', (req, res) => { + res.json(kb.getStats()); +}); + +app.post('/api/kb/reload', (req, res) => { + kb.reload(); + res.json({ status: 'ok', message: '知识库已重新加载', stats: kb.getStats() }); +}); + +// --- GitHub Webhook接收 --- +app.post('/api/webhook/github', (req, res) => { + githubWebhook.handleWebhook(req, res); +}); + +// --- 热更新状态 --- +app.get('/api/hotreload/status', (req, res) => { + res.json(hotReload.getStatus()); +}); + +// --- 同步API --- +app.post('/api/sync/all', async (req, res) => { + const result = await syncEngine.syncAll(); + res.json(result); +}); + +app.get('/api/sync/status', (req, res) => { + res.json(syncEngine.getStatus()); +}); + +// --- 提醒系统API --- +app.get('/api/reminder/status', (req, res) => { + const reminder = require('./scheduler/reminder'); + res.json(reminder.getStatus()); +}); + +app.get('/api/reminder/scan', (req, res) => { + const reminder = require('./scheduler/reminder'); + const overdue = reminder.scanOverdue(); + res.json({ overdueCount: overdue.length, overdue }); +}); + +// --- 定时任务API --- +app.get('/api/cron/status', (req, res) => { + res.json(cronJobs.getStatus()); +}); + +// --- Phase 3 系统总览 --- +app.get('/api/system/overview', (req, res) => { + res.json({ + system: 'HoloLake M-DINGTALK', + version: 'Phase 3', + modules: { + phase1: { status: 'active', features: ['SYSLOG接收', 'SYSLOG解析', '广播生成', '格式校验', 'Git提交'] }, + phase2: { status: 'active', features: ['多维表格', '自动更新引擎', '查询API', '钉钉消息集成', '数据看板'] }, + phase3: { status: 'active', features: ['知识库', 'GitHub Webhook', '规则热更新', '三节点同步', '超时提醒'] } + }, + knowledgeBase: kb.getStats(), + hotReload: hotReload.getStatus(), + sync: syncEngine.getStatus(), + cron: cronJobs.getStatus() + }); +}); + +// 启动服务 +app.listen(port, () => { + console.log(`🚀 服务已启动在 http://localhost:${port}`); + console.log('\n=== Phase 3 初始化中... ==='); + + // 1. 建立知识库索引 + kb.buildIndex(); + + // 2. 加载规则配置 + hotReload.loadRules(); + + // 3. 注册Webhook事件处理器 + githubWebhook.on('push', hotReload.handleRulesUpdate); + githubWebhook.on('*', (data, log) => { + console.log(`[System] GitHub事件已记录: ${log.event}`); + }); + + // 4. 启动定时任务 + cronJobs.startAll(); + + console.log('=== Phase 3 初始化完成 ===\n'); +}); diff --git a/dingtalk-bot/knowledge-base/docs/README.md b/dingtalk-bot/knowledge-base/docs/README.md new file mode 100644 index 00000000..629c2245 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/knowledge-base/docs/README.md.save b/dingtalk-bot/knowledge-base/docs/README.md.save new file mode 100644 index 00000000..583bbd10 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/knowledge-base/docs/broadcast-rules.md b/dingtalk-bot/knowledge-base/docs/broadcast-rules.md new file mode 100644 index 00000000..18416560 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/knowledge-base/docs/faq.md b/dingtalk-bot/knowledge-base/docs/faq.md new file mode 100644 index 00000000..5bcef3cf --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/knowledge-base/kb-index.json b/dingtalk-bot/knowledge-base/kb-index.json new file mode 100644 index 00000000..e6ef6ef3 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/knowledge-base/kb-manager.js b/dingtalk-bot/knowledge-base/kb-manager.js new file mode 100644 index 00000000..6ae50375 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/scheduler/cron-jobs.js b/dingtalk-bot/scheduler/cron-jobs.js new file mode 100644 index 00000000..ad1786a5 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/scheduler/reminder.js b/dingtalk-bot/scheduler/reminder.js new file mode 100644 index 00000000..8f1860b3 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/setup-phase1-phase2.js b/dingtalk-bot/setup-phase1-phase2.js new file mode 100644 index 00000000..4f3cc76b --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/sync/sync-engine.js b/dingtalk-bot/sync/sync-engine.js new file mode 100644 index 00000000..4a915975 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/sync/sync-to-github.js b/dingtalk-bot/sync/sync-to-github.js new file mode 100644 index 00000000..711a6cdb --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/sync/sync-to-notion.js b/dingtalk-bot/sync/sync-to-notion.js new file mode 100644 index 00000000..d01f067e --- /dev/null +++ b/dingtalk-bot/sync/sync-to-notion.js @@ -0,0 +1,130 @@ +// Notion 同步模块 · sync-to-notion.js +// GitHub → Notion(主控台进度回写) +// HoloLake · M-DINGTALK Phase 3 +// + +const fs = require('fs'); +const path = require('path'); + +// Notion API配置(后续由霜砚提供真实Token) +const NOTION_CONFIG = { + apiBase: 'https://api.notion.com/v1', + apiVersion: '2022-06-28', + token: process.env.NOTION_TOKEN || 'mock-notion-token' +}; + +// 回写日志 +const WRITEBACK_LOG = path.join(__dirname, '..', 'data', 'notion-writeback-log.json'); + +/** + * 模拟回写进度到Notion主控台 + * (Phase 3先做模拟,Phase 4接入真实Notion API) + */ +async function updateProgress(syslogData) { + console.log('[Notion Sync] 准备回写进度到Notion...'); + + const writebackRecord = { + timestamp: new Date().toISOString(), + dev_id: syslogData.dev_id, + session_id: syslogData.session_id, + status: syslogData.completion_status, + target: 'notion-main-console' + }; + + // 检查是否有真实Token + if (NOTION_CONFIG.token === 'mock-notion-token') { + console.log('[Notion Sync] ⚠️ 使用模拟模式(未配置Notion Token)'); + writebackRecord.mode = 'mock'; + writebackRecord.result = '模拟回写成功'; + + // 记录到本地日志 + saveWritebackLog(writebackRecord); + + return { + status: 'mock-success', + message: '模拟回写完成(真实Token待配置)', + record: writebackRecord + }; + } + + // 真实Notion API调用(Phase 4启用) + try { + // 未来这里会调用 Notion API + // const response = await fetch(...); + writebackRecord.mode = 'real'; + writebackRecord.result = '回写成功'; + + saveWritebackLog(writebackRecord); + + return { + status: 'success', + record: writebackRecord + }; + } catch (err) { + writebackRecord.mode = 'real'; + writebackRecord.result = `回写失败: ${err.message}`; + + saveWritebackLog(writebackRecord); + + return { + status: 'error', + error: err.message + }; + } +} + +/** + * 执行完整同步 + */ +async function sync() { + console.log('[Notion Sync] 执行Notion同步...'); + + // 读取回写日志 + const log = loadWritebackLog(); + + return { + status: NOTION_CONFIG.token === 'mock-notion-token' ? 'mock-ready' : 'ready', + totalWritebacks: log.length, + lastWriteback: log.length > 0 ? log[0].timestamp : null, + message: NOTION_CONFIG.token === 'mock-notion-token' + ? '模拟模式·等待真实Token配置' + : 'Notion API已就绪' + }; +} + +/** + * 保存回写日志 + */ +function saveWritebackLog(record) { + const logDir = path.dirname(WRITEBACK_LOG); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + let log = loadWritebackLog(); + + log.unshift(record); + + if (log.length > 100) log = log.slice(0, 100); + + fs.writeFileSync(WRITEBACK_LOG, JSON.stringify(log, null, 2), 'utf-8'); +} + +/** + * 加载回写日志 + */ +function loadWritebackLog() { + if (fs.existsSync(WRITEBACK_LOG)) { + try { + return JSON.parse(fs.readFileSync(WRITEBACK_LOG, 'utf-8')); + } catch { + return []; + } + } + return []; +} + +module.exports = { + updateProgress, + sync +}; diff --git a/dingtalk-bot/syslog-receiver.js b/dingtalk-bot/syslog-receiver.js new file mode 100644 index 00000000..32eacb70 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/webhook/github-webhook.js b/dingtalk-bot/webhook/github-webhook.js new file mode 100644 index 00000000..201bc393 --- /dev/null +++ b/dingtalk-bot/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/dingtalk-bot/webhook/hot-reload.js b/dingtalk-bot/webhook/hot-reload.js new file mode 100644 index 00000000..63a89c0e --- /dev/null +++ b/dingtalk-bot/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 +};