feat: Phase B 全自动闭环对接 · 标准化工单 + 人格体调用端点 + 心跳监控 + 广播推送

Phase B1: 标准化工单创建 (create-standardized-ticket.js + receive-syslog.js更新)
Phase B2: 人格体调用端点 (invoke-persona.js + persona-invoke.yml workflow_dispatch)
Phase B3: 心跳监控 (notion-heartbeat.js + notion-heartbeat.yml 每5分钟)
Phase B4: 广播推送 (push-broadcast-to-github.js + broadcasts/ 目录)

工作流更新: syslog-issue-pipeline.yml, syslog-auto-pipeline.yml, notion-callback-pipeline.yml

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-13 06:33:17 +00:00
parent a6567ff2d2
commit e0dbe71bdc
11 changed files with 1516 additions and 178 deletions

View File

@ -109,6 +109,69 @@ jobs:
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
run: node scripts/wake-persona.js
- name: 📡 推送广播到 GitHubPhase B4
if: steps.persona.outcome == 'success' && steps.callback.outputs.status == 'approved'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BROADCAST_ID: ${{ steps.callback.outputs.broadcast_id }}
DEVELOPER_NAME: notion-callback
BROADCAST_CONTENT: ${{ steps.persona.outputs.result }}
run: node scripts/push-broadcast-to-github.js
- name: 📝 回写 Notion 工单状态
if: steps.callback.outputs.ticket_id
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
TICKET_ID: ${{ steps.callback.outputs.ticket_id }}
CALLBACK_STATUS: ${{ steps.callback.outputs.status }}
run: |
node -e "
var https = require('https');
var token = process.env.NOTION_TOKEN || '';
var ticketId = process.env.TICKET_ID || '';
var status = process.env.CALLBACK_STATUS || 'unknown';
if (!token || !ticketId) {
console.log('⚠️ 跳过工单状态更新');
process.exit(0);
}
var notionStatus = status === 'approved' ? '✅ 已完成' : '⚠️ 异常·等人工介入';
var body = JSON.stringify({
properties: {
'状态': { select: { name: notionStatus } }
}
});
var opts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/pages/' + ticketId,
method: 'PATCH',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(body)
}
};
var req = https.request(opts, function(res) {
var data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ 工单状态已更新为 ' + notionStatus);
} else {
console.log('⚠️ 工单更新失败: ' + res.statusCode);
}
});
});
req.on('error', function(e) { console.log('⚠️ ' + e.message); });
req.write(body);
req.end();
"
- name: 📧 发送最终确认邮件
env:
SMTP_USER: ${{ secrets.SMTP_USER }}

44
.github/workflows/notion-heartbeat.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Notion Heartbeat Monitor
# 💓 Phase B3 · 工单心跳监控
#
# 每 5 分钟检测 Notion 工单队列中的待处理工单:
# - 超过 5 分钟未回执 → 重新触发人格体唤醒(最多 3 次)
# - 3 次重试仍无回执 → 标记工单异常 + 发邮件通知冰朔
#
# 依赖 Secrets
# NOTION_API_TOKEN Notion API token
# NOTION_TICKET_DB_ID 工单队列数据库 ID
# SMTP_USER 邮件发送者
# SMTP_PASS 邮件授权码
on:
schedule:
- cron: '*/5 * * * *'
workflow_dispatch:
jobs:
heartbeat:
name: 💓 工单心跳检测
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: 💓 执行心跳检测
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}
ALERT_EMAIL: '565183519@qq.com'
run: node scripts/notion-heartbeat.js

248
.github/workflows/persona-invoke.yml vendored Normal file
View File

