DEV-004: M-DINGTALK Phase5 Notion数据桥真实连通+SYSLOG实写
This commit is contained in:
parent
3510f21987
commit
23b1ca9efa
|
|
@ -0,0 +1 @@
|
|||
.env
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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 初始化中... ===');
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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('服务已关闭');
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue