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();
|