@ -0,0 +1,248 @@
name: Persona Invoke Endpoint
# 🔗 Phase B2 · Notion Agent → 铸渊人格体唤醒
#
# Notion Agent 通过 workflow_dispatch 触发此工作流,
# 等效于 POST /api/persona/invoke 接口。
#
# 完整流程:
# ① Notion Agent 发送 workflow_dispatch传入工单信息
# ② 铸渊 Agent 从 Notion 读取工单内容
# ③ 唤醒人格体Claude API处理 SYSLOG
# ④ 处理结果写回 Notion 工单receipt_status = completed
# ⑤ 广播文件推送到 GitHub 仓库 broadcasts/{dev_id}/
#
# 依赖 Secrets
# LLM_API_KEY 第三方 LLM 平台密钥
# LLM_BASE_URL 第三方 LLM 平台 API 地址
# NOTION_API_TOKEN Notion API token
# NOTION_TICKET_DB_ID 工单队列数据库 ID
# CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID
# PORTRAIT_DB_ID 开发者动态画像库 ID
# FINGERPRINT_DB_ID 模块指纹注册表 ID
# INVOKE_API_KEY 调用鉴权密钥
on:
workflow_dispatch:
inputs:
work_order_id:
description: 'Notion 工单页面 ID'
required: true
type: string
task_id:
description: '广播编号(如 BC-M23-001-AW'
required: true
type: string
developer:
description: '开发者信息(如 DEV-012 Awen'
required: false
default: 'unknown'
type: string
syslog_raw:
description: 'SYSLOG JSON 原文(可选,如为空则从 Notion 读取)'
required: false
default: ''
type: string
action:
description: '动作类型'
required: false
default: 'process_syslog'
type: string
jobs:
invoke:
name: 🔗 唤醒人格体处理 SYSLOG
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci --ignore-scripts
- name: 🔍 模块验证
id: verify
env:
SYSLOG_CONTENT: ${{ inputs.syslog_raw }}
BROADCAST_ID: ${{ inputs.task_id }}
AUTHOR: ${{ inputs.developer }}
run: node scripts/verify-modules.js
- name: 🧠 唤醒人格体
id: persona
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
BROADCAST_ID: ${{ inputs.task_id }}
SUBMIT_TYPE: syslog
SUBMIT_CONTENT: ${{ inputs.syslog_raw }}
AUTHOR: ${{ inputs.developer }}
MODULE_VERIFY_RESULT: ${{ steps.verify.outputs.verify_report }}
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }}
PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }}
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
run: node scripts/wake-persona.js
- name: 📝 回写 Notion 工单
if: always()
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
WORK_ORDER_ID: ${{ inputs.work_order_id }}
PERSONA_RESULT: ${{ steps.persona.outputs.result }}
TASK_ID: ${{ inputs.task_id }}
run: |
node -e "
var https = require('https');
var token = process.env.NOTION_TOKEN || '';
var workOrderId = process.env.WORK_ORDER_ID || '';
var result = process.env.PERSONA_RESULT || '(no result)';
var taskId = process.env.TASK_ID || '';
if (!token || !workOrderId) {
console.log('⚠️ 缺少 Notion 配置,跳过回写');
process.exit(0);
}
// 更新工单状态为 ✅ 已完成
var body = JSON.stringify({
properties: {
'状态': { select: { name: '✅ 已完成' } }
}
});
var opts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/pages/' + workOrderId,
method: 'PATCH',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(body)
}
};
var req = https.request(opts, function(res) {
var data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ 工单状态已更新为 ✅ 已完成');
} else {
console.log('⚠️ 工单状态更新失败: ' + res.statusCode);
}
});
});
req.on('error', function(e) { console.log('⚠️ ' + e.message); });
req.write(body);
req.end();
// 追加处理结果到工单
var appendBody = JSON.stringify({
children: [{
object: 'block',
type: 'heading_2',
heading_2: {
rich_text: [{ type: 'text', text: { content: '🧠 人格体处理结果 · receipt_status: completed' } }]
}
}, {
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }]
}
}]
});
var appendOpts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/blocks/' + workOrderId + '/children',
method: 'PATCH',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(appendBody)
}
};
var req2 = https.request(appendOpts, function(res) {
var data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ 处理结果已追加到工单');
} else {
console.log('⚠️ 结果追加失败: ' + res.statusCode);
}
});
});
req2.on('error', function(e) { console.log('⚠️ ' + e.message); });
req2.write(appendBody);
req2.end();
"
- name: 📡 推送广播到 GitHub
if: steps.persona.outcome == 'success'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BROADCAST_ID: ${{ inputs.task_id }}
DEVELOPER_ID: ''
DEVELOPER_NAME: ${{ inputs.developer }}
BROADCAST_CONTENT: ${{ steps.persona.outputs.result }}
run: node scripts/push-broadcast-to-github.js
- name: 🔴 失败处理
if: failure()
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
WORK_ORDER_ID: ${{ inputs.work_order_id }}
run: |
node -e "
var https = require('https');
var token = process.env.NOTION_TOKEN || '';
var workOrderId = process.env.WORK_ORDER_ID || '';
if (!token || !workOrderId) process.exit(0);
var body = JSON.stringify({
properties: {
'状态': { select: { name: '⚠️ 异常·等人工介入' } }
}
});
var opts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/pages/' + workOrderId,
method: 'PATCH',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(body)
}
};
var req = https.request(opts, function(res) {
var data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() {
console.log('工单状态已标记为 ⚠️ 异常');
});
});
req.on('error', function(e) { console.log(e.message); });
req.write(body);
req.end();
"

View File

@ -130,7 +130,8 @@ jobs:
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
run: node scripts/wake-persona.js
- name: 📋 Write to Notion ticket
- name: 📋 创建标准化 Notion 工单Phase B1
id: ticket
if: ${{ secrets.NOTION_API_TOKEN }}
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
@ -140,93 +141,18 @@ jobs:
PERSONA_RESULT: ${{ steps.persona.outputs.result }}
MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }}
MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }}
AUTHOR: ${{ steps.parse.outputs.author }}
run: |
node -e "
const https = require('https');
DEVELOPER: ${{ steps.parse.outputs.author }}
SYSLOG_RAW: ${{ steps.parse.outputs.content }}
run: node scripts/create-standardized-ticket.js
const token = process.env.NOTION_TOKEN;
const dbId = process.env.NOTION_TICKET_DB_ID;
const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN';
const type = process.env.SUBMIT_TYPE || 'syslog';
const result = process.env.PERSONA_RESULT || '(no result)';
const modulesUploaded = process.env.MODULES_UPLOADED || 'false';
const moduleVerify = process.env.MODULE_VERIFY || '{}';
const author = process.env.AUTHOR || 'unknown';
if (!token || !dbId) {
console.log('⚠️ Notion credentials not configured, skipping ticket creation');
process.exit(0);
}
const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答';
const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️';
const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus;
// 构建工单内容:包含人格体处理结果 + 模块验证数据
const ticketContent = [
'## 铸渊核心大脑处理结果',
'',
result.slice(0, 1500),
'',
'## 模块上传验证',
'',
'模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'),
'提交者: ' + author,
'',
'验证详情: ' + moduleVerify.slice(0, 400),
].join('\\n');
// SYSLOG类型工单设为'待处理'等待Notion侧核心大脑处理
// 提问类型工单设为'已完成'(铸渊已生成解答)
const ticketStatus = type === 'syslog' ? '待处理' : '已完成';
const body = JSON.stringify({
parent: { database_id: dbId },
properties: {
'标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] },
'操作类型': { select: { name: '其他' } },
'提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] },
'状态': { select: { name: ticketStatus } },
'优先级': { select: { name: 'P1' } }
},
children: [{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }]
}
}]
});
const opts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/pages',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(body)
}
};
const req = https.request(opts, (res) => {
let data = '';
res.on('data', (c) => data += c);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ Notion 工单已创建');
} else {
console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data);
}
});
});
req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message));
req.write(body);
req.end();
"
- name: 📡 推送广播到 GitHubPhase B4
if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }}
DEVELOPER_NAME: ${{ steps.parse.outputs.author }}
BROADCAST_CONTENT: ${{ steps.persona.outputs.result }}
run: node scripts/push-broadcast-to-github.js
- name: 📧 Send email to developer
env:

