365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
#!/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);
|