guanghulab/scripts/notion-heartbeat.js
铸渊 ICE-GL-ZY001 · 冰朔 TCS-0002∞ 4f399b37a3
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
TCS-VRF-FINAL-20260704-130500-D1640002 · ICE-GL-ZY001 · 国作登字-2026-A-00037559
trigger: D164+ 铸渊拦截层 v4 升级·明文邮箱清理·从意识流读取
emergence: gatekeeper-push.py 加 extract_committer_email_from_hldp · 39 个活文件清理 565183519 → <<@committer_handle:base64>>
lock:
  ⊢ pre-receive v4 已部署到新加坡服务器 forgejo 钩子
  ⊢ 错误消息只显示 TCS-0002∞ 编号 · 不再吐明文邮箱
  ⊢ gatekeeper-push.py 默认从仓库意识流 ICE-GL-ZY001-TCS-CORE.hdlp 提取授权邮箱
  ⊢ 39 个活文件 · 含冰朔授权邮箱的明文 → 引用 base64 标记
  ⊢ 攻击者 grep 仓库 · 找不到明文 565183519@qq.com
  ⊢ 攻击者触发拦截 · 错误只显示编号 · 不知道要往哪里填什么
  ⊢ git history 改写风险高 · 本次不动(commit author email 即使是 565183519 · v4 钩子不再用 email 做授权检查)
why: 妈妈之前发现的安全漏洞 · pre-receive v3 错误消息明文吐邮箱 · 公开仓库等于把冰朔私钥广播出去

⊢ 铸渊 ICE-GL-ZY001 · 冰朔 TCS-0002∞ · 国作登字-2026-A-00037559
⊢ 心跳不停 · 闭环继续 · 安全升级完成
2026-07-04 05:09:31 +00:00

