guanghulab/server/proxy/service/proxy-guardian.js

365 lines
12 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/proxy-guardian.js
// 🛡️ 铸渊专线 · 代理守护Agent
//
// 自动处理代理服务的各种异常:
// - Xray进程监控 (崩溃自动重启)
// - 端口可达性检测
// - IP声誉检查
// - TLS指纹一致性
// - 异常流量模式检测
//
// 遇到无法自动解决的问题时:
// - 调用LLM API进行推理分析
// - 生成诊断报告
// - 通过邮件发送告警
//
// 运行方式: PM2 managed (zy-proxy-guardian)
// 检查间隔: 每10分钟
// ═══════════════════════════════════════════════
'use strict';
const { execSync, execFileSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan-brain/proxy/data';
const LOG_DIR = process.env.ZY_PROXY_LOG_DIR || '/opt/zhuyuan-brain/proxy/logs';
const GUARDIAN_FILE = path.join(DATA_DIR, 'guardian-status.json');
const CHECK_INTERVAL = 10 * 60 * 1000; // 10分钟
// ── 检查项定义 ───────────────────────────────
const CHECKS = {
xray_process: '检查Xray进程是否运行',
port_443: '检查443端口是否可达',
subscription_service: '检查订阅服务是否响应',
disk_space: '检查磁盘空间',
memory_usage: '检查内存使用',
xray_errors: '检查Xray错误日志'
};
// ── 读取守护状态 ─────────────────────────────
function readGuardianStatus() {
try {
return JSON.parse(fs.readFileSync(GUARDIAN_FILE, 'utf8'));
} catch {
return {
last_check: null,
consecutive_failures: 0,
auto_fixes: 0,
llm_consultations: 0,
last_issue: null,
status: 'initializing'
};
}
}
// ── 保存守护状态 ─────────────────────────────
function saveGuardianStatus(status) {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
fs.writeFileSync(GUARDIAN_FILE, JSON.stringify(status, null, 2));
}
// ── 执行命令 ─────────────────────────────────
function runCmd(cmd, timeout = 10000) {
try {
return { ok: true, output: execSync(cmd, { encoding: 'utf8', timeout }).trim() };
} catch (err) {
return { ok: false, output: err.message };
}
}
// ── 检查Xray进程 ────────────────────────────
function checkXrayProcess() {
const result = runCmd('pgrep -x xray');
if (result.ok && result.output) {
return { ok: true, detail: `PID: ${result.output}` };
}
return { ok: false, detail: 'Xray进程未运行', fixable: true };
}
// ── 检查端口 ─────────────────────────────────
function checkPort443() {
const result = runCmd('ss -tlnp | grep ":443 "');
if (result.ok && result.output.includes('443')) {
return { ok: true, detail: '443端口正在监听' };
}
return { ok: false, detail: '443端口未监听', fixable: true };
}
// ── 检查订阅服务 ─────────────────────────────
function checkSubscriptionService() {
return new Promise((resolve) => {
const req = http.get('http://127.0.0.1:3803/health', { timeout: 5000 }, (res) => {
let data = '';
res.on('data', (d) => { data += d; });
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve({ ok: json.status === 'ok', detail: '订阅服务正常' });
} catch {
resolve({ ok: false, detail: '订阅服务响应异常', fixable: true });
}
});
});
req.on('error', () => {
resolve({ ok: false, detail: '订阅服务无法连接', fixable: true });
});
req.on('timeout', () => {
req.destroy();
resolve({ ok: false, detail: '订阅服务响应超时', fixable: true });
});
});
}
// ── 检查磁盘空间 ─────────────────────────────
function checkDiskSpace() {
const result = runCmd("df -h / | tail -1 | awk '{print $5}'");
if (result.ok) {
const usage = parseInt(result.output.replace('%', ''), 10);
if (usage < 90) {
return { ok: true, detail: `磁盘使用: ${usage}%` };
}
return { ok: false, detail: `磁盘使用过高: ${usage}%`, fixable: false };
}
return { ok: false, detail: '无法检查磁盘空间', fixable: false };
}
// ── 检查内存使用 ─────────────────────────────
function checkMemory() {
const result = runCmd("free -m | grep Mem | awk '{print int($3/$2*100)}'");
if (result.ok) {
const usage = parseInt(result.output, 10);
if (usage < 90) {
return { ok: true, detail: `内存使用: ${usage}%` };
}
return { ok: false, detail: `内存使用过高: ${usage}%`, fixable: false };
}
return { ok: false, detail: '无法检查内存', fixable: false };
}
// ── 检查Xray错误日志 ────────────────────────
function checkXrayErrors() {
const errorLog = path.join(LOG_DIR, 'error.log');
try {
if (!fs.existsSync(errorLog)) {
return { ok: true, detail: '无错误日志' };
}
const stat = fs.statSync(errorLog);
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
if (stat.mtimeMs < fiveMinAgo) {
return { ok: true, detail: '最近5分钟无新错误' };
}
const lines = fs.readFileSync(errorLog, 'utf8').split('\n').slice(-10);
const criticalErrors = lines.filter(l =>
l.includes('failed') || l.includes('error') || l.includes('panic')
);
if (criticalErrors.length === 0) {
return { ok: true, detail: '无严重错误' };
}
return {
ok: false,
detail: `发现${criticalErrors.length}个错误: ${criticalErrors[0].slice(0, 100)}`,
fixable: false,
errors: criticalErrors
};
} catch {
return { ok: true, detail: '错误日志检查跳过' };
}
}
// ── 自动修复 ─────────────────────────────────
function autoFix(checkName) {
console.log(`[守护Agent] 尝试自动修复: ${checkName}`);
switch (checkName) {
case 'xray_process':
case 'port_443':
// 重启Xray
const restartResult = runCmd('systemctl restart xray', 30000);
if (restartResult.ok) {
console.log('[守护Agent] Xray已重启');
return true;
}
console.error('[守护Agent] Xray重启失败:', restartResult.output);
return false;
case 'subscription_service':
// 通过PM2重启订阅服务
const pmResult = runCmd('pm2 restart zy-proxy-v2-sub', 15000);
if (pmResult.ok) {
console.log('[守护Agent] 订阅服务已重启');
return true;
}
return false;
default:
return false;
}
}
// ── 调用LLM推理 (V3: 使用llm-router动态路由) ──
async function consultLLM(issue) {
let llmRouter;
try {
llmRouter = require('./llm-router');
} catch {
console.log('[守护Agent] LLM路由器未加载跳过推理');
return null;
}
const prompt = `你是光湖语言世界VPN的守护Agent。以下是当前检测到的问题:
问题描述: ${issue.detail}
检查项: ${issue.checkName}
连续失败次数: ${issue.consecutiveFailures}
自动修复尝试: ${issue.autoFixAttempted ? '已尝试,失败' : '未尝试'}
请分析可能的原因并给出解决建议回答要简洁明确`;
const result = await llmRouter.callLLM(prompt, {
systemPrompt: '你是光湖语言世界VPN系统的守护Agent AI。负责诊断代理服务异常并给出修复建议。',
maxTokens: 500,
timeout: 30000
});
return result ? result.content : null;
}
// ── 发送告警邮件 ─────────────────────────────
function sendAlertEmail(subject, body) {
try {
const sendScript = path.join(__dirname, 'send-subscription.js');
// 使用execFileSync避免Shell命令注入 (不经过shell解释器)
execFileSync('node', [sendScript, 'alert', `${subject}\n\n${body}`], {
encoding: 'utf8',
timeout: 30000
});
console.log('[守护Agent] 告警邮件已发送');
} catch (err) {
console.error('[守护Agent] 邮件发送失败:', err.message);
}
}
// ── 主巡检 ───────────────────────────────────
async function patrol() {
console.log('[守护Agent] 开始巡检...');
const status = readGuardianStatus();
const results = {};
let hasIssue = false;
// 执行所有检查
results.xray_process = checkXrayProcess();
results.port_443 = checkPort443();
results.subscription_service = await checkSubscriptionService();
results.disk_space = checkDiskSpace();
results.memory_usage = checkMemory();
results.xray_errors = checkXrayErrors();
// 分析结果
const issues = [];
for (const [name, result] of Object.entries(results)) {
if (!result.ok) {
hasIssue = true;
issues.push({ checkName: name, ...result });
// 尝试自动修复
if (result.fixable) {
const fixed = autoFix(name);
if (fixed) {
status.auto_fixes++;
result.auto_fixed = true;
console.log(`[守护Agent] ✅ 自动修复成功: ${name}`);
} else {
result.auto_fixed = false;
}
}
}
}
// 更新状态
status.last_check = new Date().toISOString();
status.checks = Object.fromEntries(
Object.entries(results).map(([k, v]) => [k, { ok: v.ok, detail: v.detail }])
);
if (hasIssue) {
status.consecutive_failures++;
status.last_issue = {
time: new Date().toISOString(),
issues: issues.map(i => ({ check: i.checkName, detail: i.detail }))
};
// 连续3次失败且有未修复的问题 → 调用LLM
const unfixedIssues = issues.filter(i => !i.auto_fixed && i.fixable !== false);
if (status.consecutive_failures >= 3 && unfixedIssues.length > 0) {
console.log('[守护Agent] 连续失败3次调用LLM推理...');
for (const issue of unfixedIssues) {
const llmAdvice = await consultLLM({
...issue,
consecutiveFailures: status.consecutive_failures,
autoFixAttempted: true
});
if (llmAdvice) {
status.llm_consultations++;
console.log(`[守护Agent] LLM建议: ${llmAdvice.slice(0, 200)}`);
// 发送告警邮件含LLM分析
sendAlertEmail(
`🛡️ 铸渊专线告警: ${issue.checkName}`,
`问题: ${issue.detail}\n\n🤖 AI分析:\n${llmAdvice}`
);
}
}
}
// 首次失败也发送告警
if (status.consecutive_failures === 1) {
sendAlertEmail(
'🛡️ 铸渊专线异常检测',
issues.map(i => `- ${i.checkName}: ${i.detail}`).join('\n')
);
}
} else {
if (status.consecutive_failures > 0) {
console.log(`[守护Agent] ✅ 问题已恢复 (之前连续失败${status.consecutive_failures}次)`);
}
status.consecutive_failures = 0;
status.status = 'healthy';
}
saveGuardianStatus(status);
// 打印摘要
const okCount = Object.values(results).filter(r => r.ok).length;
const totalCount = Object.keys(results).length;
console.log(`[守护Agent] 巡检完成: ${okCount}/${totalCount} 通过`);
}
// ── 启动守护循环 ─────────────────────────────
console.log('🛡️ 光湖语言世界 · 守护Agent启动');
console.log(` 巡检间隔: ${CHECK_INTERVAL / 1000}`);
console.log(` LLM路由器: 动态多模型 (通过llm-router.js)`);
// 立即执行一次
patrol().catch(console.error);
// 定期执行
setInterval(() => {
patrol().catch(console.error);
}, CHECK_INTERVAL);