550 lines
18 KiB
JavaScript
550 lines
18 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
});
|