zhizhi/scripts/aoac/chain-repair-agent.js

318 lines
10 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
'use strict';
/**
* AOAC-07 · 链路修复Agent (Chain Repair Agent)
*
* AGE OS Agent Chain · 修复Agent3次自动修复
*
* 唤醒条件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();