361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
// 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');
|
||
const childProcess = require('child_process');
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 配置
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
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.js(fork 子进程)
|
||
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);
|
||
});
|