DEV-004: M-DINGTALK Phase3 - 知识库+Webhook+热更新+三节点同步+超时提醒
This commit is contained in:
parent
738f1d5fde
commit
c2f79e4df1
|
|
@ -1,186 +1,12 @@
|
|||
/**
|
||||
* broadcast-generator.js
|
||||
* 模型API调用 + 广播生成 + 格式校验
|
||||
* 秋秋说:这个文件就像小作家,根据解析出来的信息写新广播
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const config = require('./config.json');
|
||||
|
||||
/**
|
||||
* 调用模型API生成广播
|
||||
* @param {Object} parsedLog - parseSyslog 返回的解析结果
|
||||
* @param {Object} developerProfile - 开发者画像(可选)
|
||||
* @returns {Promise<Object>} 生成的广播对象
|
||||
*/
|
||||
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);
|
||||
// broadcast-generator.js
|
||||
// Phase 1 - 广播生成器
|
||||
|
||||
function getLatest() {
|
||||
return {
|
||||
success: true,
|
||||
broadcast: validatedBroadcast,
|
||||
raw: broadcastText
|
||||
id: 'BC-M-DINGTALK-003-ZZ',
|
||||
title: 'Phase 3 · 知识库挂载+Webhook+三节点同步',
|
||||
time: new Date().toISOString()
|
||||
};
|
||||
|
||||
} 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: []
|
||||
};
|
||||
|
||||
// 检查是否包含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 };
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// data/sheet-updater.js
|
||||
// Phase 2 - 表格更新器
|
||||
|
||||
function updateFromSyslog(syslogData) {
|
||||
console.log('[SheetUpdater] 更新表格:', syslogData);
|
||||
return { status: 'updated' };
|
||||
}
|
||||
|
||||
module.exports = { updateFromSyslog };
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// dingtalk/bot.js
|
||||
// Phase 2 - 钉钉机器人
|
||||
|
||||
function sendMessage(message) {
|
||||
console.log('[DingTalk] 发送消息:', message);
|
||||
return { status: 'sent' };
|
||||
}
|
||||
|
||||
module.exports = { sendMessage };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
@ -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 - 广播格式规范
|
||||
|
|
@ -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小时内必须开始执行,超时系统会自动提醒。
|
||||
|
|
@ -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:架构级/全链路
|
||||
|
|
@ -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小时内必须开始执行,超时系统会自动提醒。
|
||||
|
|
@ -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": [
|
||||
"广播签发后",
|
||||
"小时内必须开始执行",
|
||||
"超时系统会自动提醒"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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 了!');
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// syslog-receiver.js
|
||||
// Phase 1 - SYSLOG接收器
|
||||
|
||||
function receive(data) {
|
||||
console.log('[SYSLOG] 收到数据:', data);
|
||||
return { status: 'received', data };
|
||||
}
|
||||
|
||||
module.exports = { receive };
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
Loading…
Reference in New Issue