View File

@ -130,7 +130,8 @@ jobs:
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
run: node scripts/wake-persona.js
- name: 📋 Write to Notion ticket
- name: 📋 创建标准化 Notion 工单Phase B1
id: ticket
if: ${{ secrets.NOTION_API_TOKEN }}
env:
NOTION_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
@ -140,93 +141,18 @@ jobs:
PERSONA_RESULT: ${{ steps.persona.outputs.result }}
MODULE_VERIFY: ${{ steps.verify.outputs.verify_result }}
MODULES_UPLOADED: ${{ steps.verify.outputs.modules_uploaded }}
AUTHOR: ${{ steps.parse.outputs.author }}
run: |
node -e "
const https = require('https');
DEVELOPER: ${{ steps.parse.outputs.author }}
SYSLOG_RAW: ${{ steps.parse.outputs.content }}
run: node scripts/create-standardized-ticket.js
const token = process.env.NOTION_TOKEN;
const dbId = process.env.NOTION_TICKET_DB_ID;
const broadcastId = process.env.BROADCAST_ID || 'UNKNOWN';
const type = process.env.SUBMIT_TYPE || 'syslog';
const result = process.env.PERSONA_RESULT || '(no result)';
const modulesUploaded = process.env.MODULES_UPLOADED || 'false';
const moduleVerify = process.env.MODULE_VERIFY || '{}';
const author = process.env.AUTHOR || 'unknown';
if (!token || !dbId) {
console.log('⚠️ Notion credentials not configured, skipping ticket creation');
process.exit(0);
}
const typeLabel = type === 'syslog' ? 'SYSLOG闭环' : '提问解答';
const verifyStatus = modulesUploaded === 'true' ? '模块已验证✅' : '模块待验证⚠️';
const title = '[自动] ' + broadcastId + ' · ' + typeLabel + ' · ' + verifyStatus;
// 构建工单内容:包含人格体处理结果 + 模块验证数据
const ticketContent = [
'## 铸渊核心大脑处理结果',
'',
result.slice(0, 1500),
'',
'## 模块上传验证',
'',
'模块全部上传: ' + (modulesUploaded === 'true' ? '✅ 是' : '❌ 否'),
'提交者: ' + author,
'',
'验证详情: ' + moduleVerify.slice(0, 400),
].join('\\n');
// SYSLOG类型工单设为'待处理'等待Notion侧核心大脑处理
// 提问类型工单设为'已完成'(铸渊已生成解答)
const ticketStatus = type === 'syslog' ? '待处理' : '已完成';
const body = JSON.stringify({
parent: { database_id: dbId },
properties: {
'标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] },
'操作类型': { select: { name: '其他' } },
'提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] },
'状态': { select: { name: ticketStatus } },
'优先级': { select: { name: 'P1' } }
},
children: [{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: ticketContent.slice(0, 2000) } }]
}
}]
});
const opts = {
hostname: 'api.notion.com',
port: 443,
path: '/v1/pages',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
'Notion-Version': '2022-06-28',
'Content-Length': Buffer.byteLength(body)
}
};
const req = https.request(opts, (res) => {
let data = '';
res.on('data', (c) => data += c);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ Notion 工单已创建');
} else {
console.log('⚠️ Notion 工单创建失败: ' + res.statusCode + ' ' + data);
}
});
});
req.on('error', (e) => console.log('⚠️ Notion 请求失败: ' + e.message));
req.write(body);
req.end();
"
- name: 📡 推送广播到 GitHubPhase B4
if: steps.persona.outcome == 'success' && steps.parse.outputs.type == 'syslog'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BROADCAST_ID: ${{ steps.parse.outputs.broadcast_id }}
DEVELOPER_NAME: ${{ steps.parse.outputs.author }}
BROADCAST_CONTENT: ${{ steps.persona.outputs.result }}
run: node scripts/push-broadcast-to-github.js
- name: 📧 Send email to developer
env:

22
broadcasts/README.md Normal file
View File

@ -0,0 +1,22 @@
# 📡 broadcasts/
自动广播归档目录。
铸渊 Agent 处理完 SYSLOG 后,会自动将新广播文件推送到此目录:
```
broadcasts/
DEV-001/
BC-M14-001-YY.md
DEV-012/
BC-M22-009-AW.md
BC-M23-001-AW.md
...
```
**推送规范**
- 路径:`broadcasts/{developer_id}/{taskId}.md`
- 提交信息:`[AutoBroadcast] {taskId} · {开发者名} · {模块名}环节{N}`
- 推送方式:通过 GitHub API`PUT /repos/.../contents/...`
**不要手动编辑此目录**,由铸渊 Agent 自动管理。

