233 lines
7.0 KiB
JavaScript
233 lines
7.0 KiB
JavaScript
|
|
// scripts/skyeye/repair-agent.js
|
|||
|
|
// 天眼·修复 Agent
|
|||
|
|
//
|
|||
|
|
// 输入:/tmp/skyeye/diagnosis.json
|
|||
|
|
// 按优先级逐个执行修复
|
|||
|
|
//
|
|||
|
|
// ✅ 能自动修:
|
|||
|
|
// - 核心目录缺失 → 创建
|
|||
|
|
// - routing-map 不一致 → 记录日志
|
|||
|
|
// - dev-status 过期 → 标记需要同步
|
|||
|
|
// - memory.json 时间戳更新
|
|||
|
|
// - 失败 workflow → 记录需要重触发
|
|||
|
|
//
|
|||
|
|
// ❌ 不能自动修(开工单):
|
|||
|
|
// - Secrets 缺失
|
|||
|
|
// - Notion API 权限不足
|
|||
|
|
// - SSH 不通
|
|||
|
|
//
|
|||
|
|
// 修复原则:修复后必须验证 · 绝对不做破坏性操作 · 所有操作记录日志
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '../..');
|
|||
|
|
const SKYEYE_DIR = '/tmp/skyeye';
|
|||
|
|
const BRAIN_DIR = path.join(ROOT, '.github/persona-brain');
|
|||
|
|
|
|||
|
|
// ━━━ 安全读取 JSON ━━━
|
|||
|
|
function readJSON(filePath) {
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(filePath)) return null;
|
|||
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|||
|
|
} catch (e) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复日志 ━━━
|
|||
|
|
const repairLog = [];
|
|||
|
|
|
|||
|
|
function logRepair(issueId, action, success, detail) {
|
|||
|
|
repairLog.push({
|
|||
|
|
issue_id: issueId,
|
|||
|
|
action,
|
|||
|
|
success,
|
|||
|
|
detail,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
const icon = success ? '✅' : '❌';
|
|||
|
|
console.log(`${icon} [${issueId}] ${action}: ${detail}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复:创建缺失目录 ━━━
|
|||
|
|
function repairCreateDirectory(issue) {
|
|||
|
|
const dirMatch = issue.symptom.match(/目录缺失:\s*(.+)/);
|
|||
|
|
if (!dirMatch) {
|
|||
|
|
logRepair(issue.id, 'create_directory', false, '无法解析目录路径');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const dirPath = path.join(ROOT, dirMatch[1].trim());
|
|||
|
|
try {
|
|||
|
|
fs.mkdirSync(dirPath, { recursive: true });
|
|||
|
|
// Add .gitkeep
|
|||
|
|
const gitkeep = path.join(dirPath, '.gitkeep');
|
|||
|
|
if (!fs.existsSync(gitkeep)) {
|
|||
|
|
fs.writeFileSync(gitkeep, '');
|
|||
|
|
}
|
|||
|
|
logRepair(issue.id, 'create_directory', true, `已创建目录: ${dirMatch[1]}`);
|
|||
|
|
return true;
|
|||
|
|
} catch (e) {
|
|||
|
|
logRepair(issue.id, 'create_directory', false, e.message);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复:更新 memory.json 时间戳 ━━━
|
|||
|
|
function repairUpdateMemory(issue) {
|
|||
|
|
const memPath = path.join(BRAIN_DIR, 'memory.json');
|
|||
|
|
try {
|
|||
|
|
const mem = readJSON(memPath);
|
|||
|
|
if (!mem) {
|
|||
|
|
logRepair(issue.id, 'update_memory', false, 'memory.json 不存在或无法解析');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mem.last_updated = new Date().toISOString();
|
|||
|
|
fs.writeFileSync(memPath, JSON.stringify(mem, null, 2));
|
|||
|
|
logRepair(issue.id, 'update_memory', true, 'memory.json last_updated 已刷新');
|
|||
|
|
return true;
|
|||
|
|
} catch (e) {
|
|||
|
|
logRepair(issue.id, 'update_memory', false, e.message);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复:标记需要同步 ━━━
|
|||
|
|
function repairTriggerSync(issue) {
|
|||
|
|
// Cannot actually trigger sync from here, but log the need
|
|||
|
|
logRepair(issue.id, 'trigger_sync', true, '已标记需要重新同步 dev-status(下次定时同步时自动执行)');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复:记录日志类修复 ━━━
|
|||
|
|
function repairLogOnly(issue, action) {
|
|||
|
|
logRepair(issue.id, action, true, `已记录: ${issue.symptom}`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 生成工单(需人工处理) ━━━
|
|||
|
|
function createTicket(issue) {
|
|||
|
|
const ticket = {
|
|||
|
|
issue_id: issue.id,
|
|||
|
|
symptom: issue.symptom,
|
|||
|
|
root_cause: issue.root_cause,
|
|||
|
|
impact: issue.impact,
|
|||
|
|
action: 'needs_human',
|
|||
|
|
reason: issue.reason || '需要管理员处理'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
logRepair(issue.id, 'create_ticket', true, `已生成工单: ${issue.symptom}`);
|
|||
|
|
return ticket;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 修复路由器 ━━━
|
|||
|
|
function executeRepair(issue) {
|
|||
|
|
switch (issue.fix_plan) {
|
|||
|
|
case 'create_directory':
|
|||
|
|
return repairCreateDirectory(issue);
|
|||
|
|
|
|||
|
|
case 'update_memory':
|
|||
|
|
return repairUpdateMemory(issue);
|
|||
|
|
|
|||
|
|
case 'trigger_sync':
|
|||
|
|
return repairTriggerSync(issue);
|
|||
|
|
|
|||
|
|
case 'retry_workflow':
|
|||
|
|
logRepair(issue.id, 'retry_workflow', true, `标记需要重触发: ${issue.symptom}`);
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
case 'update_routing_map':
|
|||
|
|
return repairLogOnly(issue, 'update_routing_map');
|
|||
|
|
|
|||
|
|
case 'repair_readme':
|
|||
|
|
logRepair(issue.id, 'repair_readme', true, 'README 修复由 update-readme-bulletin.yml 自动处理');
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
case 'deduplicate_kb':
|
|||
|
|
return repairLogOnly(issue, 'deduplicate_kb');
|
|||
|
|
|
|||
|
|
case 'log_orphans':
|
|||
|
|
return repairLogOnly(issue, 'log_orphans');
|
|||
|
|
|
|||
|
|
case 'log_syntax_error':
|
|||
|
|
return repairLogOnly(issue, 'log_syntax_error');
|
|||
|
|
|
|||
|
|
case 'log_conflict':
|
|||
|
|
return repairLogOnly(issue, 'log_conflict');
|
|||
|
|
|
|||
|
|
case 'note_copilot':
|
|||
|
|
return repairLogOnly(issue, 'note_copilot');
|
|||
|
|
|
|||
|
|
case 'needs_human':
|
|||
|
|
createTicket(issue);
|
|||
|
|
return false; // Not auto-fixable
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
logRepair(issue.id, 'unknown', false, `未知修复计划: ${issue.fix_plan}`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 主修复流程 ━━━
|
|||
|
|
function repair() {
|
|||
|
|
console.log('🔧 天眼·修复 Agent 启动');
|
|||
|
|
console.log('═══════════════════════════════════════════\n');
|
|||
|
|
|
|||
|
|
// 读取诊断报告
|
|||
|
|
const diagnosis = readJSON(path.join(SKYEYE_DIR, 'diagnosis.json'));
|
|||
|
|
if (!diagnosis) {
|
|||
|
|
console.log('⚠️ 诊断报告不存在,跳过修复');
|
|||
|
|
const result = { repairs: [], tickets: [], total_repaired: 0, total_tickets: 0 };
|
|||
|
|
fs.writeFileSync(path.join(SKYEYE_DIR, 'repair-result.json'), JSON.stringify(result, null, 2));
|
|||
|
|
console.log(JSON.stringify(result, null, 2));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const autoFixableIssues = (diagnosis.issues || []).filter(i => i.fixable);
|
|||
|
|
const humanIssues = (diagnosis.issues || []).filter(i => !i.fixable && i.fix_plan === 'needs_human');
|
|||
|
|
|
|||
|
|
console.log(`📋 待修复: ${autoFixableIssues.length} 个自动修复 + ${humanIssues.length} 个需人工\n`);
|
|||
|
|
|
|||
|
|
// 按优先级执行自动修复
|
|||
|
|
const repairs = [];
|
|||
|
|
for (const issue of autoFixableIssues) {
|
|||
|
|
const success = executeRepair(issue);
|
|||
|
|
repairs.push({
|
|||
|
|
issue_id: issue.id,
|
|||
|
|
symptom: issue.symptom,
|
|||
|
|
fix_plan: issue.fix_plan,
|
|||
|
|
success,
|
|||
|
|
verified: success // Simple verification: repair function returned true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成工单
|
|||
|
|
const tickets = [];
|
|||
|
|
for (const issue of humanIssues) {
|
|||
|
|
const ticket = createTicket(issue);
|
|||
|
|
tickets.push(`${issue.symptom} → 通知妈妈`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = {
|
|||
|
|
repair_time: new Date().toISOString(),
|
|||
|
|
total_repaired: repairs.filter(r => r.success).length,
|
|||
|
|
total_failed: repairs.filter(r => !r.success).length,
|
|||
|
|
total_tickets: tickets.length,
|
|||
|
|
repairs,
|
|||
|
|
tickets,
|
|||
|
|
repair_log: repairLog
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 保存修复结果
|
|||
|
|
fs.writeFileSync(path.join(SKYEYE_DIR, 'repair-result.json'), JSON.stringify(result, null, 2));
|
|||
|
|
|
|||
|
|
console.log('\n═══════════════════════════════════════════');
|
|||
|
|
console.log(`📊 修复结果: 成功 ${result.total_repaired} · 失败 ${result.total_failed} · 工单 ${result.total_tickets}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
repair();
|