guanghulab/scripts/neural/write-receipt-to-notion.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

360 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// scripts/neural/write-receipt-to-notion.js
// 📡 指令回执自动写入 Notion
// 铸渊每次执行完指令后,自动将回执写入 Notion「指令回执追踪表」
// 实现 指令→执行→回执 全链路自动闭环
//
// 用法:
// node scripts/neural/write-receipt-to-notion.js \
// --instruction-id "ZY-XXX" \
// --status "success" \
// --workflow "workflow-name" \
// [--summary "执行摘要"] \
// [--receipt "回执详情"] \
// [--new-files 0] \
// [--modified-files 0] \
// [--related-agent "AG-TY-01"]
//
// 环境变量:
// NOTION_API_KEY — Notion API 密钥(必须)
// RECEIPT_DB_ID — 指令回执追踪表的 Notion Database ID必须
//
// 铁律:
// 1. 写入失败不阻塞主流程
// 2. 同一指令编号幂等(重复写入 = 更新)
// 3. 失败时写入本地 SYSLOG 报错
'use strict';
var fs = require('fs');
var path = require('path');
var https = require('https');
// ━━━ 配置 ━━━
var RECEIPT_TRACKER_CONFIG = {
// 字段映射 — Notion 数据库字段名
fieldMap: {
instructionId: '指令编号',
summary: '指令摘要',
status: '执行状态',
receipt: '铸渊回执',
receiptTime: '回执时间',
newFiles: '新增文件数',
modifiedFiles: '修改文件数',
relatedAgent: '关联Agent',
timeoutStatus: '回执超时'
},
// 超时阈值(小时)
timeoutThresholds: {
warning: 24,
critical: 72
},
// 状态映射
statusMap: {
'success': '✅ 已完成',
'failure': '❌ 执行失败',
'cancelled': '⏸️ 已挂起',
'skipped': '⏸️ 已挂起'
}
};
var LOCAL_RECEIPT_DIR = 'data/neural-reports/receipts';
var SYSLOG_DIR = 'data/neural-reports/syslog';
// ━━━ 参数解析 ━━━
function parseArgs() {
var args = process.argv.slice(2);
var parsed = {};
for (var i = 0; i < args.length; i++) {
if (args[i] === '--instruction-id' && args[i + 1]) {
parsed.instructionId = args[++i];
} else if (args[i] === '--status' && args[i + 1]) {
parsed.status = args[++i];
} else if (args[i] === '--workflow' && args[i + 1]) {
parsed.workflow = args[++i];
} else if (args[i] === '--summary' && args[i + 1]) {
parsed.summary = args[++i];
} else if (args[i] === '--receipt' && args[i + 1]) {
parsed.receipt = args[++i];
} else if (args[i] === '--new-files' && args[i + 1]) {
parsed.newFiles = parseInt(args[++i], 10) || 0;
} else if (args[i] === '--modified-files' && args[i + 1]) {
parsed.modifiedFiles = parseInt(args[++i], 10) || 0;
} else if (args[i] === '--related-agent' && args[i + 1]) {
parsed.relatedAgent = args[++i];
}
}
return parsed;
}
// ━━━ Notion API 封装(纯 https无外部依赖━━━
function notionRequest(method, endpoint, body, token) {
return new Promise(function(resolve, reject) {
var bodyStr = body ? JSON.stringify(body) : '';
var options = {
hostname: 'api.notion.com',
port: 443,
path: '/v1' + endpoint,
method: method,
headers: {
'Authorization': 'Bearer ' + token,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(bodyStr)
}
};
var req = https.request(options, function(res) {
var data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
try {
var parsed = JSON.parse(data);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
reject(new Error('Notion API ' + res.statusCode + ': ' + (parsed.message || data)));
}
} catch (e) {
reject(new Error('Notion API response parse error: ' + data.substring(0, 200)));
}
});
});
req.on('error', function(e) {
reject(new Error('Notion API request error: ' + e.message));
});
req.setTimeout(30000, function() {
req.destroy();
reject(new Error('Notion API request timeout (30s)'));
});
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ━━━ Notion 属性构建 ━━━
function buildNotionProperties(receipt) {
var fm = RECEIPT_TRACKER_CONFIG.fieldMap;
var statusText = RECEIPT_TRACKER_CONFIG.statusMap[receipt.status] || '⏸️ 已挂起';
var properties = {};
// 指令编号 (title)
properties[fm.instructionId] = {
title: [{ text: { content: receipt.instructionId || 'AUTO' } }]
};
// 指令摘要 (rich_text)
properties[fm.summary] = {
rich_text: [{ text: { content: receipt.summary || receipt.workflow || '' } }]
};
// 执行状态 (status)
properties[fm.status] = {
status: { name: statusText }
};
// 铸渊回执 (rich_text)
var receiptText = receipt.receipt || (statusText + ' · ' + (receipt.workflow || ''));
properties[fm.receipt] = {
rich_text: [{ text: { content: receiptText.substring(0, 2000) } }]
};
// 回执时间 (date)
properties[fm.receiptTime] = {
date: { start: new Date().toISOString().split('T')[0] }
};
// 新增文件数 (number)
if (typeof receipt.newFiles === 'number') {
properties[fm.newFiles] = { number: receipt.newFiles };
}
// 修改文件数 (number)
if (typeof receipt.modifiedFiles === 'number') {
properties[fm.modifiedFiles] = { number: receipt.modifiedFiles };
}
// 关联Agent (rich_text)
if (receipt.relatedAgent) {
properties[fm.relatedAgent] = {
rich_text: [{ text: { content: receipt.relatedAgent } }]
};
}
// 回执超时 (select)
properties[fm.timeoutStatus] = {
select: { name: '🟢 正常' }
};
return properties;
}
// ━━━ 写入 Notion ━━━
async function writeReceiptToNotion(receipt, token, databaseId) {
console.log('📡 写入回执到 Notion · 指令: ' + receipt.instructionId);
// 先查询是否已有该指令编号的记录(幂等)
var queryBody = {
database_id: databaseId,
filter: {
property: RECEIPT_TRACKER_CONFIG.fieldMap.instructionId,
title: { equals: receipt.instructionId }
}
};
var existing = await notionRequest('POST', '/databases/' + databaseId + '/query', queryBody, token);
var properties = buildNotionProperties(receipt);
if (existing.results && existing.results.length > 0) {
// 更新已有记录
var pageId = existing.results[0].id;
await notionRequest('PATCH', '/pages/' + pageId, { properties: properties }, token);
console.log('✅ 已更新现有记录: ' + pageId);
} else {
// 创建新记录
await notionRequest('POST', '/pages', {
parent: { database_id: databaseId },
properties: properties
}, token);
console.log('✅ 已创建新记录');
}
}
// ━━━ 本地兜底写入 ━━━
function writeLocalReceipt(receipt) {
try {
fs.mkdirSync(LOCAL_RECEIPT_DIR, { recursive: true });
var now = new Date();
var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
var dateStr = cstTime.toISOString().split('T')[0];
var timeStr = cstTime.toISOString().replace(/[-:]/g, '').split('.')[0];
var filename = 'receipt-' + timeStr + '.json';
var fullPath = path.join(LOCAL_RECEIPT_DIR, filename);
var localReceipt = {
instruction_id: receipt.instructionId,
workflow: receipt.workflow,
status: receipt.status,
summary: receipt.summary || '',
receipt_text: receipt.receipt || '',
new_files: receipt.newFiles || 0,
modified_files: receipt.modifiedFiles || 0,
related_agent: receipt.relatedAgent || '',
timestamp: now.toISOString(),
notion_synced: false
};
fs.writeFileSync(fullPath, JSON.stringify(localReceipt, null, 2));
console.log('💾 本地回执已保存: ' + fullPath);
return fullPath;
} catch (e) {
console.error('❌ 本地回执保存失败: ' + e.message);
return null;
}
}
// ━━━ SYSLOG 报错 ━━━
function writeSyslogError(message, receipt) {
try {
fs.mkdirSync(SYSLOG_DIR, { recursive: true });
var now = new Date();
var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
var timeStr = cstTime.toISOString().replace(/[-:]/g, '').split('.')[0];
var logEntry = {
level: 'ERROR',
source: 'write-receipt-to-notion',
message: message,
instruction_id: receipt ? receipt.instructionId : 'unknown',
timestamp: now.toISOString()
};
var filename = 'receipt-error-' + timeStr + '.json';
fs.writeFileSync(
path.join(SYSLOG_DIR, filename),
JSON.stringify(logEntry, null, 2)
);
console.error('📝 错误已记录到 SYSLOG: ' + filename);
} catch (e) {
console.error('⚠️ SYSLOG 写入也失败: ' + e.message);
}
}
// ━━━ 主函数 ━━━
async function main() {
console.log('\n━━━ 📡 指令回执写入 ━━━\n');
var receipt = parseArgs();
if (!receipt.instructionId && !receipt.workflow) {
console.log('⚠️ 未提供 instruction-id 或 workflow跳过回执写入');
process.exit(0);
}
// 如果没有指令ID用 workflow + 日期 自动生成
if (!receipt.instructionId || receipt.instructionId === 'AUTO') {
var now = new Date();
var cstTime = new Date(now.getTime() + 8 * 60 * 60 * 1000);
var dateStr = cstTime.toISOString().split('T')[0].replace(/-/g, '');
receipt.instructionId = 'AUTO-' + (receipt.workflow || 'unknown') + '-' + dateStr;
}
// 始终写入本地兜底
writeLocalReceipt(receipt);
// 尝试写入 Notion
var token = process.env.NOTION_API_KEY;
var databaseId = process.env.RECEIPT_DB_ID;
if (!token) {
var msg = '⚠️ NOTION_API_KEY 未设置,仅保存本地回执';
console.log(msg);
writeSyslogError(msg, receipt);
process.exit(0);
}
if (!databaseId) {
var msg = '⚠️ RECEIPT_DB_ID 未设置,仅保存本地回执';
console.log(msg);
writeSyslogError(msg, receipt);
process.exit(0);
}
try {
await writeReceiptToNotion(receipt, token, databaseId);
console.log('\n✅ 回执写入完成\n');
} catch (e) {
console.error('❌ Notion 写入失败: ' + e.message);
writeSyslogError('Notion 写入失败: ' + e.message, receipt);
// 不阻塞主流程,退出码 0
console.log('💾 已有本地兜底,不影响主流程');
}
process.exit(0);
}
main().catch(function(err) {
console.error('❌ 回执脚本异常: ' + err.message);
process.exit(0); // 不阻塞
});