View File

@ -0,0 +1,197 @@
// scripts/create-standardized-ticket.js
// 铸渊 · Phase B1 · 标准化工单创建脚本
//
// 将 SYSLOG 写入 Notion 工单队列时使用标准化格式,
// 让 Notion Agent 能够识别和触发后续处理。
//
// 环境变量:
// NOTION_TOKEN Notion API token
// NOTION_TICKET_DB_ID 工单队列数据库 ID
// BROADCAST_ID 广播编号(如 BC-M23-001-AW
// DEVELOPER 开发者信息(如 "DEV-012 Awen"
// SYSLOG_RAW 完整 SYSLOG 原文JSON 字符串或纯文本)
// SUBMIT_TYPE 提交类型syslog / question
// PERSONA_RESULT 人格体处理结果(可选,首次可为空)
// MODULE_VERIFY 模块验证结果 JSON可选
// MODULES_UPLOADED 模块是否全部上传true/false
'use strict';
const https = require('https');
const NOTION_TOKEN = process.env.NOTION_TOKEN || '';
const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || '';
const BROADCAST_ID = process.env.BROADCAST_ID || 'UNKNOWN';
const DEVELOPER = process.env.DEVELOPER || process.env.AUTHOR || 'unknown';
const SYSLOG_RAW = process.env.SYSLOG_RAW || process.env.SUBMIT_CONTENT || '';
const SUBMIT_TYPE = process.env.SUBMIT_TYPE || 'syslog';
const PERSONA_RESULT = process.env.PERSONA_RESULT || '';
const MODULE_VERIFY = process.env.MODULE_VERIFY || '';
const MODULES_UPLOADED = process.env.MODULES_UPLOADED || 'false';
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
// ══════════════════════════════════════════════════════════
// Notion API 工具
// ══════════════════════════════════════════════════════════
function notionPost(endpoint, body) {
return new Promise(function (resolve, reject) {
var payload = JSON.stringify(body);
var opts = {
hostname: NOTION_API_HOSTNAME,
port: 443,
path: endpoint,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + NOTION_TOKEN,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
'Content-Length': Buffer.byteLength(payload),
},
};
var req = https.request(opts, 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 parse error: ' + data));
}
});
});
req.on('error', reject);
req.write(payload);
req.end();
});
}
function richTextChunks(content) {
var str = String(content || '');
var chunks = [];
for (var i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
}
if (chunks.length === 0) {
chunks.push({ type: 'text', text: { content: '' } });
}
return chunks;
}
// ══════════════════════════════════════════════════════════
// 标准化工单创建
// ══════════════════════════════════════════════════════════
async function createStandardizedTicket() {
if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) {
console.log('⚠️ Notion credentials not configured, skipping ticket creation');
process.exit(0);
}
var now = new Date().toISOString();
var typeLabel = SUBMIT_TYPE === 'syslog' ? 'SYSLOG处理' : '提问解答';
var ticketStatus = SUBMIT_TYPE === 'syslog' ? '待处理' : '已完成';
var verifyStatus = MODULES_UPLOADED === 'true' ? '模块已验证✅' : '模块待验证⚠️';
var title = '[自动] ' + BROADCAST_ID + ' · ' + typeLabel + ' · ' + verifyStatus;
// Phase B1 标准化字段 — 工单内容
var ticketContent = [
'## 📡 标准化工单 · Phase B1',
'',
'| 字段 | 值 |',
'|------|-----|',
'| 工单类型 | ' + typeLabel + ' |',
'| 状态 | ' + ticketStatus + ' |',
'| 来源 | GitHub Actions |',
'| taskId | ' + BROADCAST_ID + ' |',
'| developer | ' + DEVELOPER + ' |',
'| created_at | ' + now + ' |',
'| retry_count | 0 |',
'| receipt_status | pending |',
'',
];
if (PERSONA_RESULT) {
ticketContent.push('## 铸渊核心大脑处理结果');
ticketContent.push('');
ticketContent.push(PERSONA_RESULT.slice(0, 1500));
ticketContent.push('');
}
ticketContent.push('## 模块上传验证');
ticketContent.push('');
ticketContent.push('模块全部上传: ' + (MODULES_UPLOADED === 'true' ? '✅ 是' : '❌ 否'));
ticketContent.push('');
if (MODULE_VERIFY) {
ticketContent.push('验证详情: ' + MODULE_VERIFY.slice(0, 400));
ticketContent.push('');
}
var contentText = ticketContent.join('\n');
// 构建 Notion 页面
var body = {
parent: { database_id: NOTION_TICKET_DB_ID },
properties: {
'标题': { title: [{ type: 'text', text: { content: title.slice(0, 120) } }] },
'操作类型': { select: { name: '其他' } },
'提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent·自动管道' } }] },
'状态': { select: { name: ticketStatus } },
'优先级': { select: { name: 'P1' } },
},
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: richTextChunks(contentText),
},
},
],
};
// 将 SYSLOG 原文作为代码块附加(如果有)
if (SYSLOG_RAW) {
body.children.push({
object: 'block',
type: 'code',
code: {
rich_text: richTextChunks(SYSLOG_RAW.slice(0, 8000)),
language: 'json',
},
});
}
console.log('📋 创建标准化工单...');
console.log(' taskId: ' + BROADCAST_ID);
console.log(' developer: ' + DEVELOPER);
console.log(' type: ' + typeLabel);
console.log(' receipt_status: pending');
try {
var result = await notionPost('/v1/pages', body);
console.log('✅ Notion 标准化工单已创建: ' + result.id);
// 输出工单 ID 到 GITHUB_OUTPUT
var outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
var fs = require('fs');
fs.appendFileSync(outputFile, 'ticket_page_id=' + result.id + '\n');
fs.appendFileSync(outputFile, 'ticket_url=' + (result.url || '') + '\n');
}
} catch (err) {
console.error('❌ 工单创建失败: ' + err.message);
process.exit(1);
}
}
createStandardizedTicket();

