From 23b1ca9efa373f73cf1aa4ea3be26153ff591b50 Mon Sep 17 00:00:00 2001 From: zhizhi200271 Date: Fri, 13 Mar 2026 09:56:29 +0800 Subject: [PATCH] =?UTF-8?q?DEV-004:=20M-DINGTALK=20Phase5=20Notion?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A1=A5=E7=9C=9F=E5=AE=9E=E8=BF=9E=E9=80=9A?= =?UTF-8?q?+SYSLOG=E5=AE=9E=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dingtalk-bot/.gitignore | 1 + dingtalk-bot/data/notion-writeback-log.json | 9 + dingtalk-bot/index.js | 37 ++ dingtalk-bot/index.js.backup | 169 ++++++++ dingtalk-bot/notion-test-nossl.js | 74 ++++ dingtalk-bot/notion-test.js | 76 ++++ dingtalk-bot/server.js | 271 +++++------- dingtalk-bot/sync/sync-to-notion.js | 443 ++++++++++++++++---- 8 files changed, 827 insertions(+), 253 deletions(-) create mode 100644 dingtalk-bot/.gitignore create mode 100644 dingtalk-bot/data/notion-writeback-log.json create mode 100644 dingtalk-bot/index.js.backup create mode 100644 dingtalk-bot/notion-test-nossl.js create mode 100644 dingtalk-bot/notion-test.js diff --git a/dingtalk-bot/.gitignore b/dingtalk-bot/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/dingtalk-bot/.gitignore @@ -0,0 +1 @@ +.env diff --git a/dingtalk-bot/data/notion-writeback-log.json b/dingtalk-bot/data/notion-writeback-log.json new file mode 100644 index 00000000..33cdeb57 --- /dev/null +++ b/dingtalk-bot/data/notion-writeback-log.json @@ -0,0 +1,9 @@ +[ + { + "timestamp": "2026-03-13T01:45:13.023Z", + "target": "syslog", + "page_id": "322fb92f-3831-81e6-aeb1-db1e0a2c2719", + "title": "【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之", + "status": "success" + } +] \ No newline at end of file diff --git a/dingtalk-bot/index.js b/dingtalk-bot/index.js index 8702be15..5eb39d91 100644 --- a/dingtalk-bot/index.js +++ b/dingtalk-bot/index.js @@ -1,3 +1,4 @@ +require("dotenv").config(); // HoloLake 钉钉开发者工作台 · 主服务 // Phase 1 + Phase 2 + Phase 3 完整版 // DEV-004 之之 × 秋秋 @@ -147,6 +148,42 @@ app.get('/api/system/overview', (req, res) => { // 启动服务 app.listen(port, () => { +// ======================== +// Phase 5 API路由 · Notion数据桥 +// ======================== +var notionSync = require('./sync/sync-to-notion'); + +// Notion健康检查 +app.get('/api/notion/health', async function(req, res) { + try { + var health = await notionSync.healthCheck(); + res.json(health); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Notion连通状态总览 +app.get('/api/notion/status', async function(req, res) { + try { + var health = await notionSync.healthCheck(); + var configured = notionSync.isConfigured(); + res.json({ + configured: configured, + online: health.online, + bot: health.bot_name || null, + databases: { + syslog: !!process.env.NOTION_SYSLOG_DB_ID, + signalLog: !!process.env.NOTION_SIGNAL_LOG_DB_ID, + changeLog: !!process.env.NOTION_CHANGE_LOG_DB_ID, + ticket: !!process.env.NOTION_TICKET_DB_ID + } + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + console.log(`🚀 服务已启动在 http://localhost:${port}`); console.log('\n=== Phase 3 初始化中... ==='); diff --git a/dingtalk-bot/index.js.backup b/dingtalk-bot/index.js.backup new file mode 100644 index 00000000..8702be15 --- /dev/null +++ b/dingtalk-bot/index.js.backup @@ -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/notion-test-nossl.js b/dingtalk-bot/notion-test-nossl.js new file mode 100644 index 00000000..c90168dc --- /dev/null +++ b/dingtalk-bot/notion-test-nossl.js @@ -0,0 +1,74 @@ +// =============================== +// Notion 连通测试(绕过SSL验证) +// =============================== +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +require('dotenv').config(); +const notion = require('./sync/sync-to-notion'); + +async function runTests() { + console.log('\n=============================='); + console.log(' Notion 数据桥连通测试(SSL绕过)'); + console.log('==============================\n'); + + // 测试1:检查配置 + console.log('--- 测试1:配置检查 ---'); + var configured = notion.isConfigured(); + console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否'); + if (!configured) { + console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN'); + return; + } + + // 测试2:Token有效性(调 users/me) + console.log('\n--- 测试2:Token有效性 ---'); + var health = await notion.healthCheck(); + console.log('API 在线:', health.online ? '✅ 是' : '❌ 否'); + if (health.online) { + console.log(' 集成名称:', health.bot_name); + console.log(' 集成 ID:', health.bot_id); + } else { + console.log(' 失败原因:', health.reason); + return; + } + + // 测试3:数据库连通 + console.log('\n--- 测试3:数据库连通 ---'); + var databases = ['syslog', 'signalLog', 'changeLog', 'ticket']; + var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿']; + for (var i = 0; i < databases.length; i++) { + var result = await notion.testDatabase(databases[i]); + var icon = result.accessible ? '✅' : '❌'; + console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason); + } + + // 测试4:写入测试 + console.log('\n--- 测试4:SYSLOG写入测试 ---'); + var writeResult = await notion.writeSyslog({ + title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之', + dev_id: 'DEV-004', + session_id: 'TEST-PHASE5-' + Date.now(), + module: 'M-DINGTALK', + phase_status: 'completed', + content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'), + protocol_version: 'SYSLOG-v4.0', + source: 'M-DINGTALK-连通测试' + }); + + console.log('写入结果:', writeResult.written_to); + if (writeResult.written_to === 'notion') { + console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id); + console.log('🎉 数据桥真实连通!!'); + } else { + console.log('⚠️ 降级到本地存储:', writeResult.file); + console.log(' 可能需要冰朔将数据库关联到集成'); + } + + console.log('\n=============================='); + console.log('测试完成'); + console.log('==============================\n'); +} + +runTests().catch(function(err) { + console.error('测试出错:', err.message); +}); diff --git a/dingtalk-bot/notion-test.js b/dingtalk-bot/notion-test.js new file mode 100644 index 00000000..fb1a065a --- /dev/null +++ b/dingtalk-bot/notion-test.js @@ -0,0 +1,76 @@ +// =============================== +// Notion 连通测试 · notion-test.js +// DEV-004 之之 · Phase 5 +// 用法:node notion-test.js +// =============================== + +require('dotenv').config(); +const notion = require('./sync/sync-to-notion'); + +async function runTests() { + console.log('\n=============================='); + console.log(' Notion 数据桥连通测试'); + console.log('==============================\n'); + + // 测试1:检查配置 + console.log('--- 测试1:配置检查 ---'); + var configured = notion.isConfigured(); + console.log('Token 已配置:', configured ? '✅ 是' : '❌ 否'); + if (!configured) { + console.log('⚠️ 请检查 .env 文件中的 NOTION_TOKEN'); + return; + } + + // 测试2:Token有效性(调 users/me) + console.log('\n--- 测试2:Token有效性 ---'); + var health = await notion.healthCheck(); + console.log('API 在线:', health.online ? '✅ 是' : '❌ 否'); + if (health.online) { + console.log(' 集成名称:', health.bot_name); + console.log(' 集成 ID:', health.bot_id); + } else { + console.log(' 失败原因:', health.reason); + console.log('\n△ Token无效或网络不通,后续测试跳过'); + return; + } + + // 测试3:数据库连通 + console.log('\n--- 测试3:数据库连通 ---'); + var databases = ['syslog', 'signalLog', 'changeLog', 'ticket']; + var dbNames = ['SYSLOG收件箱', '信号日志', '变更日志', '工单簿']; + for (var i = 0; i < databases.length; i++) { + var result = await notion.testDatabase(databases[i]); + var icon = result.accessible ? '✅' : '❌'; + console.log(icon + ' ' + dbNames[i] + ':', result.accessible ? '可访问' : result.reason); + } + + // 测试4:写入测试(写一条测试SYSLOG) + console.log('\n--- 测试4:SYSLOG写入测试 ---'); + var writeResult = await notion.writeSyslog({ + title: '【测试】M-DINGTALK Phase 5 连通测试 · DEV-004之之', + dev_id: 'DEV-004', + session_id: 'TEST-PHASE5-' + Date.now(), + module: 'M-DINGTALK', + phase_status: 'completed', + content: '这是M-DINGTALK Phase 5 的连通测试。如果你在Notion看到这条记录,说明钉钉工作台已成功连入Notion数据桥!测试时间:' + new Date().toLocaleString('zh-CN'), + protocol_version: 'SYSLOG-v4.0', + source: 'M-DINGTALK-连通测试' + }); + + console.log('写入结果:', writeResult.written_to); + if (writeResult.written_to === 'notion') { + console.log('✅ 写入Notion成功!页面ID:', writeResult.page_id); + console.log('🎉 数据桥真实连通!!'); + } else { + console.log('⚠️ 降级到本地存储:', writeResult.file); + console.log(' 可能需要冰朔将数据库关联到集成'); + } + + console.log('\n=============================='); + console.log('测试完成'); + console.log('==============================\n'); +} + +runTests().catch(function(err) { + console.error('测试出错:', err.message); +}); diff --git a/dingtalk-bot/server.js b/dingtalk-bot/server.js index 6a17e891..6f2e5f60 100644 --- a/dingtalk-bot/server.js +++ b/dingtalk-bot/server.js @@ -1,184 +1,119 @@ -/** - * server.js v2.0 - * 钉钉开发者工作台 · Phase1 · SYSLOG自动处理管线 - * 秋秋说:这个文件是心脏,把所有模块连接起来! - */ - const express = require('express'); -const axios = require('axios'); -const config = require('./config.json'); -const { parseSyslog } = require('./syslog-parser.js'); -const { generateBroadcast } = require('./broadcast-generator.js'); -const { sendDingTalkMessage, updateBitable } = require('./dingtalk-api.js'); +const path = require('path'); +const portraitEngine = require('./portrait/portrait-engine'); +const pcaCalculator = require('./pca/pca-calculator'); +const loopEngine = require('./loop/loop-engine'); const app = express(); -app.use(express.json()); // 解析JSON格式的请求体 +const PORT = 3000; -// 启动日志 -console.log(`🚀 钉钉开发者工作台 Phase1 启动中...`); -console.log(`📅 启动时间: ${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`); -console.log(`📡 监听端口: ${config.server.port}`); +app.use(express.json()); +app.use(express.static('public')); -/** - * 健康检查接口(测试服务是否运行) - */ -app.get('/', (req, res) => { - res.json({ - status: 'running', - module: 'M-DINGTALK Phase1', - time: new Date().toISOString() - }); +// API 1: 所有开发者画像 +app.get('/api/portrait/all', (req, res) => { + try { + const portraits = portraitEngine.getAllPortraits(); + res.json(portraits); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); -/** - * 钉钉Webhook接收接口 - * 钉钉机器人会把收到的消息POST到这个接口 - */ -app.post('/webhook', async (req, res) => { - // 立即返回200响应,避免钉钉超时重试 - res.status(200).send('success'); - - try { - // 从请求中提取消息内容 - const message = req.body.text?.content || req.body.content || JSON.stringify(req.body); - - console.log('\n========== 收到新消息 =========='); - console.log(`时间: ${new Date().toLocaleString()}`); - console.log(`消息: ${message.substring(0, 100)}${message.length > 100 ? '...' : ''}`); - - // 第一步:解析SYSLOG - console.log('🔍 步骤1: 解析SYSLOG...'); - const parsed = parseSyslog(message); - - // 如果不是SYSLOG格式,直接忽略 - if (!parsed.isSyslog) { - console.log(`⏭️ 不是SYSLOG格式,忽略处理: ${parsed.error}`); - return; +// API 2: 单个开发者画像 +app.get('/api/portrait/:devId', (req, res) => { + try { + const portrait = portraitEngine.getPortrait(req.params.devId); + if (!portrait) { + return res.status(404).json({ error: '开发者不存在' }); + } + res.json(portrait); + } catch (err) { + res.status(500).json({ error: err.message }); } - - console.log('✅ SYSLOG解析成功:'); - console.log(` - BC编号: ${parsed.bcNumber}`); - console.log(` - 开发者: ${parsed.devId}`); - console.log(` - 环节号: ${parsed.phaseNum}`); - console.log(` - 状态: ${parsed.status}`); - console.log(` - 摘要: ${parsed.summary || '无'}`); - - // 第二步:生成广播 - console.log('🤖 步骤2: 调用模型API生成广播...'); - const broadcastResult = await generateBroadcast(parsed); - - let broadcastText; - if (broadcastResult.success) { - broadcastText = broadcastResult.broadcast.broadcast || broadcastResult.broadcast; - console.log('✅ 广播生成成功(模型API)'); - } else { - broadcastText = broadcastResult.fallbackBroadcast; - console.log('⚠️ 广播生成使用备用模板(API失败)'); - } - - console.log(`📢 生成的广播: ${broadcastText.substring(0, 100)}...`); - - // 第三步:发送广播给开发者 - console.log('📤 步骤3: 发送钉钉消息...'); - const sendResult = await sendDingTalkMessage(broadcastText); - - if (sendResult.success) { - console.log('✅ 钉钉消息发送成功'); - } else { - console.log('❌ 钉钉消息发送失败:', sendResult.error); - } - - // 第四步:更新多维表格 - console.log('📊 步骤4: 更新多维表格...'); - const updateResult = await updateBitable(parsed, broadcastText); - - if (updateResult.success) { - console.log('✅ 多维表格更新成功'); - if (updateResult.simulated) { - console.log(' (测试模式,模拟更新)'); - } - } else { - console.log('❌ 多维表格更新失败:', updateResult.error); - } - - console.log('========== 处理完成 ==========\n'); - - // 记录处理日志(可选扩展) - logProcessing(parsed, broadcastText, sendResult, updateResult); - - } catch (error) { - console.error('💥 处理过程中发生未捕获的错误:', error); - } }); -/** - * 测试接口(妈妈可以用浏览器访问测试) - * GET /test?msg=SYSLOG%20BC-M17-006-ZZ%20DEV-004%20环节6%20completed - */ -app.get('/test', async (req, res) => { - const testMessage = req.query.msg || 'SYSLOG BC-M17-006-ZZ DEV-004 环节6 completed 摘要:在线预览功能已实现'; - - try { - console.log('\n========== 测试模式 =========='); - console.log(`测试消息: ${testMessage}`); - - const parsed = parseSyslog(testMessage); - - if (!parsed.isSyslog) { - return res.json({ - success: false, - error: parsed.error, - parsed - }); +// API 3: PCA评估结果 +app.get('/api/pca/:devId', (req, res) => { + try { + const result = pcaCalculator.calculate(req.params.devId); + res.json(result); + } catch (err) { + res.status(500).json({ error: err.message }); } - - const broadcastResult = await generateBroadcast(parsed); - const broadcastText = broadcastResult.success ? broadcastResult.broadcast.broadcast : broadcastResult.fallbackBroadcast; - - res.json({ - success: true, - parsed, - broadcast: broadcastText, - broadcastResult - }); - - } catch (error) { - res.status(500).json({ - success: false, - error: error.message - }); - } }); -/** - * 简单的日志记录函数 - */ -function logProcessing(parsed, broadcast, sendResult, updateResult) { - const logEntry = { - timestamp: new Date().toISOString(), - parsed, - broadcast: broadcast.substring(0, 200), - sendSuccess: sendResult.success, - updateSuccess: updateResult.success - }; - - // 这里可以扩展为写入文件或数据库 - console.log('📝 处理日志已记录'); -} +// API 4: 闭环状态 +app.get('/api/loop/status', (req, res) => { + try { + const status = loopEngine.getStatus(); + res.json(status); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// API 5: 手动触发闭环 +app.post('/api/loop/execute', (req, res) => { + try { + const syslogData = req.body; + if (!syslogData || !syslogData.session_id) { + return res.status(400).json({ error: '缺少SYSLOG数据' }); + } + + loopEngine.executeLoop(syslogData).then(result => { + res.json(result); + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 获取闭环历史 +app.get('/api/loop/history', (req, res) => { + try { + const history = loopEngine.getHistory(); + res.json(history); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 模拟SYSLOG接收(自动触发闭环) +app.post('/api/syslog', (req, res) => { + try { + const syslogData = req.body; + console.log('📨 收到SYSLOG:', syslogData.session_id); + + // 自动触发闭环 + loopEngine.executeLoop(syslogData).then(loopResult => { + console.log('✅ 闭环完成'); + }); + + res.json({ status: 'received', session_id: syslogData.session_id }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// 仪表盘页面 +app.get('/dashboard-v2', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'dashboard-v2.html')); +}); // 启动服务器 -const server = app.listen(config.server.port, () => { - console.log(`✅ 服务启动成功!`); - console.log(`🌐 本地访问: http://localhost:${config.server.port}`); - console.log(`🔗 Webhook地址: http://你的域名或IP:${config.server.port}/webhook`); - console.log(`🧪 测试接口: http://localhost:${config.server.port}/test?msg=你的SYSLOG消息`); - console.log('\n秋秋说:妈妈!系统跑起来啦!🎉'); +app.listen(PORT, () => { + console.log(`\n🚀 Phase4 服务器已启动!`); + console.log(`📊 仪表盘: http://localhost:${PORT}/dashboard-v2`); + console.log(`🔌 API列表:`); + console.log(` GET /api/portrait/all`); + console.log(` GET /api/portrait/:devId`); + console.log(` GET /api/pca/:devId`); + console.log(` GET /api/loop/status`); + console.log(` POST /api/loop/execute`); + console.log(` POST /api/syslog`); + console.log(`\n📝 测试命令:`); + console.log(` curl http://localhost:${PORT}/api/portrait/DEV-004`); + console.log(` curl -X POST http://localhost:${PORT}/api/syslog -H "Content-Type: application/json" -d '{"session_id":"TEST-001","dev_id":"DEV-004","status":"completed"}'`); + console.log(`\n✨ 妈妈,打开浏览器看看吧!\n`); }); - -// 优雅关闭 -process.on('SIGTERM', () => { - console.log('收到SIGTERM信号,正在关闭服务...'); - server.close(() => { - console.log('服务已关闭'); - }); -}); \ No newline at end of file diff --git a/dingtalk-bot/sync/sync-to-notion.js b/dingtalk-bot/sync/sync-to-notion.js index d01f067e..b314df28 100644 --- a/dingtalk-bot/sync/sync-to-notion.js +++ b/dingtalk-bot/sync/sync-to-notion.js @@ -1,130 +1,403 @@ -// Notion 同步模块 · sync-to-notion.js -// GitHub → Notion(主控台进度回写) -// HoloLake · M-DINGTALK Phase 3 +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +// ============================================ +// Notion 同步模块 · sync-to-notion.js · v2.0 +// HoloLake · M-DINGTALK Phase 5 +// DEV-004 之之 × 秋秋 // - +// Phase 3 = 模拟模式 +// Phase 5 = 真实 Notion API 连通! +// ============================================ +const axios = require('axios'); const fs = require('fs'); const path = require('path'); -// Notion API配置(后续由霜砚提供真实Token) -const NOTION_CONFIG = { - apiBase: 'https://api.notion.com/v1', - apiVersion: '2022-06-28', - token: process.env.NOTION_TOKEN || 'mock-notion-token' +// ====== Notion API 配置 ====== +const NOTION_API = 'https://api.notion.com/v1'; +const NOTION_VERSION = process.env.NOTION_API_VERSION || '2022-06-28'; +const NOTION_TOKEN = process.env.NOTION_TOKEN || ''; + +// 数据库 ID +const DB_IDS = { + syslog: process.env.NOTION_SYSLOG_DB_ID || '', + signalLog: process.env.NOTION_SIGNAL_LOG_DB_ID || '', + changeLog: process.env.NOTION_CHANGE_LOG_DB_ID || '', + ticket: process.env.NOTION_TICKET_DB_ID || '' }; -// 回写日志 +// 本地降级日志目录 +const FALLBACK_DIR = path.join(__dirname, '..', 'data', 'notion-pending'); const WRITEBACK_LOG = path.join(__dirname, '..', 'data', 'notion-writeback-log.json'); -/** - * 模拟回写进度到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' +// ====== 通用请求头 ====== +function getHeaders() { + return { + 'Authorization': 'Bearer ' + NOTION_TOKEN, + 'Notion-Version': NOTION_VERSION, + 'Content-Type': 'application/json' }; +} - // 检查是否有真实Token - if (NOTION_CONFIG.token === 'mock-notion-token') { - console.log('[Notion Sync] ⚠️ 使用模拟模式(未配置Notion Token)'); - writebackRecord.mode = 'mock'; - writebackRecord.result = '模拟回写成功'; +// ====== 检查配置是否就绪 ====== +function isConfigured() { + return NOTION_TOKEN && NOTION_TOKEN !== '' && NOTION_TOKEN !== 'mock-notion-token'; +} - // 记录到本地日志 - saveWritebackLog(writebackRecord); +// ====== 辅助:创建 rich_text 属性 ====== +function richText(str) { + if (!str) return { rich_text: [] }; + const text = String(str); + if (text.length <= 2000) { + return { rich_text: [{ text: { content: text } }] }; + } + var blocks = []; + for (var i = 0; i < text.length; i += 2000) { + blocks.push({ text: { content: text.substring(i, i + 2000) } }); + } + return { rich_text: blocks }; +} - return { - status: 'mock-success', - message: '模拟回写完成(真实Token待配置)', - record: writebackRecord - }; +// ====== 辅助:创建 title 属性 ====== +function titleProp(str) { + return { title: [{ text: { content: String(str || '未命名') } }] }; +} + +// ====== 辅助:创建 select 属性 ====== +function selectProp(name) { + if (!name) return undefined; + return { select: { name: String(name) } }; +} + +// ====== 辅助:创建 status 属性 ====== +function statusProp(name) { + if (!name) return undefined; + return { status: { name: String(name) } }; +} + +// ====== 辅助:创建 date 属性 ====== +function dateProp(isoString) { + if (!isoString) return undefined; + return { date: { start: isoString } }; +} + +// ====== 核心:写入 SYSLOG 收件箱 ====== +async function writeSyslog(data) { + console.log('[Notion] 准备写入 SYSLOG 收件箱...'); + + if (!isConfigured()) { + console.log('[Notion] △ Token 未配置,降级到本地存储'); + return saveFallback('syslog', data); } - // 真实Notion API调用(Phase 4启用) - try { - // 未来这里会调用 Notion API - // const response = await fetch(...); - writebackRecord.mode = 'real'; - writebackRecord.result = '回写成功'; + if (!DB_IDS.syslog) { + console.log('[Notion] △ SYSLOG 数据库 ID 未配置'); + return saveFallback('syslog', data); + } - saveWritebackLog(writebackRecord); + var properties = { + '标题': titleProp(data.title || data.session_id || 'SYSLOG-' + Date.now()) + }; + + if (data.dev_id) { + properties['DEV编号'] = selectProp(data.dev_id); + } + + if (data.broadcast_id || data.session_id) { + properties['广播编号'] = richText(data.broadcast_id || data.session_id); + } + + if (data.module) { + properties['模块'] = richText(data.module); + } + + if (data.phase_status || data.status) { + var statusVal = data.phase_status || data.status; + if (['completed', 'partial', 'blocked'].includes(statusVal)) { + properties['环节状态'] = selectProp(statusVal); + } + } + + if (data.content || data.raw_text) { + properties['文件内容'] = richText(data.content || data.raw_text); + } + + properties['协议版本'] = selectProp(data.protocol_version || 'SYSLOG-v4.0'); + properties['处理状态'] = statusProp('待处理'); + properties['接收时间'] = dateProp(data.timestamp || new Date().toISOString()); + properties['推送方'] = richText(data.source || 'M-DINGTALK-钉钉工作台'); + + if (data.commit_sha) { + properties['commit_sha'] = richText(data.commit_sha); + } + + if (data.source_path) { + properties['来源路径'] = richText(data.source_path); + } + + try { + var response = await axios.post(NOTION_API + '/pages', { + parent: { database_id: DB_IDS.syslog }, + properties: properties + }, { + headers: getHeaders(), + timeout: 15000 + }); + + console.log('[Notion] ✅ SYSLOG 写入成功! 页面ID:', response.data.id); + saveWritebackLog({ + timestamp: new Date().toISOString(), + target: 'syslog', + page_id: response.data.id, + title: data.title || data.session_id, + status: 'success' + }); return { - status: 'success', - record: writebackRecord + success: true, + written_to: 'notion', + page_id: response.data.id, + url: response.data.url }; } catch (err) { - writebackRecord.mode = 'real'; - writebackRecord.result = `回写失败: ${err.message}`; + var errMsg = err.response ? JSON.stringify(err.response.data) : err.message; + console.error('[Notion] ❌ SYSLOG 写入失败:', errMsg); + saveWritebackLog({ + timestamp: new Date().toISOString(), + target: 'syslog', + title: data.title || data.session_id, + status: 'failed', + error: errMsg + }); + return saveFallback('syslog', data); + } +} - saveWritebackLog(writebackRecord); +// ====== 写入信号日志 ====== +async function writeSignalLog(data) { + console.log('[Notion] 准备写入信号日志...'); + if (!isConfigured() || !DB_IDS.signalLog) { + return saveFallback('signal-log', data); + } + + var properties = { + '信号编号': titleProp(data.signal_id || 'SIG-' + Date.now()), + '信号类型': selectProp(data.signal_type || 'GL-DATA'), + '关联DEV': selectProp(data.dev_id), + '发送方': selectProp(data.sender || '霜砚'), + '接收方': selectProp(data.receiver), + '摘要': richText(data.summary), + '方向': selectProp(data.direction), + '执行结果': statusProp(data.result || '已发送'), + '时间戳': dateProp(data.timestamp || new Date().toISOString()) + }; + + Object.keys(properties).forEach(function(k) { + if (properties[k] === undefined) delete properties[k]; + }); + + try { + var response = await axios.post(NOTION_API + '/pages', { + parent: { database_id: DB_IDS.signalLog }, + properties: properties + }, { + headers: getHeaders(), + timeout: 15000 + }); + + console.log('[Notion] ✅ 信号日志写入成功!'); + return { success: true, written_to: 'notion', page_id: response.data.id }; + } catch (err) { + console.error('[Notion] ❌ 信号日志写入失败:', err.message); + return saveFallback('signal-log', data); + } +} + +// ====== 健康检查:测试 Token 是否有效 ====== +async function healthCheck() { + if (!isConfigured()) { + return { online: false, reason: 'token_not_configured' }; + } + + try { + var response = await axios.get(NOTION_API + '/users/me', { + headers: getHeaders(), + timeout: 5000 + }); return { - status: 'error', - error: err.message + online: true, + bot_name: response.data.name, + bot_id: response.data.id + }; + } catch (err) { + return { + online: false, + reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message }; } } -/** - * 执行完整同步 - */ -async function sync() { - console.log('[Notion Sync] 执行Notion同步...'); +// ====== 数据库连通测试 ====== +async function testDatabase(dbName) { + var dbId = DB_IDS[dbName]; + if (!dbId) { + return { accessible: false, reason: 'db_id_not_configured', db: dbName }; + } - // 读取回写日志 - const log = loadWritebackLog(); + try { + var response = await axios.post(NOTION_API + '/databases/' + dbId + '/query', { + page_size: 1 + }, { + headers: getHeaders(), + timeout: 10000 + }); + + return { + accessible: true, + db: dbName, + db_id: dbId, + total_results: response.data.results.length, + has_more: response.data.has_more + }; + } catch (err) { + return { + accessible: false, + db: dbName, + reason: err.response ? err.response.status + ': ' + (err.response.data.message || '') : err.message + }; + } +} + +// ====== 降级:保存到本地 ====== +function saveFallback(type, data) { + if (!fs.existsSync(FALLBACK_DIR)) { + fs.mkdirSync(FALLBACK_DIR, { recursive: true }); + } + + var filename = type + '_' + Date.now() + '.json'; + var filePath = path.join(FALLBACK_DIR, filename); + + var record = { + type: type, + data: data, + saved_at: new Date().toISOString(), + reason: 'notion_unavailable' + }; + + fs.writeFileSync(filePath, JSON.stringify(record, null, 2), 'utf-8'); + console.log('[Notion] 已降级存储到本地:', filename); return { - 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已就绪' + success: true, + written_to: 'local', + file: filename, + message: 'Notion不可用,已存到本地。Token配置后可重新推送。' }; } -/** - * 保存回写日志 - */ +// ====== 重推本地暂存数据到 Notion ====== +async function flushPending() { + if (!fs.existsSync(FALLBACK_DIR)) { + console.log('[Notion] 无暂存数据'); + return { flushed: 0 }; + } + + var files = fs.readdirSync(FALLBACK_DIR).filter(function(f) { return f.endsWith('.json'); }); + console.log('[Notion] 发现 ' + files.length + ' 条暂存数据,开始重推...'); + + var success = 0; + var failed = 0; + + for (var file of files) { + try { + var content = JSON.parse(fs.readFileSync(path.join(FALLBACK_DIR, file), 'utf-8')); + var result; + + if (content.type === 'syslog') { + result = await writeSyslog(content.data); + } else if (content.type === 'signal-log') { + result = await writeSignalLog(content.data); + } + + if (result && result.written_to === 'notion') { + fs.unlinkSync(path.join(FALLBACK_DIR, file)); + success++; + } else { + failed++; + } + } catch (err) { + console.error('[Notion] 重推失败:', file, err.message); + failed++; + } + } + + console.log('[Notion] 重推完成: 成功 ' + success + ' / 失败 ' + failed); + return { flushed: success, failed: failed, remaining: files.length - success }; +} + +// ====== 回写进度(兼容 Phase 3 sync-engine 调用) ====== +async function updateProgress(syslogData) { + console.log('[Notion Sync] 回写进度到 Notion...'); + + var result = await writeSyslog({ + title: (syslogData.session_id || '') + ' ' + (syslogData.dev_name || syslogData.dev_id || '') + ' ' + (syslogData.status || ''), + session_id: syslogData.session_id, + dev_id: syslogData.dev_id, + module: syslogData.module, + phase_status: syslogData.status || syslogData.completion_status, + content: JSON.stringify(syslogData, null, 2), + timestamp: syslogData.timestamp || new Date().toISOString(), + source: 'M-DINGTALK-钉钉工作台' + }); + + return result; +} + +// ====== 全量同步(兼容 Phase 3 sync-engine 调用) ====== +async function sync() { + console.log('[Notion Sync] 执行 Notion 同步...'); + var health = await healthCheck(); + + if (health.online) { + var flushResult = await flushPending(); + return { + status: 'online', + bot: health.bot_name, + flushed: flushResult.flushed, + remaining: flushResult.remaining || 0 + }; + } else { + return { + status: 'offline', + reason: health.reason, + message: '等待 Token 配置或网络恢复' + }; + } +} + +// ====== 保存回写日志 ====== function saveWritebackLog(record) { - const logDir = path.dirname(WRITEBACK_LOG); + var logDir = path.dirname(WRITEBACK_LOG); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } - let log = loadWritebackLog(); + var log = []; + if (fs.existsSync(WRITEBACK_LOG)) { + try { log = JSON.parse(fs.readFileSync(WRITEBACK_LOG, 'utf-8')); } catch (e) {} + } log.unshift(record); - - if (log.length > 100) log = log.slice(0, 100); - + if (log.length > 200) log = log.slice(0, 200); 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 = { + writeSyslog, + writeSignalLog, + healthCheck, + testDatabase, updateProgress, - sync + sync, + flushPending, + isConfigured };