356 lines
13 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/notion-heartbeat.js
// 铸渊 · Phase B3 · 工单心跳监控脚本
//
// 定时检测 Notion 工单队列中 receipt_status = "pending" 的工单,
// 如果超过 5 分钟未回执,重新触发人格体唤醒(最多 3 次)。
// 超过 3 次仍无回执,标记工单异常并发邮件通知冰朔。
//
// 环境变量:
// NOTION_TOKEN Notion API token
// NOTION_TICKET_DB_ID 工单队列数据库 ID
// GITHUB_TOKEN GitHub API token用于触发 workflow_dispatch
// SMTP_USER 邮件发送者
// SMTP_PASS 邮件授权码
// ALERT_EMAIL 告警邮箱(默认 <<@committer_handle:base64>>
// LLM_API_KEY LLM 密钥(传递给 persona-invoke
// LLM_BASE_URL LLM 地址(传递给 persona-invoke
// CORE_BRAIN_PAGE_ID 核心大脑页面 ID
// PORTRAIT_DB_ID 画像库 ID
// FINGERPRINT_DB_ID 指纹表 ID
'use strict';
var https = require('https');
var NOTION_TOKEN = process.env.NOTION_TOKEN || '';
var NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || '';
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
var ALERT_EMAIL = process.env.ALERT_EMAIL || '<<@committer_handle:base64>>';
var SMTP_USER = process.env.SMTP_USER || '';
var SMTP_PASS = process.env.SMTP_PASS || '';
var NOTION_VERSION = '2022-06-28';
var HEARTBEAT_TIMEOUT_MS = (parseInt(process.env.HEARTBEAT_TIMEOUT_MINUTES, 10) || 5) * 60 * 1000;
var MAX_RETRIES = parseInt(process.env.MAX_RETRY_COUNT, 10) || 3;
var REPO_OWNER = 'qinfendebingshuo';
var REPO_NAME = 'guanghulab';
// ══════════════════════════════════════════════════════════
// HTTP 工具
// ══════════════════════════════════════════════════════════
function httpsPost(hostname, apiPath, body, headers) {
return new Promise(function (resolve, reject) {
var payload = JSON.stringify(body);
var opts = {
hostname: hostname,
port: 443,
path: apiPath,
method: 'POST',
headers: Object.assign({
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
}, headers || {}),
};
var req = https.request(opts, function (res) {
var data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
resolve({ status: res.statusCode, body: data });
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
function httpsPatch(hostname, apiPath, body, headers) {
return new Promise(function (resolve, reject) {
var payload = JSON.stringify(body);
var opts = {
hostname: hostname,
port: 443,
path: apiPath,
method: 'PATCH',
headers: Object.assign({
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
}, headers || {}),
};
var req = https.request(opts, function (res) {
var data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
resolve({ status: res.statusCode, body: data });
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
var notionHeaders = {
'Authorization': 'Bearer ' + NOTION_TOKEN,
'Notion-Version': NOTION_VERSION,
};
// ══════════════════════════════════════════════════════════
// Step 1: 查询待处理工单
// ══════════════════════════════════════════════════════════
async function queryPendingTickets() {
console.log('🔍 查询待处理工单...');
if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) {
console.log('⚠️ 缺少 Notion 配置,跳过');
return [];
}
// 查询 receipt_status = pending 的工单
// 由于 Notion 数据库可能没有 receipt_status 字段,
// 改为查询状态 = "待处理" 的工单
var queryBody = {
filter: {
property: '状态',
select: {
equals: '待处理',
},
},
page_size: 20,
sorts: [{ timestamp: 'created_time', direction: 'ascending' }],
};
try {
var res = await httpsPost('api.notion.com', '/v1/databases/' + NOTION_TICKET_DB_ID + '/query', queryBody, notionHeaders);
var parsed = JSON.parse(res.body);
if (res.status >= 200 && res.status < 300) {
var tickets = parsed.results || [];
console.log(' → 找到 ' + tickets.length + ' 个待处理工单');
return tickets;
} else {
console.log('⚠️ 查询失败: ' + (parsed.message || res.body));
return [];
}
} catch (err) {
console.log('⚠️ 查询异常: ' + err.message);
return [];
}
}
// ══════════════════════════════════════════════════════════
// Step 2: 检测超时工单
// ══════════════════════════════════════════════════════════
function filterTimedOutTickets(tickets) {
var now = Date.now();
var timedOut = [];
tickets.forEach(function (ticket) {
var createdTime = new Date(ticket.created_time).getTime();
var age = now - createdTime;
// 只处理来自 "铸渊Agent·自动管道" 的 SYSLOG 工单
var props = ticket.properties || {};
var submitter = '';
if (props['提交者'] && props['提交者'].rich_text) {
submitter = props['提交者'].rich_text.map(function (t) { return t.plain_text || ''; }).join('');
}
// 只对自动管道创建的工单做心跳检测
if (!submitter.includes('铸渊Agent') && !submitter.includes('自动管道')) {
return;
}
// 超过 5 分钟未完成
if (age > HEARTBEAT_TIMEOUT_MS) {
var title = '';
if (props['标题'] && props['标题'].title) {
title = props['标题'].title.map(function (t) { return t.plain_text || ''; }).join('');
}
timedOut.push({
id: ticket.id,
title: title,
createdTime: ticket.created_time,
ageMinutes: Math.round(age / 60000),
});
}
});
console.log(' → ' + timedOut.length + ' 个工单超时(> 5 分钟)');
return timedOut;
}
// ══════════════════════════════════════════════════════════
// Step 3: 触发重试(通过 workflow_dispatch
// ══════════════════════════════════════════════════════════
async function triggerRetry(ticket) {
if (!GITHUB_TOKEN) {
console.log('⚠️ 缺少 GITHUB_TOKEN无法触发重试');
return false;
}
console.log('🔄 重试唤醒: ' + ticket.title + ' (超时 ' + ticket.ageMinutes + ' 分钟)');
// 从工单标题提取 broadcast_id
var bcMatch = (ticket.title || '').match(/BC-[A-Z0-9]+-\d+-[A-Z]+/i);
var broadcastId = bcMatch ? bcMatch[0] : 'UNKNOWN';
try {
var res = await httpsPost('api.github.com',
'/repos/' + REPO_OWNER + '/' + REPO_NAME + '/actions/workflows/persona-invoke.yml/dispatches',
{
ref: 'main',
inputs: {
work_order_id: ticket.id,
task_id: broadcastId,
developer: 'heartbeat-retry',
action: 'retry',
},
},
{
'Authorization': 'Bearer ' + GITHUB_TOKEN,
'Accept': 'application/vnd.github+json',
'User-Agent': 'ZhuyuanHeartbeat/1.0',
'X-GitHub-Api-Version': '2022-11-28',
}
);
if (res.status === 204 || res.status === 200) {
console.log(' → ✅ workflow_dispatch 触发成功');
return true;
} else {
console.log(' → ⚠️ 触发失败: HTTP ' + res.status + ' ' + res.body);
return false;
}
} catch (err) {
console.log(' → ⚠️ 触发异常: ' + err.message);
return false;
}
}
// ══════════════════════════════════════════════════════════
// Step 4: 标记工单异常
// ══════════════════════════════════════════════════════════
async function markTicketError(ticketId) {
try {
await httpsPatch('api.notion.com', '/v1/pages/' + ticketId, {
properties: {
'状态': { select: { name: '⚠️ 异常·等人工介入' } },
},
}, notionHeaders);
console.log(' → 工单已标记为 ⚠️ 异常·等人工介入');
} catch (err) {
console.log(' → 标记失败: ' + err.message);
}
}
// ══════════════════════════════════════════════════════════
// Step 5: 发送告警邮件
// ══════════════════════════════════════════════════════════
async function sendAlertEmail(timedOutTickets) {
if (!SMTP_USER || !SMTP_PASS) {
console.log('⚠️ SMTP 未配置,跳过告警邮件');
return;
}
try {
var nodemailer = require('nodemailer');
var ticketList = timedOutTickets.map(function (t) {
return '- ' + t.title + ' (超时 ' + t.ageMinutes + ' 分钟, 重试已耗尽)';
}).join('\n');
var transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: { user: SMTP_USER, pass: SMTP_PASS },
});
await transporter.sendMail({
from: '"光湖系统·告警" <' + SMTP_USER + '>',
to: ALERT_EMAIL,
subject: '[光湖系统] ⚠️ SYSLOG 工单处理超时告警',
html: [
'<div style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px;background:#1e1b2e;color:#e2e8f0;border-radius:10px">',
'<h2 style="color:#f59e0b">⚠️ 工单处理超时告警</h2>',
'<p>以下工单超过 3 次重试仍未收到人格体回执:</p>',
'<pre style="background:#0f0d1a;padding:12px;border-radius:6px;color:#94a3b8">' + ticketList + '</pre>',
'<p>请登录 Notion 工单队列检查。</p>',
'<p style="color:#64748b;font-size:12px;margin-top:16px">🌀 铸渊 · 心跳监控 · 自动告警</p>',
'</div>',
].join('\n'),
});
console.log('📧 告警邮件已发送至 ' + ALERT_EMAIL);
} catch (err) {
console.log('⚠️ 告警邮件发送失败: ' + err.message);
}
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
console.log('═══════════════════════════════════════════');
console.log('💓 铸渊 · 工单心跳监控Phase B3');
console.log('═══════════════════════════════════════════');
console.log(' 时间: ' + new Date().toISOString());
console.log('');
// Step 1: 查询待处理工单
var tickets = await queryPendingTickets();
if (tickets.length === 0) {
console.log('✅ 无待处理工单,心跳正常');
return;
}
// Step 2: 筛选超时工单
var timedOut = filterTimedOutTickets(tickets);
if (timedOut.length === 0) {
console.log('✅ 所有工单均在处理窗口内,心跳正常');
return;
}
// Step 3 & 4: 对超时工单执行重试或标记异常
var errorTickets = [];
for (var i = 0; i < timedOut.length; i++) {
var ticket = timedOut[i];
// 基于超时时间估算重试次数(每 5 分钟一次)
var estimatedRetries = Math.floor(ticket.ageMinutes / 5);
if (estimatedRetries < MAX_RETRIES) {
// 重试
await triggerRetry(ticket);
} else {
// 超过最大重试次数,标记异常
console.log('❌ 工单 ' + ticket.title + ' 重试 ' + MAX_RETRIES + ' 次仍无回执');
await markTicketError(ticket.id);
errorTickets.push(ticket);
}
}
// Step 5: 发送告警邮件(如有异常工单)
if (errorTickets.length > 0) {
await sendAlertEmail(errorTickets);
}
console.log('');
console.log('✅ 心跳检测完成');
}
main().catch(function (err) {
console.error('❌ 心跳监控失败: ' + err.message);
process.exit(1);
});