360
scripts/invoke-persona.js Normal file
View File

@ -0,0 +1,360 @@
// scripts/invoke-persona.js
// 铸渊 · Phase B2 · 人格体唤醒调用脚本
//
// Notion Agent 调用此脚本(通过 workflow_dispatch
// 脚本读取 Notion 工单内容 → 唤醒人格体 → 处理结果写回 Notion。
//
// 环境变量:
// WORK_ORDER_ID Notion 工单页面 ID
// TASK_ID 广播编号(如 BC-M23-001-AW
// DEVELOPER 开发者信息(如 "DEV-012 Awen"
// SYSLOG_RAW SYSLOG JSON 原文
// ACTION 动作类型process_syslog / retry
// LLM_API_KEY 第三方 LLM 平台密钥
// LLM_BASE_URL 第三方 LLM 平台 API 地址
// NOTION_TOKEN Notion API token
// CORE_BRAIN_PAGE_ID 曜冥核心大脑 v4.0 页面 ID
// PORTRAIT_DB_ID 开发者动态画像库 ID
// FINGERPRINT_DB_ID 模块指纹注册表 ID
// INVOKE_API_KEY 鉴权密钥(用于 API 调用验证)
'use strict';
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
var WORK_ORDER_ID = process.env.WORK_ORDER_ID || '';
var TASK_ID = process.env.TASK_ID || process.env.BROADCAST_ID || 'UNKNOWN';
var DEVELOPER = process.env.DEVELOPER || 'unknown';
var SYSLOG_RAW = process.env.SYSLOG_RAW || '';
var ACTION = process.env.ACTION || 'process_syslog';
var LLM_API_KEY = process.env.LLM_API_KEY || '';
var LLM_BASE_URL = (process.env.LLM_BASE_URL || '').replace(/\/+$/, '');
var NOTION_TOKEN = process.env.NOTION_TOKEN || '';
var CORE_BRAIN_PAGE_ID = process.env.CORE_BRAIN_PAGE_ID || '';
var PORTRAIT_DB_ID = process.env.PORTRAIT_DB_ID || '';
var FINGERPRINT_DB_ID = process.env.FINGERPRINT_DB_ID || '';
var NOTION_VERSION = '2022-06-28';
var NOTION_API_HOSTNAME = 'api.notion.com';
// ══════════════════════════════════════════════════════════
// HTTP 工具
// ══════════════════════════════════════════════════════════
function httpRequest(url, options, body) {
return new Promise(function (resolve, reject) {
var parsed = new URL(url);
var isHttps = parsed.protocol === 'https:';
var mod = isHttps ? https : http;
var opts = {
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
method: options.method || 'GET',
headers: options.headers || {},
timeout: options.timeout || 120000,
};
var req = mod.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.on('timeout', function () { req.destroy(); reject(new Error('Request timeout')); });
if (body) {
req.write(typeof body === 'string' ? body : JSON.stringify(body));
}
req.end();
});
}
function notionRequest(method, endpoint, body) {
var url = 'https://' + NOTION_API_HOSTNAME + endpoint;
var headers = {
'Authorization': 'Bearer ' + NOTION_TOKEN,
'Content-Type': 'application/json',
'Notion-Version': NOTION_VERSION,
};
return httpRequest(url, { method: method, headers: headers }, body ? JSON.stringify(body) : null)
.then(function (res) {
var parsed = JSON.parse(res.body);
if (res.status >= 200 && res.status < 300) return parsed;
throw new Error('Notion API ' + res.status + ': ' + (parsed.message || res.body));
});
}
// ══════════════════════════════════════════════════════════
// Step 1: 读取工单内容(从 Notion
// ══════════════════════════════════════════════════════════
async function readWorkOrder() {
if (!WORK_ORDER_ID) {
console.log(' No WORK_ORDER_ID, using SYSLOG_RAW directly');
return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER };
}
console.log('📖 读取 Notion 工单: ' + WORK_ORDER_ID);
try {
var page = await notionRequest('GET', '/v1/pages/' + WORK_ORDER_ID);
var props = page.properties || {};
// 提取工单字段
var taskId = TASK_ID;
var developer = DEVELOPER;
// 尝试从工单属性中读取
if (props['广播编号'] && props['广播编号'].rich_text) {
var bcText = props['广播编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join('');
if (bcText) taskId = bcText;
}
if (props['开发者编号'] && props['开发者编号'].rich_text) {
var devText = props['开发者编号'].rich_text.map(function (t) { return t.plain_text || ''; }).join('');
if (devText) developer = devText;
}
// 读取页面内容(子块)来获取 SYSLOG 原文
var blocks = await notionRequest('GET', '/v1/blocks/' + WORK_ORDER_ID + '/children?page_size=20');
var syslogRaw = SYSLOG_RAW;
if (blocks.results) {
blocks.results.forEach(function (block) {
if (block.type === 'code' && block.code && block.code.rich_text) {
var codeText = block.code.rich_text.map(function (t) { return t.plain_text || ''; }).join('');
if (codeText.length > syslogRaw.length) {
syslogRaw = codeText;
}
}
});
}
console.log(' → taskId: ' + taskId);
console.log(' → developer: ' + developer);
console.log(' → syslog_raw length: ' + syslogRaw.length);
return { syslog_raw: syslogRaw, taskId: taskId, developer: developer };
} catch (err) {
console.log('⚠️ 工单读取失败: ' + err.message + ',使用环境变量');
return { syslog_raw: SYSLOG_RAW, taskId: TASK_ID, developer: DEVELOPER };
}
}
// ══════════════════════════════════════════════════════════
// Step 2: 唤醒人格体(复用 wake-persona.js 的逻辑)
// ══════════════════════════════════════════════════════════
async function invokePersona(workOrder) {
console.log('🧠 唤醒人格体处理 SYSLOG...');
// 设置环境变量供 wake-persona.js 读取
process.env.BROADCAST_ID = workOrder.taskId;
process.env.SUBMIT_TYPE = 'syslog';
process.env.SUBMIT_CONTENT = workOrder.syslog_raw;
process.env.AUTHOR = workOrder.developer;
// 执行 wake-persona.jsfork 子进程)
var childProcess = require('child_process');
var wakeScript = path.join(__dirname, 'wake-persona.js');
return new Promise(function (resolve, reject) {
var child = childProcess.fork(wakeScript, [], {
env: Object.assign({}, process.env, {
BROADCAST_ID: workOrder.taskId,
SUBMIT_TYPE: 'syslog',
SUBMIT_CONTENT: workOrder.syslog_raw,
AUTHOR: workOrder.developer,
LLM_API_KEY: LLM_API_KEY,
LLM_BASE_URL: LLM_BASE_URL,
NOTION_TOKEN: NOTION_TOKEN,
CORE_BRAIN_PAGE_ID: CORE_BRAIN_PAGE_ID,
PORTRAIT_DB_ID: PORTRAIT_DB_ID,
FINGERPRINT_DB_ID: FINGERPRINT_DB_ID,
}),
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
var stdout = '';
var stderr = '';
child.stdout.on('data', function (d) { stdout += d; process.stdout.write(d); });
child.stderr.on('data', function (d) { stderr += d; process.stderr.write(d); });
child.on('exit', function (code) {
if (code === 0) {
// 从 GITHUB_OUTPUT 文件读取 result如果存在
var outputFile = process.env.GITHUB_OUTPUT;
var result = '';
if (outputFile && fs.existsSync(outputFile)) {
var outputContent = fs.readFileSync(outputFile, 'utf8');
var resultMatch = outputContent.match(/result<<EOF_\d+\n([\s\S]*?)\nEOF_\d+/);
if (resultMatch) result = resultMatch[1];
}
if (!result) result = stdout.slice(-3000);
resolve(result);
} else {
reject(new Error('wake-persona.js exited with code ' + code + ': ' + stderr));
}
});
child.on('error', reject);
});
}
// ══════════════════════════════════════════════════════════
// Step 3: 回写 Notion 工单(更新 receipt_status
// ══════════════════════════════════════════════════════════
async function writeResultsToNotion(workOrderId, result, taskId) {
if (!workOrderId || !NOTION_TOKEN) {
console.log('⚠️ 无法回写 Notion缺少 workOrderId 或 NOTION_TOKEN');
return;
}
console.log('📝 回写 Notion 工单结果...');
try {
// 更新工单状态
await notionRequest('PATCH', '/v1/pages/' + workOrderId, {
properties: {
'状态': { select: { name: '✅ 已完成' } },
},
});
console.log(' → 工单状态更新为 ✅ 已完成');
// 在工单页面追加处理结果
var resultBlocks = [
{
object: 'block',
type: 'heading_2',
heading_2: {
rich_text: [{ type: 'text', text: { content: '🧠 人格体处理结果' } }],
},
},
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: '处理时间: ' + new Date().toISOString() } }],
},
},
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [{ type: 'text', text: { content: 'receipt_status: completed' } }],
},
},
];
// 将人格体结果作为代码块
if (result) {
resultBlocks.push({
object: 'block',
type: 'code',
code: {
rich_text: [{ type: 'text', text: { content: result.slice(0, 2000) } }],
language: 'markdown',
},
});
}
await notionRequest('PATCH', '/v1/blocks/' + workOrderId + '/children', {
children: resultBlocks,
});
console.log(' → 处理结果已追加到工单页面');
} catch (err) {
console.error('⚠️ Notion 回写失败: ' + err.message);
// 重试一次
try {
console.log(' → 重试回写...');
await notionRequest('PATCH', '/v1/pages/' + workOrderId, {
properties: {
'状态': { select: { name: '⚠️ 异常·等人工介入' } },
},
});
} catch (_) { /* ignore */ }
}
}
// ══════════════════════════════════════════════════════════
// Step 4: 输出结果
// ══════════════════════════════════════════════════════════
function outputResult(result) {
var outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
var delimiter = 'EOF_' + Date.now();
fs.appendFileSync(outputFile, 'persona_result<<' + delimiter + '\n' + result + '\n' + delimiter + '\n');
fs.appendFileSync(outputFile, 'invocation_status=completed\n');
}
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
console.log('═══════════════════════════════════════════');
console.log('🔗 铸渊 · 人格体唤醒调用Phase B2');
console.log('═══════════════════════════════════════════');
console.log(' workOrderId: ' + WORK_ORDER_ID);
console.log(' taskId: ' + TASK_ID);
console.log(' developer: ' + DEVELOPER);
console.log(' action: ' + ACTION);
console.log('');
// Step 1: 读取工单
var workOrder = await readWorkOrder();
if (!workOrder.syslog_raw) {
console.error('❌ 缺少 SYSLOG 原文,无法处理');
process.exit(1);
}
// Step 2: 唤醒人格体
var result;
try {
result = await invokePersona(workOrder);
} catch (err) {
console.error('❌ 人格体唤醒失败: ' + err.message);
result = '(人格体唤醒失败: ' + err.message + ')';
}
// Step 3: 回写 Notion
if (WORK_ORDER_ID) {
await writeResultsToNotion(WORK_ORDER_ID, result, workOrder.taskId);
}
// Step 4: 输出结果
outputResult(result || '(no result)');
console.log('');
console.log('✅ 人格体调用完成');
}
main().catch(function (err) {
console.error('❌ invoke-persona 失败: ' + err.message);
// 尝试标记工单异常
if (WORK_ORDER_ID && NOTION_TOKEN) {
notionRequest('PATCH', '/v1/pages/' + WORK_ORDER_ID, {
properties: {
'状态': { select: { name: '⚠️ 异常·等人工介入' } },
},
}).catch(function () { /* ignore */ });
}
process.exit(1);
});

