293 lines
9.4 KiB
JavaScript
293 lines
9.4 KiB
JavaScript
#!/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();
|