guanghulab/scripts/deputy-auto-repair.js

550 lines
18 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// scripts/deputy-auto-repair.js
// 🔧 铸渊副将自动修复引擎 v1.0
//
// 核心理念 (冰朔第三十一次对话):
// 副将在铸渊休眠时自动处理系统报错
// 用铸渊配置的思维逻辑和推理能力
// 看到报错日志后自动运行修复
// 最多修复3次 · 仍失败则推送给人类干预
//
// 修复流程:
// 1. 读取部署日志 → 分析错误
// 2. 匹配已知修复方案 (经验数据库)
// 3. SSH到服务器执行修复命令
// 4. 验证修复结果 (健康检查)
// 5. 如需要 → 调用LLM深度推理
// 6. 记录修复过程到经验库
//
// 环境变量:
// GH_TOKEN — GitHub API
// ZY_SERVER_HOST, ZY_SERVER_USER — SSH访问
// ZY_LLM_API_KEY, ZY_LLM_BASE_URL — LLM深度推理
// LOG_FILE — 部署日志文件路径
// RUN_ID — 触发的工作流运行ID
// ═══════════════════════════════════════════════
'use strict';
const { execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const ROOT = path.resolve(__dirname, '..');
const LOGS_DIR = path.join(ROOT, 'data/deploy-logs');
const REPAIR_HISTORY = path.join(LOGS_DIR, 'repair-history.json');
const INDEX_FILE = path.join(LOGS_DIR, 'latest-index.json');
const EXPERIENCE_DB = path.join(ROOT, 'brain/dev-experience/experience-db.json');
const MAX_REPAIR_ATTEMPTS = 3;
// ── 辅助:判断是否需要人工干预 ─────────────────
function shouldRequestHuman(repairSuccess, attemptNumber) {
return !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
}
// ── 修复策略库 (副将的思维逻辑) ────────────────
const REPAIR_STRATEGIES = [
{
id: 'REPAIR-PM2-RESTART',
name: 'PM2进程重启',
triggers: ['EADDRINUSE', 'ECONNREFUSED', 'HEALTH_FAIL', '502 Bad Gateway', 'pm2.*error'],
commands: [
'pm2 delete all 2>/dev/null || true',
'sleep 2',
'pm2 start /opt/zhuyuan/config/pm2/ecosystem.config.js',
'pm2 save',
'sleep 5'
],
verify: 'curl -sf http://localhost:3800/api/health || exit 1',
severity: 'medium',
success_rate: '高 · PM2重启通常能解决进程级问题'
},
{
id: 'REPAIR-NPM-INSTALL',
name: 'Node依赖重装',
triggers: ['MODULE_NOT_FOUND', 'npm ERR!', 'Cannot find module'],
commands: [
'cd /opt/zhuyuan/app',
'rm -rf node_modules',
'npm install --production 2>&1 | tail -20',
'pm2 restart all',
'sleep 5'
],
verify: 'curl -sf http://localhost:3800/api/health || exit 1',
severity: 'medium',
success_rate: '高 · 依赖问题通过重装通常能解决'
},
{
id: 'REPAIR-NGINX-RELOAD',
name: 'Nginx配置重载',
triggers: ['nginx.*failed', 'nginx.*error', '502 Bad Gateway'],
commands: [
'sudo nginx -t 2>&1',
'sudo systemctl reload nginx 2>&1 || sudo systemctl restart nginx 2>&1',
'sleep 3'
],
verify: 'curl -sf http://localhost:80/ -o /dev/null || exit 1',
severity: 'medium',
success_rate: '中 · 取决于配置文件是否正确'
},
{
id: 'REPAIR-DISK-CLEANUP',
name: '磁盘空间清理',
triggers: ['ENOSPC', '磁盘空间不足'],
commands: [
'sudo journalctl --vacuum-time=2d 2>/dev/null || true',
'pm2 flush 2>/dev/null || true',
'sudo find /tmp -type f -mtime +1 -delete 2>/dev/null || true',
'df -h /'
],
verify: 'USED=$(df / | tail -1 | awk \'{print $5}\' | tr -d \'%\'); [ "$USED" -lt 90 ] || exit 1',
severity: 'high',
success_rate: '中 · 取决于可清理的文件量'
},
{
id: 'REPAIR-MEMORY-FREE',
name: '内存释放',
triggers: ['ENOMEM', '内存不足'],
commands: [
'pm2 delete all 2>/dev/null || true',
'sync && echo 3 | sudo tee /proc/sys/vm/drop_caches',
'sleep 3',
'pm2 start /opt/zhuyuan/config/pm2/ecosystem.config.js',
'pm2 save',
'sleep 5',
'free -m'
],
verify: 'curl -sf http://localhost:3800/api/health || exit 1',
severity: 'high',
success_rate: '中 · 重启进程通常能释放泄漏的内存'
}
];
// ── SSH命令执行 (安全化·使用execFileSync避免shell注入) ────
function sshExec(command) {
const host = process.env.ZY_SERVER_HOST;
const user = process.env.ZY_SERVER_USER;
if (!host || !user) {
throw new Error('SSH配置缺失 (ZY_SERVER_HOST/ZY_SERVER_USER)');
}
// 安全检查:命令必须通过危险命令检测
if (isDangerousCommand(command)) {
return { ok: false, output: '', error: '⛔ 命令被安全策略拦截' };
}
const sshKeyPath = path.join(process.env.HOME || '/root', '.ssh', 'zy_key');
try {
// 使用execFileSync直接调用ssh二进制文件避免shell解析
// command作为ssh的远程执行参数由ssh在远端服务器上执行
const output = execFileSync('ssh', [
'-i', sshKeyPath,
'-o', 'ConnectTimeout=10',
`${user}@${host}`,
command
], {
timeout: 60000,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
return { ok: true, output: output.trim() };
} catch (err) {
return {
ok: false,
output: err.stdout ? err.stdout.trim() : '',
error: err.stderr ? err.stderr.trim() : err.message
};
}
}
// ── LLM深度分析 ────────────────────────────
async function llmAnalysis(errorLog) {
const apiKey = process.env.ZY_LLM_API_KEY;
const baseUrl = process.env.ZY_LLM_BASE_URL;
if (!apiKey || !baseUrl) {
return { ok: false, analysis: 'LLM API未配置' };
}
const prompt = `你是铸渊副将(ZY-DEPUTY-001),光湖系统的自动化运维代理。
以下是部署失败的日志请分析根因并给出可在服务器上执行的修复命令
## 日志:
\`\`\`
${errorLog.slice(0, 3000)}
\`\`\`
请用JSON格式回答:
{
"root_cause": "一句话根因",
"severity": "low/medium/high/critical",
"fix_commands": ["命令1", "命令2"],
"verify_command": "验证修复的命令",
"needs_human": true/false,
"explanation": "详细解释"
}`;
try {
const urlObj = new URL(`${baseUrl}/chat/completions`);
const isHttps = urlObj.protocol === 'https:';
const requestModule = isHttps ? https : http;
const body = JSON.stringify({
model: process.env.ZY_LLM_MODEL || 'deepseek-chat',
messages: [
{ role: 'system', content: '你是铸渊副将精通Linux服务器运维、Node.js、Nginx、PM2。返回纯JSON格式。' },
{ role: 'user', content: prompt }
],
temperature: 0.2,
max_tokens: 1000
});
const result = await new Promise((resolve, reject) => {
const req = requestModule.request({
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(body)
},
timeout: 30000
}, (res) => {
let data = '';
res.on('data', (d) => { data += d; });
res.on('end', () => {
try {
const json = JSON.parse(data);
const content = json.choices?.[0]?.message?.content || '';
// 尝试提取JSON部分
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
resolve(JSON.parse(jsonMatch[0]));
} else {
resolve({ root_cause: content.slice(0, 200), fix_commands: [], needs_human: true });
}
} catch {
resolve({ root_cause: 'LLM响应解析失败', fix_commands: [], needs_human: true });
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('LLM API超时')); });
req.write(body);
req.end();
});
return { ok: true, analysis: result };
} catch (err) {
return { ok: false, analysis: `LLM调用失败: ${err.message}` };
}
}
// ── 修复主流程 ────────────────────────────
async function repair() {
console.log('🔧 铸渊副将自动修复引擎 v1.0');
console.log('═'.repeat(60));
const logFile = process.env.LOG_FILE;
const runId = process.env.RUN_ID;
const conclusion = process.env.DEPLOY_CONCLUSION;
const workflowName = process.env.WORKFLOW_NAME;
// §1 读取部署日志
let logData;
if (logFile) {
const logPath = path.join(ROOT, logFile);
try {
logData = JSON.parse(fs.readFileSync(logPath, 'utf8'));
console.log(`📋 读取日志: ${logFile}`);
} catch (err) {
console.error(`❌ 无法读取日志文件: ${err.message}`);
}
}
// §2 加载修复历史
let history;
try {
history = JSON.parse(fs.readFileSync(REPAIR_HISTORY, 'utf8'));
} catch {
history = { repairs: [] };
}
// 检查此运行ID的修复次数
const existingRepairs = history.repairs.filter(r => r.run_id === runId);
const attemptNumber = existingRepairs.length + 1;
if (attemptNumber > MAX_REPAIR_ATTEMPTS) {
console.log(`❌ 已达最大修复次数(${MAX_REPAIR_ATTEMPTS}) · 放弃修复 · 通知人类`);
setOutput('repair_success', 'false');
setOutput('repair_attempt', String(attemptNumber - 1));
setOutput('needs_human', 'true');
return;
}
console.log(`🔧 修复尝试 #${attemptNumber}/${MAX_REPAIR_ATTEMPTS}`);
console.log('');
// §3 分析错误并选择修复策略
const errorContent = logData
? logData.failed_jobs?.map(j => j.log_content).join('\n') || ''
: '';
let selectedStrategies = [];
// 先用模式匹配选择策略
for (const strategy of REPAIR_STRATEGIES) {
for (const trigger of strategy.triggers) {
const regex = new RegExp(trigger, 'i');
if (regex.test(errorContent) || regex.test(conclusion || '')) {
if (!selectedStrategies.find(s => s.id === strategy.id)) {
selectedStrategies.push(strategy);
}
}
}
}
// 如果没匹配到策略默认用PM2重启
if (selectedStrategies.length === 0) {
console.log(' 未匹配到具体修复策略 · 使用默认修复(PM2重启)');
selectedStrategies = [REPAIR_STRATEGIES[0]]; // PM2重启是通用修复
}
console.log(`📋 选择 ${selectedStrategies.length} 个修复策略:`);
for (const s of selectedStrategies) {
console.log(` - ${s.name} (${s.id})`);
}
console.log('');
// §4 执行修复
let repairSuccess = false;
const repairRecord = {
run_id: runId,
workflow: workflowName,
attempt: attemptNumber,
timestamp: new Date().toISOString(),
strategies_applied: [],
results: [],
final_success: false
};
for (const strategy of selectedStrategies) {
console.log(`🔧 执行修复策略: ${strategy.name}`);
console.log('─'.repeat(40));
const strategyResult = {
strategy_id: strategy.id,
strategy_name: strategy.name,
commands_executed: [],
verify_result: null,
success: false
};
// 执行修复命令
let allCommandsOk = true;
for (const cmd of strategy.commands) {
console.log(` $ ${cmd}`);
const result = sshExec(cmd);
strategyResult.commands_executed.push({
command: cmd,
ok: result.ok,
output: result.output?.slice(0, 500),
error: result.error?.slice(0, 300)
});
if (result.output) {
console.log(` ${result.output.split('\n').slice(0, 3).join('\n ')}`);
}
if (!result.ok) {
console.log(` ⚠️ 命令执行异常: ${result.error?.slice(0, 100)}`);
// 不中断,继续执行其他命令
}
}
// 验证修复结果
console.log(` 🔍 验证: ${strategy.verify}`);
const verifyResult = sshExec(strategy.verify);
strategyResult.verify_result = {
ok: verifyResult.ok,
output: verifyResult.output?.slice(0, 300)
};
if (verifyResult.ok) {
console.log(' ✅ 验证通过 · 修复成功');
strategyResult.success = true;
repairSuccess = true;
} else {
console.log(' ❌ 验证失败 · 修复未成功');
}
repairRecord.strategies_applied.push(strategy.id);
repairRecord.results.push(strategyResult);
console.log('');
if (repairSuccess) break;
}
// §5 如果简单修复失败尝试LLM深度分析
if (!repairSuccess && attemptNumber >= 2) {
console.log('🧠 简单修复失败 · 启动LLM深度推理...');
const llmResult = await llmAnalysis(errorContent);
if (llmResult.ok && llmResult.analysis) {
const analysis = llmResult.analysis;
console.log(` 📝 根因: ${analysis.root_cause || 'N/A'}`);
console.log(` ⚠️ 严重程度: ${analysis.severity || 'N/A'}`);
console.log(` 🔧 修复命令: ${(analysis.fix_commands || []).length}`);
console.log(` 👤 需要人工: ${analysis.needs_human ? '是' : '否'}`);
// 如果LLM建议的修复命令看起来安全执行它们
if (!analysis.needs_human && analysis.fix_commands && analysis.fix_commands.length > 0) {
console.log('');
console.log('🤖 执行LLM建议的修复命令:');
for (const cmd of analysis.fix_commands) {
// 安全检查:不执行危险命令
if (isDangerousCommand(cmd)) {
console.log(` ⛔ 跳过危险命令: ${cmd}`);
continue;
}
console.log(` $ ${cmd}`);
const result = sshExec(cmd);
if (result.output) {
console.log(` ${result.output.split('\n').slice(0, 3).join('\n ')}`);
}
}
// 验证
if (analysis.verify_command && !isDangerousCommand(analysis.verify_command)) {
const verifyResult = sshExec(analysis.verify_command);
if (verifyResult.ok) {
console.log(' ✅ LLM修复方案验证通过');
repairSuccess = true;
}
}
}
repairRecord.llm_analysis = {
root_cause: analysis.root_cause,
severity: analysis.severity,
needs_human: analysis.needs_human,
commands_count: (analysis.fix_commands || []).length
};
} else {
console.log(` ⚠️ LLM分析失败: ${llmResult.analysis}`);
}
}
// §6 记录修复结果
repairRecord.final_success = repairSuccess;
history.repairs.push(repairRecord);
// 保留最近50条修复记录
history.repairs = history.repairs.slice(-50);
fs.writeFileSync(REPAIR_HISTORY, JSON.stringify(history, null, 2) + '\n');
// 更新日志索引
try {
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf8'));
index.stats.repair_attempted = (index.stats.repair_attempted || 0) + 1;
if (repairSuccess) {
index.stats.repair_success = (index.stats.repair_success || 0) + 1;
}
index.last_updated = new Date().toISOString();
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2) + '\n');
} catch (err) {
console.log(`⚠️ 索引更新失败: ${err.message}`);
}
// §7 设置输出
const needsHuman = shouldRequestHuman(repairSuccess, attemptNumber);
setOutput('repair_success', repairSuccess ? 'true' : 'false');
setOutput('repair_attempt', String(attemptNumber));
setOutput('needs_human', needsHuman ? 'true' : 'false');
console.log('═'.repeat(60));
if (repairSuccess) {
console.log(`✅ 修复成功 · 第${attemptNumber}次尝试`);
} else if (needsHuman) {
console.log(`${MAX_REPAIR_ATTEMPTS}次修复均失败 · 需要人工干预`);
} else {
console.log(`❌ 第${attemptNumber}次修复失败 · 还有${MAX_REPAIR_ATTEMPTS - attemptNumber}次尝试机会 · 等待下次重试`);
}
}
// ── 危险命令检测 ────────────────────────────
function isDangerousCommand(cmd) {
const dangerous = [
/rm\s+-r[fF]*\s+(\/|~|\$HOME)/, // rm -rf / or ~ or $HOME
/mkfs/, // 格式化
/dd\s+if=/, // 磁盘写入
/shutdown/, // 关机
/reboot/, // 重启系统
/init\s+0/, // 关机
/:\(\)\{/, // fork bomb
/>\s*\/dev\/sd/, // 直接写磁盘设备
/chmod\s+-R\s+777\s+\//, // 全局777
/userdel/, // 删除用户
/passwd/, // 修改密码
/rm\s+-r[fF]*\s+\/\w/, // rm -rf /home etc
];
return dangerous.some(regex => regex.test(cmd));
}
// ── 工具函数 ────────────────────────────────
function setOutput(key, value) {
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
}
}
function parseArgs() {
const args = process.argv.slice(2);
const cmd = args[0];
const params = {};
for (let i = 1; i < args.length; i++) {
if (args[i].startsWith('--')) {
params[args[i].slice(2)] = args[i + 1] || '';
i++;
}
}
return { cmd, ...params };
}
// ── 主入口 ──────────────────────────────────
async function main() {
const args = parseArgs();
switch (args.cmd) {
case 'repair':
await repair();
break;
default:
console.log('🔧 铸渊副将自动修复引擎 v1.0');
console.log('');
console.log('用法:');
console.log(' node deputy-auto-repair.js repair');
console.log('');
console.log('环境变量:');
console.log(' LOG_FILE — 部署日志文件路径');
console.log(' RUN_ID — 工作流运行ID');
console.log(' ZY_SERVER_HOST/ZY_SERVER_USER — SSH配置');
console.log(' ZY_LLM_API_KEY/ZY_LLM_BASE_URL — LLM API');
break;
}
}
main().catch(err => {
console.error(`❌ 自动修复引擎错误: ${err.message}`);
process.exit(1);
});