guanghulab/scripts/aoac/repair-supervisor.js
2026-05-10 13:12:44 +08:00

293 lines
9.4 KiB
JavaScript
Raw 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.

#!/usr/bin/env node
'use strict';
/**
* AOAC-08 · 修复监督Agent (Repair Supervisor)
* ═══════════════════════════════════════════════
* AGE OS Agent Chain · 监督Agent修复监督层
*
* 唤醒条件AOAC-07修复行为启动事件触发
* 职责:监督修复过程→接收修复报告→判断最终结果
* 成功→Notion标绿 失败→发邮件给冰朔
*
* 版权:国作登字-2026-A-00037559 · TCS-0002∞
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const { createTransport } = (() => {
try { return require('nodemailer'); }
catch { return { createTransport: null }; }
})();
const ROOT = path.resolve(__dirname, '..', '..');
const REPAIR_REPORT_PATH = path.join(ROOT, 'data', 'aoac', 'repair-report.json');
const VERDICT_PATH = path.join(ROOT, 'data', 'aoac', 'supervisor-verdict.json');
const CHAIN_STATUS_PATH = path.join(ROOT, 'data', 'aoac', 'chain-status.json');
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
function getBeijingTime() {
const now = new Date();
return new Date(now.getTime() + BEIJING_OFFSET_MS)
.toISOString().replace('T', ' ').slice(0, 19) + '+08:00';
}
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch { return null; }
}
function writeJSON(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
}
function notionRequest(method, endpoint, body, token) {
return new Promise((resolve, reject) => {
const bodyStr = body ? JSON.stringify(body) : '';
const options = {
hostname: NOTION_API_HOSTNAME,
path: endpoint,
method,
headers: {
'Authorization': `Bearer ${token}`,
'Notion-Version': NOTION_VERSION,
'Content-Type': 'application/json',
...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {})
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) reject(new Error(`${res.statusCode}: ${parsed.message || data}`));
else resolve(parsed);
} catch { resolve(data); }
});
});
req.on('error', reject);
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout')); });
if (bodyStr) req.write(bodyStr);
req.end();
});
}
async function updateNotionReceiptGreen(pageId) {
const notionToken = process.env.ZY_NOTION_TOKEN || process.env.NOTION_TOKEN;
if (!notionToken || !pageId) return false;
try {
await notionRequest('PATCH', `/v1/pages/${pageId}`, {
properties: {
'状态': { select: { name: '✅ 已确认' } }
}
}, notionToken);
return true;
} catch (err) {
console.error('⚠️ 更新Notion状态失败:', err.message);
return false;
}
}
async function sendAlertEmail(repairReport) {
const smtpUser = process.env.ZY_SMTP_USER;
const smtpPass = process.env.ZY_SMTP_PASS;
if (!smtpUser || !smtpPass) {
console.log('⚠️ SMTP未配置无法发送告警邮件');
return { sent: false, reason: 'missing_smtp_credentials' };
}
// Use nodemailer if available, otherwise use raw SMTP via https webhook
if (!createTransport) {
console.log('⚠️ nodemailer不可用将通过GitHub Issue告警');
return { sent: false, reason: 'nodemailer_unavailable', fallback: 'github_issue' };
}
try {
const transporter = createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: { user: smtpUser, pass: smtpPass }
});
const diagnosis = repairReport.diagnosis || {};
const body = [
`🔴 AOAC链路修复失败告警`,
``,
`修复ID: ${repairReport.repair_id}`,
`时间: ${repairReport.timestamp}`,
`诊断问题: ${diagnosis.total_issues || 0}个 (关键: ${diagnosis.critical || 0}个)`,
`修复尝试: ${(repairReport.repair_attempts || []).length}`,
`最终结果: ${repairReport.final_result}`,
``,
`问题详情:`,
...(diagnosis.issues || []).map(i => ` - [${i.severity}] ${i.agent}: ${i.issue}`),
``,
`请人工介入检查。`,
``,
`—— 铸渊·AOAC-08监督Agent`
].join('\n');
await transporter.sendMail({
from: smtpUser,
to: smtpUser,
subject: `🔴 AOAC链路修复失败 · ${repairReport.repair_id}`,
text: body
});
return { sent: true, method: 'email' };
} catch (err) {
console.error('❌ 邮件发送失败:', err.message);
return { sent: false, reason: err.message };
}
}
async function createAlertIssue(repairReport) {
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
if (!token) return { created: false, reason: 'no_token' };
const diagnosis = repairReport.diagnosis || {};
const body = [
`## 🔴 AOAC链路修复失败`,
``,
`**修复ID**: ${repairReport.repair_id}`,
`**时间**: ${repairReport.timestamp}`,
`**诊断**: ${diagnosis.total_issues || 0}个问题 (${diagnosis.critical || 0}个关键)`,
`**尝试**: ${(repairReport.repair_attempts || []).length}次修复`,
`**结果**: ❌ ${repairReport.final_result}`,
``,
`### 问题详情`,
...(diagnosis.issues || []).map(i => `- \`[${i.severity}]\` **${i.agent}**: ${i.issue}`),
``,
`---`,
`*此Issue由AOAC-08监督Agent自动创建请冰朔人工介入*`
].join('\n');
return new Promise((resolve) => {
const reqBody = JSON.stringify({
title: `🔴 AOAC链路修复失败 · ${repairReport.repair_id}`,
body,
labels: ['aoac-alert', 'needs-human']
});
const options = {
hostname: 'api.github.com',
path: `/repos/${repo}/issues`,
method: 'POST',
headers: {
'User-Agent': 'AOAC-Supervisor/1.0',
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(reqBody)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve({ created: res.statusCode < 400, issue_number: parsed.number });
} catch { resolve({ created: false }); }
});
});
req.on('error', () => resolve({ created: false }));
req.write(reqBody);
req.end();
});
}
async function main() {
console.log('👁️ AOAC-08 · 修复监督Agent · 唤醒中...');
console.log(`📅 ${getBeijingTime()}`);
const now = new Date();
const repairReport = readJSON(REPAIR_REPORT_PATH);
if (!repairReport) {
console.log('⚠️ 修复报告不存在,跳过监督');
return;
}
const isSuccess = repairReport.final_result === 'success';
const neededRepair = repairReport.needed_repair;
let verdictAction = {};
if (isSuccess || !neededRepair) {
console.log('✅ 修复成功或无需修复');
// Update Notion receipt to green if there's a page ID
if (repairReport.receipt_check && repairReport.receipt_check.page_id) {
const updated = await updateNotionReceiptGreen(repairReport.receipt_check.page_id);
verdictAction.notion_updated = updated;
}
verdictAction.action = 'close_green';
} else {
console.log('🔴 修复失败 · 触发告警');
// Try email first, then GitHub Issue as fallback
const emailResult = await sendAlertEmail(repairReport);
verdictAction.email = emailResult;
if (!emailResult.sent) {
const issueResult = await createAlertIssue(repairReport);
verdictAction.github_issue = issueResult;
}
verdictAction.action = 'alert_human';
}
// Write supervisor verdict
const verdict = {
aoac_agent: 'AOAC-08',
aoac_agent_name: 'repair-supervisor',
timestamp: getBeijingTime(),
timestamp_utc: now.toISOString(),
repair_id: repairReport.repair_id,
verdict: isSuccess || !neededRepair ? 'green' : 'red',
needed_repair: neededRepair,
repair_result: repairReport.final_result,
action: verdictAction,
chain_closed: isSuccess || !neededRepair
};
writeJSON(VERDICT_PATH, verdict);
console.log(`📝 监督判定: ${verdict.verdict === 'green' ? '🟢' : '🔴'} ${verdict.verdict}`);
// Update chain status
const chainStatus = readJSON(CHAIN_STATUS_PATH) || {};
if (chainStatus.agents && chainStatus.agents['AOAC-08']) {
chainStatus.agents['AOAC-08'].status = 'completed';
chainStatus.agents['AOAC-08'].last_run = now.toISOString();
chainStatus.agents['AOAC-08'].last_success = now.toISOString();
}
if (chainStatus && verdict.chain_closed) {
chainStatus.chain_health = 'healthy';
}
writeJSON(CHAIN_STATUS_PATH, chainStatus);
// Output for workflow
const outputFile = process.env.GITHUB_OUTPUT;
if (outputFile) {
fs.appendFileSync(outputFile, `verdict=${verdict.verdict}\n`);
fs.appendFileSync(outputFile, `chain_closed=${verdict.chain_closed}\n`);
}
console.log(`👁️ AOAC-08 · 监督完成 · 闭环${verdict.chain_closed ? '✅' : '❌'}`);
}
main();