guanghulab/scripts/aoac/repair-supervisor.js

293 lines
9.4 KiB
JavaScript
Raw Normal View History

#!/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();