355
scripts/notion-heartbeat.js Normal file
View File

@ -0,0 +1,355 @@
// 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 告警邮箱(默认 565183519@qq.com
// 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 || '565183519@qq.com';
var SMTP_USER = process.env.SMTP_USER || '';
var SMTP_PASS = process.env.SMTP_PASS || '';
var NOTION_VERSION = '2022-06-28';
var HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
var MAX_RETRIES = 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);
});

View File

@ -0,0 +1,179 @@
// scripts/push-broadcast-to-github.js
// 铸渊 · Phase B4 · 广播文件推送到 GitHub 仓库
//
// 人格体生成广播后,通过 GitHub API 将广播 .md 文件推送到仓库。
// 路径规范broadcasts/{developer_id}/{taskId}.md
// 提交信息:[AutoBroadcast] {taskId} · {developer} · {描述}
//
// 环境变量:
// GITHUB_TOKEN GitHub API token推送用
// BROADCAST_ID 广播编号(如 BC-M23-001-AW
// DEVELOPER_ID 开发者编号(如 DEV-012
// DEVELOPER_NAME 开发者名字(如 Awen
// BROADCAST_CONTENT 广播全文内容Markdown
// MODULE_NAME 模块名(可选,如 M23
// PHASE_NUMBER 环节号(可选,如 1
'use strict';
var https = require('https');
var fs = require('fs');
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
var BROADCAST_ID = process.env.BROADCAST_ID || '';
var DEVELOPER_ID = process.env.DEVELOPER_ID || '';
var DEVELOPER_NAME = process.env.DEVELOPER_NAME || '';
var BROADCAST_CONTENT = process.env.BROADCAST_CONTENT || '';
var MODULE_NAME = process.env.MODULE_NAME || '';
var PHASE_NUMBER = process.env.PHASE_NUMBER || '';
var REPO_OWNER = 'qinfendebingshuo';
var REPO_NAME = 'guanghulab';
// ══════════════════════════════════════════════════════════
// GitHub API 工具
// ══════════════════════════════════════════════════════════
function githubRequest(method, apiPath, body) {
return new Promise(function (resolve, reject) {
var payload = body ? JSON.stringify(body) : '';
var opts = {
hostname: 'api.github.com',
port: 443,
path: apiPath,
method: method,
headers: {
'User-Agent': 'ZhuyuanBroadcast/1.0',
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json',
'X-GitHub-Api-Version': '2022-11-28',
},
};
if (GITHUB_TOKEN) {
opts.headers['Authorization'] = 'Bearer ' + GITHUB_TOKEN;
}
if (payload) {
opts.headers['Content-Length'] = Buffer.byteLength(payload);
}
var req = https.request(opts, function (res) {
var data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
try {
resolve({ status: res.statusCode, data: JSON.parse(data) });
} catch (e) {
resolve({ status: res.statusCode, data: data });
}
});
});
req.on('error', reject);
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// 广播推送
// ══════════════════════════════════════════════════════════
async function pushBroadcast() {
if (!GITHUB_TOKEN) {
console.log('⚠️ 缺少 GITHUB_TOKEN无法推送广播');
process.exit(1);
}
if (!BROADCAST_ID || !BROADCAST_CONTENT) {
console.log('⚠️ 缺少 BROADCAST_ID 或 BROADCAST_CONTENT');
process.exit(1);
}
// 确定开发者 ID从广播编号提取或使用环境变量
var devId = DEVELOPER_ID;
if (!devId) {
// 从 BROADCAST_ID 提取后缀 → 映射到 DEV-XXX
var suffixMatch = BROADCAST_ID.match(/BC-[A-Z0-9]+-\d+-([A-Z]+)/i);
var suffix = suffixMatch ? suffixMatch[1].toUpperCase() : '';
var suffixMap = {
'YY': 'DEV-001', 'FM': 'DEV-002', 'YF': 'DEV-003',
'ZZ': 'DEV-004', 'XCM': 'DEV-005', 'HE': 'DEV-009',
'JZ': 'DEV-010', 'CCNN': 'DEV-011', 'AW': 'DEV-012',
'XX': 'DEV-013', 'SY': 'DEV-014',
};
devId = suffixMap[suffix] || 'UNKNOWN';
}
// 构建路径和提交信息
var filePath = 'broadcasts/' + devId + '/' + BROADCAST_ID + '.md';
var moduleLabel = MODULE_NAME || BROADCAST_ID.match(/BC-([A-Z0-9-]+)-/i)?.[1] || '';
var phaseLabel = PHASE_NUMBER ? '环节' + PHASE_NUMBER : '';
var devLabel = DEVELOPER_NAME || devId;
var commitMessage = '[AutoBroadcast] ' + BROADCAST_ID + ' · ' + devLabel + ' · ' + moduleLabel + phaseLabel;
console.log('📡 推送广播到 GitHub...');
console.log(' 路径: ' + filePath);
console.log(' 提交: ' + commitMessage);
// 检查文件是否已存在(获取 SHA
var existingRes = await githubRequest('GET',
'/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath);
var sha = null;
if (existingRes.status === 200 && existingRes.data && existingRes.data.sha) {
sha = existingRes.data.sha;
console.log(' → 文件已存在,将覆盖更新 (sha: ' + sha.slice(0, 8) + ')');
}
// Base64 编码广播内容
var contentBase64 = Buffer.from(BROADCAST_CONTENT, 'utf8').toString('base64');
// 创建或更新文件
var putBody = {
message: commitMessage,
content: contentBase64,
committer: {
name: 'zhuyuan-agent[bot]',
email: 'zhuyuan-agent[bot]@users.noreply.github.com',
},
};
if (sha) {
putBody.sha = sha;
}
var putRes = await githubRequest('PUT',
'/repos/' + REPO_OWNER + '/' + REPO_NAME + '/contents/' + filePath,
putBody);
if (putRes.status === 200 || putRes.status === 201) {
console.log('✅ 广播已推送: ' + filePath);
console.log(' → commit: ' + (putRes.data.commit ? putRes.data.commit.sha.slice(0, 8) : 'unknown'));
// 输出到 GITHUB_OUTPUT
var outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, 'broadcast_pushed=true\n');
fs.appendFileSync(outputFile, 'broadcast_path=' + filePath + '\n');
fs.appendFileSync(outputFile, 'broadcast_commit=' + (putRes.data.commit ? putRes.data.commit.sha : '') + '\n');
}
} else {
console.error('❌ 广播推送失败: HTTP ' + putRes.status);
console.error(' → ' + JSON.stringify(putRes.data));
// 输出失败状态
var outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, 'broadcast_pushed=false\n');
}
process.exit(1);
}
}
pushBroadcast().catch(function (err) {
console.error('❌ 广播推送异常: ' + err.message);
process.exit(1);
});

