318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
/**
|
||
* AOAC-07 · 链路修复Agent (Chain Repair Agent)
|
||
* ═══════════════════════════════════════════════
|
||
* AGE OS Agent Chain · 修复Agent(3次自动修复)
|
||
*
|
||
* 唤醒条件:Notion侧RECEIPT_DB打红色标签 / 定时检查
|
||
* 职责:定位断链位置→自动修复→最多3次
|
||
* 输出:data/aoac/repair-report.json
|
||
*
|
||
* 版权:国作登字-2026-A-00037559 · TCS-0002∞
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const https = require('https');
|
||
|
||
const ROOT = path.resolve(__dirname, '..', '..');
|
||
const CHAIN_STATUS_PATH = path.join(ROOT, 'data', 'aoac', 'chain-status.json');
|
||
const SYNC_LOG_PATH = path.join(ROOT, 'data', 'aoac', 'notion-sync-log.json');
|
||
const REPAIR_REPORT_PATH = path.join(ROOT, 'data', 'aoac', 'repair-report.json');
|
||
const HISTORY_DIR = path.join(ROOT, 'data', 'aoac', 'history');
|
||
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
||
|
||
const MAX_REPAIR_ATTEMPTS = 3;
|
||
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 getDateStr() {
|
||
const now = new Date();
|
||
return new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString().slice(0, 10);
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
|
||
function diagnoseChain(chainStatus) {
|
||
const issues = [];
|
||
if (!chainStatus || !chainStatus.agents) {
|
||
issues.push({ agent: 'ALL', issue: 'chain-status.json missing or corrupt', severity: 'critical' });
|
||
return issues;
|
||
}
|
||
|
||
const agentOrder = ['AOAC-01', 'AOAC-02', 'AOAC-03', 'AOAC-04', 'AOAC-05', 'AOAC-06'];
|
||
for (const id of agentOrder) {
|
||
const agent = chainStatus.agents[id];
|
||
if (!agent) {
|
||
issues.push({ agent: id, issue: 'agent config missing', severity: 'warning' });
|
||
continue;
|
||
}
|
||
if (agent.status === 'error') {
|
||
issues.push({ agent: id, issue: `agent in error state`, severity: 'critical' });
|
||
}
|
||
if (!agent.last_run) {
|
||
issues.push({ agent: id, issue: 'never executed', severity: 'info' });
|
||
}
|
||
}
|
||
|
||
// Check sync log
|
||
const syncLog = readJSON(SYNC_LOG_PATH);
|
||
if (syncLog && syncLog.notion_sync && !syncLog.notion_sync.sent) {
|
||
issues.push({
|
||
agent: 'AOAC-06',
|
||
issue: `Notion sync failed: ${syncLog.notion_sync.reason}`,
|
||
severity: 'critical'
|
||
});
|
||
}
|
||
|
||
return issues;
|
||
}
|
||
|
||
function attemptRepair(issues, attempt) {
|
||
const results = [];
|
||
|
||
for (const issue of issues) {
|
||
if (issue.severity === 'info') continue;
|
||
|
||
const result = {
|
||
agent: issue.agent,
|
||
issue: issue.issue,
|
||
attempt,
|
||
action: 'none',
|
||
success: false
|
||
};
|
||
|
||
// Repair strategy: reset agent status
|
||
if (issue.issue.includes('error state')) {
|
||
const chainStatus = readJSON(CHAIN_STATUS_PATH);
|
||
if (chainStatus && chainStatus.agents && chainStatus.agents[issue.agent]) {
|
||
chainStatus.agents[issue.agent].status = 'idle';
|
||
writeJSON(CHAIN_STATUS_PATH, chainStatus);
|
||
result.action = 'reset_status_to_idle';
|
||
result.success = true;
|
||
}
|
||
}
|
||
|
||
// Repair strategy: re-trigger Notion sync on next AOAC-05 cycle
|
||
if (issue.issue.includes('Notion sync failed') && attempt <= 2) {
|
||
// Reset AOAC-06 status to idle so next AOAC-05 cycle will re-trigger it
|
||
const cs = readJSON(CHAIN_STATUS_PATH);
|
||
if (cs && cs.agents && cs.agents['AOAC-06']) {
|
||
cs.agents['AOAC-06'].status = 'idle';
|
||
writeJSON(CHAIN_STATUS_PATH, cs);
|
||
result.action = 'reset_aoac06_for_retry';
|
||
result.success = true;
|
||
} else {
|
||
result.action = 'cannot_reset_aoac06';
|
||
result.success = false;
|
||
}
|
||
}
|
||
|
||
// Repair strategy: recreate missing chain-status from default template
|
||
if (issue.issue.includes('missing or corrupt')) {
|
||
const templatePath = path.join(ROOT, 'data', 'aoac', 'chain-status.json');
|
||
if (!fs.existsSync(templatePath)) {
|
||
// Recreate from scratch
|
||
const defaultStatus = {
|
||
_meta: { system: 'AGE OS Agent Chain (AOAC) v1.0', recreated: new Date().toISOString() },
|
||
agents: {},
|
||
chain_health: 'unknown',
|
||
last_full_cycle: null,
|
||
total_cycles: 0,
|
||
total_repairs: 0
|
||
};
|
||
writeJSON(templatePath, defaultStatus);
|
||
result.action = 'recreated_chain_status';
|
||
result.success = true;
|
||
} else {
|
||
result.action = 'config_exists_no_action';
|
||
result.success = true;
|
||
}
|
||
}
|
||
|
||
results.push(result);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
async function checkNotionReceipt() {
|
||
const notionToken = process.env.ZY_NOTION_TOKEN || process.env.NOTION_TOKEN;
|
||
const receiptDb = process.env.ZY_NOTION_RECEIPT_DB;
|
||
|
||
if (!notionToken || !receiptDb) {
|
||
return { checked: false, reason: 'missing_credentials' };
|
||
}
|
||
|
||
try {
|
||
// Query latest AOAC sync receipts
|
||
const body = {
|
||
filter: {
|
||
property: '类型',
|
||
select: { equals: 'AOAC链路同步' }
|
||
},
|
||
sorts: [{ property: '时间戳', direction: 'descending' }],
|
||
page_size: 5
|
||
};
|
||
|
||
const result = await notionRequest('POST', `/v1/databases/${receiptDb}/query`, body, notionToken);
|
||
if (result && result.results && result.results.length > 0) {
|
||
const latest = result.results[0];
|
||
const status = latest.properties && latest.properties['状态'] &&
|
||
latest.properties['状态'].select ? latest.properties['状态'].select.name : 'unknown';
|
||
return {
|
||
checked: true,
|
||
latest_status: status,
|
||
needs_repair: status === 'failed' || status === 'red' || status === '红色',
|
||
page_id: latest.id
|
||
};
|
||
}
|
||
return { checked: true, latest_status: 'no_records', needs_repair: false };
|
||
} catch (err) {
|
||
return { checked: false, reason: err.message };
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
console.log('🔧 AOAC-07 · 链路修复Agent · 唤醒中...');
|
||
console.log(`📅 ${getBeijingTime()}`);
|
||
|
||
const now = new Date();
|
||
const dateStr = getDateStr();
|
||
const seq = String(Math.floor(Math.random() * 1000)).padStart(3, '0');
|
||
const repairId = `AOAC-REPAIR-${dateStr.replace(/-/g, '')}-${seq}`;
|
||
|
||
// Step 1: Check Notion receipt status
|
||
const receiptCheck = await checkNotionReceipt();
|
||
console.log(`📋 Notion回执检查: ${JSON.stringify(receiptCheck)}`);
|
||
|
||
// Step 2: Diagnose chain
|
||
const chainStatus = readJSON(CHAIN_STATUS_PATH);
|
||
const issues = diagnoseChain(chainStatus);
|
||
console.log(`🔍 诊断发现 ${issues.length} 个问题`);
|
||
|
||
const needsRepair = receiptCheck.needs_repair || issues.some(i => i.severity === 'critical');
|
||
|
||
// Step 3: Attempt repairs (up to 3 times)
|
||
const allRepairResults = [];
|
||
let repairSuccess = false;
|
||
|
||
if (needsRepair) {
|
||
console.log('🔧 开始修复流程...');
|
||
for (let attempt = 1; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
||
console.log(` 第${attempt}次修复尝试...`);
|
||
const results = attemptRepair(issues, attempt);
|
||
allRepairResults.push({ attempt, results });
|
||
|
||
const allFixed = results.every(r => r.success);
|
||
if (allFixed) {
|
||
repairSuccess = true;
|
||
console.log(` ✅ 第${attempt}次修复成功`);
|
||
break;
|
||
}
|
||
console.log(` ⚠️ 第${attempt}次修复部分失败`);
|
||
}
|
||
} else {
|
||
console.log('✅ 链路健康,无需修复');
|
||
repairSuccess = true;
|
||
}
|
||
|
||
// Step 4: Generate repair report
|
||
const repairReport = {
|
||
aoac_agent: 'AOAC-07',
|
||
aoac_agent_name: 'chain-repair-agent',
|
||
repair_id: repairId,
|
||
timestamp: getBeijingTime(),
|
||
timestamp_utc: now.toISOString(),
|
||
receipt_check: receiptCheck,
|
||
diagnosis: {
|
||
total_issues: issues.length,
|
||
critical: issues.filter(i => i.severity === 'critical').length,
|
||
issues
|
||
},
|
||
needed_repair: needsRepair,
|
||
repair_attempts: allRepairResults,
|
||
final_result: repairSuccess ? 'success' : 'failure',
|
||
triggers_next: 'AOAC-08 (repair-supervisor)'
|
||
};
|
||
|
||
writeJSON(REPAIR_REPORT_PATH, repairReport);
|
||
console.log(`📝 修复报告: ${repairId} → ${repairReport.final_result}`);
|
||
|
||
// Archive
|
||
const historyDir = path.join(HISTORY_DIR, dateStr);
|
||
fs.mkdirSync(historyDir, { recursive: true });
|
||
writeJSON(path.join(historyDir, `${repairId}.json`), repairReport);
|
||
|
||
// Update chain status
|
||
if (chainStatus && chainStatus.agents && chainStatus.agents['AOAC-07']) {
|
||
chainStatus.agents['AOAC-07'].status = repairSuccess ? 'completed' : 'error';
|
||
chainStatus.agents['AOAC-07'].last_run = now.toISOString();
|
||
if (repairSuccess) chainStatus.agents['AOAC-07'].last_success = now.toISOString();
|
||
chainStatus.total_repairs = (chainStatus.total_repairs || 0) + (needsRepair ? 1 : 0);
|
||
writeJSON(CHAIN_STATUS_PATH, chainStatus);
|
||
}
|
||
|
||
// Output for workflow
|
||
const outputFile = process.env.GITHUB_OUTPUT;
|
||
if (outputFile) {
|
||
fs.appendFileSync(outputFile, `repair_result=${repairReport.final_result}\n`);
|
||
fs.appendFileSync(outputFile, `needed_repair=${needsRepair}\n`);
|
||
fs.appendFileSync(outputFile, `repair_id=${repairId}\n`);
|
||
}
|
||
|
||
console.log(`🔧 AOAC-07 · 修复Agent完成 → ${repairReport.final_result}`);
|
||
}
|
||
|
||
main();
|