DEV-004: M-DINGTALK Phase5 Notion数据桥真实连通+SYSLOG实写

This commit is contained in:
zhizhi200271 2026-03-13 09:56:29 +08:00
parent 3510f21987
commit 23b1ca9efa
8 changed files with 827 additions and 253 deletions

1
dingtalk-bot/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

View File

@ -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"
}
]

View File

@ -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 初始化中... ===');

View File

@ -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');
});

View File

@ -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;
}
// 测试2Token有效性调 users/me
console.log('\n--- 测试2Token有效性 ---');
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--- 测试4SYSLOG写入测试 ---');
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);
});

View File

@ -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;
}
// 测试2Token有效性调 users/me
console.log('\n--- 测试2Token有效性 ---');
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--- 测试4SYSLOG写入测试 ---');
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);
});

View File

@ -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('服务已关闭');
});
});

View File

@ -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
};