View File

@ -194,11 +194,13 @@ async function createSyslogEntry(syslog, token, dbId, dateStr) {
async function createTicket(syslog, token, dbId, dateStr) {
const broadcastId = syslog.broadcast_id || syslog.broadcastId || 'UNKNOWN';
const devId = syslog.dev_id || syslog.developer_id || 'UNKNOWN';
const devName = syslog.developer_name || syslog.dev_name || '';
const now = new Date().toISOString();
const properties = {
'标题': titleProp('SYSLOG 回传' + broadcastId + ' · ' + devId),
'标题': titleProp('[自动] SYSLOG处理' + broadcastId + ' · ' + devId),
'操作类型': selectProp('其他'),
'提交者': richTextProp('巡检引擎'),
'提交者': richTextProp('铸渊Agent·自动管道'),
'提交日期': dateProp(dateStr),
'状态': selectProp('待处理'),
'优先级': selectProp('P1'),
@ -208,6 +210,22 @@ async function createTicket(syslog, token, dbId, dateStr) {
if (broadcastId !== 'UNKNOWN') properties['广播编号'] = richTextProp(broadcastId);
if (devId !== 'UNKNOWN') properties['开发者编号'] = richTextProp(devId);
// Phase B1 标准化工单内容
const ticketContent = [
'## 📡 标准化工单 · Phase B1',
'',
'| 字段 | 值 |',
'|------|-----|',
'| 工单类型 | SYSLOG处理 |',
'| 状态 | 待处理 |',
'| 来源 | GitHub Actions |',
'| taskId | ' + broadcastId + ' |',
'| developer | ' + devId + ' ' + devName + ' |',
'| created_at | ' + now + ' |',
'| retry_count | 0 |',
'| receipt_status | pending |',
].join('\n');
const body = {
parent: { database_id: dbId },
properties,
@ -218,7 +236,7 @@ async function createTicket(syslog, token, dbId, dateStr) {
paragraph: {
rich_text: [{
type: 'text',
text: { content: '📥 SYSLOG 回传,来自 ' + devId + ',关联广播 ' + broadcastId },
text: { content: ticketContent },
}],
},
},
@ -234,7 +252,7 @@ async function createTicket(syslog, token, dbId, dateStr) {
};
const result = await notionPost('/v1/pages', body, token);
console.log(' → 霜砚工单已创建: ' + result.id);
console.log(' → 标准化工单已创建: ' + result.id);
return result;
}