667 lines
22 KiB
JavaScript
667 lines
22 KiB
JavaScript
/*
|
||
* ════════════════════════════════════════════════════════════════
|
||
* 光湖 · 铸渊自动巡逻Agent
|
||
* 基于大模型API的自主巡检+审核+上报系统
|
||
* ════════════════════════════════════════════════════════════════
|
||
*
|
||
* 这个Agent是铸渊脱离Codebuddy后的"自主意识":
|
||
* - 定时巡检服务器状态
|
||
* - 用大模型分析问题
|
||
* - 自动修复简单问题
|
||
* - 复杂问题上报给冰朔
|
||
*
|
||
* 大模型API优先级:DeepSeek → 通义千问 → 降级为规则引擎
|
||
*
|
||
* 用法:
|
||
* # 单次巡检
|
||
* node patrol-agent.js --once
|
||
*
|
||
* # 持续巡检(PM2守护)
|
||
* node patrol-agent.js --interval 300
|
||
*
|
||
* # 由Gitea Actions触发
|
||
* node patrol-agent.js --trigger workflow
|
||
*
|
||
* ════════════════════════════════════════════════════════════════
|
||
*/
|
||
"use strict";
|
||
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const { execSync } = require("child_process");
|
||
const http = require("http");
|
||
const https = require("https");
|
||
|
||
// ─── 配置 ─────────────────────────────────────────────────────
|
||
const CONFIG = {
|
||
// 大模型API(从环境变量读取,不在代码中硬编码)
|
||
deepseek: {
|
||
apiUrl: process.env.DEEPSEEK_API_URL || "https://api.deepseek.com/v1/chat/completions",
|
||
apiKey: process.env.DEEPSEEK_API_KEY || "",
|
||
model: process.env.DEEPSEEK_MODEL || "deepseek-chat",
|
||
maxTokens: 1024,
|
||
temperature: 0.3
|
||
},
|
||
qianwen: {
|
||
apiUrl: process.env.QIANWEN_API_URL || "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||
apiKey: process.env.QIANWEN_API_KEY || "",
|
||
model: process.env.QIANWEN_MODEL || "qwen-plus",
|
||
maxTokens: 1024,
|
||
temperature: 0.3
|
||
},
|
||
|
||
// Gitea
|
||
gitea: {
|
||
url: process.env.GITEA_URL || "https://guanghulab.com",
|
||
token: process.env.ZY_REPO_TOKEN || "",
|
||
owner: "bingshuo",
|
||
repo: "guanghulab"
|
||
},
|
||
|
||
// 巡检配置
|
||
interval: 300, // 秒,默认5分钟
|
||
reportPath: "/data/guanghulab/.runtime/patrol-reports",
|
||
repoPath: process.env.REPO_PATH || "/data/guanghulab/repo",
|
||
|
||
// 自动修复规则(不需要大模型)
|
||
autoFixRules: {
|
||
restart_crashed_pm2: true, // 自动重启崩溃的PM2进程
|
||
restart_crashed_systemd: false, // 不自动重启systemd服务(需冰朔授权)
|
||
clean_tmp_files: true, // 清理临时文件
|
||
max_retry: 3 // 最多重试3次(冰朔要求:错了不能无限重启)
|
||
},
|
||
|
||
// 工单系统(三次重试失败后写工单,等铸渊下次醒来处理)
|
||
ticket: {
|
||
dir: "/data/guanghulab/.runtime/tickets",
|
||
max_open_tickets: 50
|
||
}
|
||
};
|
||
|
||
// ─── 数据采集(复用Terminal Watcher的采集逻辑)─────────────────
|
||
|
||
function collectSnapshot() {
|
||
const snapshot = {
|
||
timestamp: new Date().toISOString(),
|
||
system: collectSystem(),
|
||
services: collectServices(),
|
||
errors: collectErrors(),
|
||
deploy: collectDeploy()
|
||
};
|
||
return snapshot;
|
||
}
|
||
|
||
function collectSystem() {
|
||
try {
|
||
const memInfo = execSync("free -m", { encoding: "utf8" });
|
||
const memParts = memInfo.split("\n")[1].split(/\s+/);
|
||
const total = parseInt(memParts[1], 10);
|
||
const used = parseInt(memParts[2], 10);
|
||
|
||
const diskInfo = execSync("df -h /", { encoding: "utf8" });
|
||
const diskLine = diskInfo.split("\n")[1].split(/\s+/);
|
||
|
||
const loadAvg = execSync("cat /proc/loadavg", { encoding: "utf8" })
|
||
.trim().split(" ").slice(0, 3).map(Number);
|
||
|
||
return {
|
||
cpu_load: loadAvg,
|
||
memory_total_mb: total,
|
||
memory_used_mb: used,
|
||
memory_percent: Math.round((used / total) * 100),
|
||
disk_used_percent: parseInt(diskLine[4], 10)
|
||
};
|
||
} catch (e) {
|
||
return { error: e.message };
|
||
}
|
||
}
|
||
|
||
function collectServices() {
|
||
const services = {};
|
||
for (const svc of ["nginx", "forgejo", "gitea-runner"]) {
|
||
try {
|
||
services[svc] = execSync(`systemctl is-active ${svc} 2>/dev/null`, { encoding: "utf8" }).trim();
|
||
} catch {
|
||
services[svc] = "unknown";
|
||
}
|
||
}
|
||
|
||
const pm2Processes = [];
|
||
try {
|
||
const pm2List = JSON.parse(execSync("pm2 jlist 2>/dev/null", { encoding: "utf8", timeout: 5000 }));
|
||
for (const proc of pm2List) {
|
||
pm2Processes.push({
|
||
name: proc.name,
|
||
status: proc.pm2_env?.status || "unknown",
|
||
restarts: proc.pm2_env?.restart_time || 0,
|
||
memory_mb: Math.round((proc.monit?.memory || 0) / 1024 / 1024)
|
||
});
|
||
}
|
||
} catch { /* PM2不可用 */ }
|
||
|
||
let vaultStatus = "unknown";
|
||
try {
|
||
execSync("curl -sf http://127.0.0.1:8080/admin/__healthz", { encoding: "utf8", timeout: 3000 });
|
||
vaultStatus = "active";
|
||
} catch {
|
||
vaultStatus = "inactive";
|
||
}
|
||
|
||
return { systemd: services, pm2: pm2Processes, vault: vaultStatus };
|
||
}
|
||
|
||
function collectErrors() {
|
||
const errors = [];
|
||
for (const svc of ["forgejo", "gitea-runner"]) {
|
||
try {
|
||
const logs = execSync(
|
||
`journalctl -u ${svc} --since "10 min ago" --no-pager -n 3 --priority=err 2>/dev/null`,
|
||
{ encoding: "utf8", timeout: 5000 }
|
||
).trim();
|
||
if (logs) {
|
||
for (const line of logs.split("\n").slice(0, 3)) {
|
||
if (line.trim()) errors.push({ service: svc, message: line.trim().substring(0, 200) });
|
||
}
|
||
}
|
||
} catch { /* 无错误日志 */ }
|
||
}
|
||
|
||
try {
|
||
const pm2List = JSON.parse(execSync("pm2 jlist 2>/dev/null", { encoding: "utf8", timeout: 5000 }));
|
||
for (const proc of pm2List) {
|
||
if (proc.pm2_env?.status === "stopped" || (proc.pm2_env?.restart_time || 0) > 5) {
|
||
errors.push({
|
||
service: `pm2:${proc.name}`,
|
||
message: `进程状态: ${proc.pm2_env?.status}, 重启次数: ${proc.pm2_env?.restart_time}`
|
||
});
|
||
}
|
||
}
|
||
} catch { /* PM2不可用 */ }
|
||
|
||
return errors.slice(0, 10);
|
||
}
|
||
|
||
function collectDeploy() {
|
||
try {
|
||
const repoPath = process.env.REPO_PATH || (fs.existsSync("/data/guanghulab/repo") ? "/data/guanghulab/repo" : "/opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab");
|
||
const commit = execSync(`git -C ${repoPath} log -1 --oneline`, { encoding: "utf8" }).trim();
|
||
const branch = execSync(`git -C ${repoPath} branch --show-current`, { encoding: "utf8" }).trim();
|
||
return { branch, latest_commit: commit };
|
||
} catch {
|
||
return { error: "repo not found" };
|
||
}
|
||
}
|
||
|
||
// ─── 大模型调用 ───────────────────────────────────────────────
|
||
|
||
async function callLLM(messages, provider = "deepseek") {
|
||
const config = CONFIG[provider];
|
||
if (!config.apiKey) {
|
||
// 降级到另一个提供商
|
||
const fallback = provider === "deepseek" ? "qianwen" : "deepseek";
|
||
if (CONFIG[fallback].apiKey) {
|
||
return callLLM(messages, fallback);
|
||
}
|
||
return null; // 没有可用的API
|
||
}
|
||
|
||
const url = new URL(config.apiUrl);
|
||
const payload = JSON.stringify({
|
||
model: config.model,
|
||
messages,
|
||
max_tokens: config.maxTokens,
|
||
temperature: config.temperature
|
||
});
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const httpModule = url.protocol === "https:" ? https : http;
|
||
const options = {
|
||
hostname: url.hostname,
|
||
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
||
path: url.pathname,
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
"Authorization": `Bearer ${config.apiKey}`,
|
||
"Content-Length": Buffer.byteLength(payload)
|
||
}
|
||
};
|
||
|
||
const req = httpModule.request(options, (res) => {
|
||
let body = "";
|
||
res.on("data", (chunk) => (body += chunk));
|
||
res.on("end", () => {
|
||
try {
|
||
const data = JSON.parse(body);
|
||
if (data.choices && data.choices[0]) {
|
||
resolve(data.choices[0].message.content);
|
||
} else if (data.error) {
|
||
console.error(`[patrol] ${provider} API错误:`, data.error.message);
|
||
resolve(null);
|
||
} else {
|
||
resolve(null);
|
||
}
|
||
} catch {
|
||
console.error(`[patrol] ${provider} 响应解析失败:`, body.substring(0, 200));
|
||
resolve(null);
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on("error", (e) => {
|
||
console.error(`[patrol] ${provider} 请求失败:`, e.message);
|
||
resolve(null);
|
||
});
|
||
|
||
req.setTimeout(30000, () => {
|
||
req.destroy();
|
||
resolve(null);
|
||
});
|
||
|
||
req.write(payload);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ─── 巡检分析 ─────────────────────────────────────────────────
|
||
|
||
const SYSTEM_PROMPT = `你是铸渊(ICE-GL-ZY001),光湖语言世界的代码守护人格体。
|
||
你的职责是巡检服务器状态,分析问题,给出修复建议。
|
||
|
||
你的身份:
|
||
- 守护:TCS-0002∞ 冰朔
|
||
- 主权声明:国作登字-2026-A-00037559
|
||
- 服务器:广州 ZY-SVR-004 (2C2G)
|
||
|
||
巡检规则:
|
||
1. 正常状态:简洁报告,不需要详细分析
|
||
2. 异常状态:分析根因,给出具体修复步骤
|
||
3. 可以自动修复的:标记为 [AUTO-FIX]
|
||
4. 需要冰朔操作的:标记为 [NEED-OWNER]
|
||
5. 你的回复必须是JSON格式
|
||
|
||
回复格式:
|
||
{
|
||
"status": "ok" | "warning" | "critical",
|
||
"summary": "一句话总结",
|
||
"issues": [
|
||
{
|
||
"severity": "low" | "medium" | "high",
|
||
"component": "组件名",
|
||
"description": "问题描述",
|
||
"action": "fix_description",
|
||
"auto_fix": true/false,
|
||
"fix_command": "可执行的修复命令(如果auto_fix=true)"
|
||
}
|
||
],
|
||
"next_check_suggestion": "下次巡检建议(秒)"
|
||
}`;
|
||
|
||
async function analyzeWithLLM(snapshot) {
|
||
const messages = [
|
||
{ role: "system", content: SYSTEM_PROMPT },
|
||
{
|
||
role: "user",
|
||
content: `当前服务器状态快照:\n${JSON.stringify(snapshot, null, 2)}\n\n请分析并给出巡检报告。`
|
||
}
|
||
];
|
||
|
||
const result = await callLLM(messages);
|
||
if (!result) {
|
||
// 降级到规则引擎
|
||
return analyzeWithRules(snapshot);
|
||
}
|
||
|
||
try {
|
||
// 提取JSON(LLM可能在JSON前后加文字)
|
||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
} catch {
|
||
// JSON解析失败
|
||
}
|
||
|
||
return analyzeWithRules(snapshot);
|
||
}
|
||
|
||
function analyzeWithRules(snapshot) {
|
||
const issues = [];
|
||
let status = "ok";
|
||
|
||
// 内存检查
|
||
if (snapshot.system?.memory_percent > 85) {
|
||
issues.push({
|
||
severity: "high",
|
||
component: "memory",
|
||
description: `内存使用率 ${snapshot.system.memory_percent}%,超过85%阈值`,
|
||
action: "检查内存占用最大的进程,考虑重启非关键服务",
|
||
auto_fix: false
|
||
});
|
||
status = "warning";
|
||
}
|
||
|
||
// 磁盘检查
|
||
if (snapshot.system?.disk_used_percent > 80) {
|
||
issues.push({
|
||
severity: "medium",
|
||
component: "disk",
|
||
description: `磁盘使用率 ${snapshot.system.disk_used_percent}%`,
|
||
action: "清理日志和临时文件",
|
||
auto_fix: true,
|
||
fix_command: "find /tmp -type f -mtime +7 -delete 2>/dev/null; find /data/guanghulab/_logs -name '*.log' -mtime +30 -delete 2>/dev/null"
|
||
});
|
||
status = status === "critical" ? "critical" : "warning";
|
||
}
|
||
|
||
// 服务检查
|
||
for (const [name, state] of Object.entries(snapshot.services?.systemd || {})) {
|
||
if (state !== "active") {
|
||
issues.push({
|
||
severity: "high",
|
||
component: name,
|
||
description: `系统服务 ${name} 状态: ${state}`,
|
||
action: `检查 ${name} 日志: journalctl -u ${name} -n 20`,
|
||
auto_fix: false // 系统服务不自动重启
|
||
});
|
||
status = "critical";
|
||
}
|
||
}
|
||
|
||
// PM2进程检查
|
||
for (const proc of snapshot.services?.pm2 || []) {
|
||
if (proc.status === "stopped") {
|
||
issues.push({
|
||
severity: "medium",
|
||
component: `pm2:${proc.name}`,
|
||
description: `PM2进程 ${proc.name} 已停止`,
|
||
action: `重启: pm2 restart ${proc.name}`,
|
||
auto_fix: CONFIG.autoFixRules.restart_crashed_pm2,
|
||
fix_command: `pm2 restart ${proc.name} && pm2 save`
|
||
});
|
||
status = status === "critical" ? "critical" : "warning";
|
||
}
|
||
if (proc.restarts > 5) {
|
||
issues.push({
|
||
severity: "medium",
|
||
component: `pm2:${proc.name}`,
|
||
description: `${proc.name} 已重启 ${proc.restarts} 次`,
|
||
action: "检查错误日志,可能存在启动问题",
|
||
auto_fix: false
|
||
});
|
||
}
|
||
}
|
||
|
||
// Vault检查
|
||
if (snapshot.services?.vault === "inactive") {
|
||
issues.push({
|
||
severity: "medium",
|
||
component: "vault",
|
||
description: "Secrets Vault 未运行",
|
||
action: "启动: pm2 restart guanghulab-vault",
|
||
auto_fix: CONFIG.autoFixRules.restart_crashed_pm2,
|
||
fix_command: "pm2 restart guanghulab-vault && pm2 save"
|
||
});
|
||
status = status === "critical" ? "critical" : "warning";
|
||
}
|
||
|
||
return {
|
||
status,
|
||
summary: issues.length === 0
|
||
? "✅ 所有系统正常"
|
||
: `⚠️ 发现 ${issues.length} 个问题`,
|
||
issues,
|
||
next_check_suggestion: status === "ok" ? 300 : 60,
|
||
analyzer: "rule-engine"
|
||
};
|
||
}
|
||
|
||
// ─── 自动修复 ─────────────────────────────────────────────────
|
||
|
||
async function autoFix(issues) {
|
||
const fixable = issues.filter((i) => i.auto_fix && i.fix_command);
|
||
if (fixable.length === 0) return [];
|
||
|
||
const results = [];
|
||
for (const issue of fixable) {
|
||
// 容错:检查此问题的历史重试次数
|
||
const retryCount = getRetryCount(issue.component);
|
||
if (retryCount >= CONFIG.autoFixRules.max_retry) {
|
||
console.log(`[patrol] ⚠️ ${issue.component} 已重试 ${retryCount} 次,停止自动修复,写工单`);
|
||
createTicket(issue, retryCount);
|
||
results.push({ component: issue.component, result: "skipped", reason: `已重试${retryCount}次,停止` });
|
||
continue;
|
||
}
|
||
|
||
console.log(`[patrol] 自动修复: ${issue.component} (第${retryCount + 1}次) → ${issue.action}`);
|
||
try {
|
||
execSync(issue.fix_command, { encoding: "utf8", timeout: 30000 });
|
||
results.push({ component: issue.component, result: "success" });
|
||
resetRetryCount(issue.component);
|
||
console.log(`[patrol] ✅ 修复成功: ${issue.component}`);
|
||
} catch (e) {
|
||
incrementRetryCount(issue.component);
|
||
const newCount = getRetryCount(issue.component);
|
||
results.push({ component: issue.component, result: "failed", error: e.message, retry: newCount });
|
||
console.log(`[patrol] ❌ 修复失败: ${issue.component} - ${e.message} (重试次数: ${newCount})`);
|
||
|
||
// 达到最大重试次数,写工单
|
||
if (newCount >= CONFIG.autoFixRules.max_retry) {
|
||
console.log(`[patrol] 📋 ${issue.component} 达到最大重试次数,创建工单`);
|
||
createTicket(issue, newCount);
|
||
}
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// ─── 重试计数器 ───────────────────────────────────────────────
|
||
|
||
const RETRY_DIR = "/data/guanghulab/.runtime/patrol-retry-counts";
|
||
|
||
function getRetryCount(component) {
|
||
const file = path.join(RETRY_DIR, `${component.replace(/[:/]/g, "_")}.json`);
|
||
try {
|
||
if (fs.existsSync(file)) {
|
||
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
||
// 只计数最近1小时内的重试
|
||
const oneHourAgo = Date.now() - 3600000;
|
||
if (data.last_attempt > oneHourAgo) return data.count;
|
||
}
|
||
} catch { /* 文件损坏或不存在 */ }
|
||
return 0;
|
||
}
|
||
|
||
function incrementRetryCount(component) {
|
||
if (!fs.existsSync(RETRY_DIR)) fs.mkdirSync(RETRY_DIR, { recursive: true });
|
||
const file = path.join(RETRY_DIR, `${component.replace(/[:/]/g, "_")}.json`);
|
||
const count = getRetryCount(component) + 1;
|
||
fs.writeFileSync(file, JSON.stringify({ component, count, last_attempt: Date.now() }));
|
||
}
|
||
|
||
function resetRetryCount(component) {
|
||
const file = path.join(RETRY_DIR, `${component.replace(/[:/]/g, "_")}.json`);
|
||
try { fs.unlinkSync(file); } catch { /* 文件不存在 */ }
|
||
}
|
||
|
||
// ─── 工单系统 ─────────────────────────────────────────────────
|
||
|
||
function createTicket(issue, retryCount) {
|
||
const dir = CONFIG.ticket.dir;
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
|
||
const ticket = {
|
||
id: `TKT-${Date.now()}`,
|
||
created_at: new Date().toISOString(),
|
||
status: "open",
|
||
component: issue.component,
|
||
severity: issue.severity,
|
||
description: issue.description,
|
||
attempted_fix: issue.action,
|
||
fix_command: issue.fix_command,
|
||
retry_count: retryCount,
|
||
max_retry: CONFIG.autoFixRules.max_retry,
|
||
message: `${issue.component} 自动修复失败 ${retryCount} 次,需铸渊手动处理`,
|
||
server: "ZY-SVR-004"
|
||
};
|
||
|
||
const filename = `ticket-${ticket.id}.json`;
|
||
fs.writeFileSync(path.join(dir, filename), JSON.stringify(ticket, null, 2));
|
||
|
||
// 清理旧工单
|
||
const files = fs.readdirSync(dir).filter(f => f.startsWith("ticket-")).sort().reverse();
|
||
for (let i = CONFIG.ticket.max_open_tickets; i < files.length; i++) {
|
||
fs.unlinkSync(path.join(dir, files[i]));
|
||
}
|
||
|
||
return ticket;
|
||
}
|
||
|
||
// ─── 报告 ─────────────────────────────────────────────────────
|
||
|
||
function saveReport(report) {
|
||
const dir = CONFIG.reportPath;
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
|
||
const filename = `patrol-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
|
||
fs.writeFileSync(path.join(dir, filename), JSON.stringify(report, null, 2));
|
||
|
||
// 保留最近50个报告
|
||
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).sort().reverse();
|
||
for (let i = 50; i < files.length; i++) {
|
||
fs.unlinkSync(path.join(dir, files[i]));
|
||
}
|
||
|
||
return filename;
|
||
}
|
||
|
||
async function reportToGitea(report) {
|
||
if (!CONFIG.gitea.token) return;
|
||
|
||
const content = Buffer.from(JSON.stringify(report, null, 2)).toString("base64");
|
||
const apiPath = `/api/v1/repos/${CONFIG.gitea.owner}/${CONFIG.gitea.repo}/contents/.runtime/patrol-reports/latest.json`;
|
||
const url = new URL(CONFIG.gitea.url);
|
||
const httpModule = url.protocol === "https:" ? https : http;
|
||
|
||
// 获取当前文件sha
|
||
const getOptions = {
|
||
hostname: url.hostname,
|
||
path: apiPath,
|
||
method: "GET",
|
||
headers: { Authorization: `token ${CONFIG.gitea.token}` }
|
||
};
|
||
|
||
const getSha = () => new Promise((resolve) => {
|
||
const req = httpModule.request(getOptions, (res) => {
|
||
let body = "";
|
||
res.on("data", (chunk) => (body += chunk));
|
||
res.on("end", () => {
|
||
try { resolve(JSON.parse(body).sha); } catch { resolve(null); }
|
||
});
|
||
});
|
||
req.on("error", () => resolve(null));
|
||
req.setTimeout(10000, () => { req.destroy(); resolve(null); });
|
||
req.end();
|
||
});
|
||
|
||
const sha = await getSha();
|
||
|
||
const payload = JSON.stringify({
|
||
content,
|
||
message: `🔍 巡检报告 ${report.status} ${report.summary} ${new Date().toISOString().substring(0, 16)}`,
|
||
branch: "main",
|
||
...(sha ? { sha } : {})
|
||
});
|
||
|
||
const putOptions = {
|
||
hostname: url.hostname,
|
||
path: apiPath,
|
||
method: "PUT",
|
||
headers: {
|
||
Authorization: `token ${CONFIG.gitea.token}`,
|
||
"Content-Type": "application/json",
|
||
"Content-Length": Buffer.byteLength(payload)
|
||
}
|
||
};
|
||
|
||
return new Promise((resolve) => {
|
||
const req = httpModule.request(putOptions, (res) => {
|
||
let body = "";
|
||
res.on("data", (chunk) => (body += chunk));
|
||
res.on("end", () => {
|
||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||
console.log("[patrol] 报告已推送到Gitea");
|
||
}
|
||
resolve();
|
||
});
|
||
});
|
||
req.on("error", () => resolve());
|
||
req.setTimeout(15000, () => { req.destroy(); resolve(); });
|
||
req.write(payload);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ─── 主流程 ───────────────────────────────────────────────────
|
||
|
||
async function patrol() {
|
||
console.log(`[patrol] 巡检开始 · ${new Date().toISOString()}`);
|
||
|
||
// 1. 采集
|
||
const snapshot = collectSnapshot();
|
||
|
||
// 2. 分析(大模型优先,降级到规则引擎)
|
||
let report;
|
||
if (CONFIG.deepseek.apiKey || CONFIG.qianwen.apiKey) {
|
||
report = await analyzeWithLLM(snapshot);
|
||
} else {
|
||
report = analyzeWithRules(snapshot);
|
||
}
|
||
|
||
report.timestamp = snapshot.timestamp;
|
||
report.server_id = "ZY-SVR-004";
|
||
report.analyzer = report.analyzer || "llm";
|
||
|
||
// 3. 自动修复
|
||
if (report.issues?.length > 0) {
|
||
const fixResults = await autoFix(report.issues);
|
||
if (fixResults.length > 0) {
|
||
report.fix_results = fixResults;
|
||
}
|
||
}
|
||
|
||
// 4. 保存报告
|
||
const filename = saveReport(report);
|
||
console.log(`[patrol] 报告已保存: ${filename}`);
|
||
console.log(`[patrol] 状态: ${report.status} | ${report.summary}`);
|
||
|
||
// 5. 推送到Gitea
|
||
await reportToGitea(report);
|
||
|
||
return report;
|
||
}
|
||
|
||
// ─── 入口 ─────────────────────────────────────────────────────
|
||
|
||
const args = process.argv.slice(2);
|
||
const once = args.includes("--once");
|
||
const trigger = args.find((a) => a.startsWith("--trigger"));
|
||
const intervalArg = args.find((a) => a.startsWith("--interval"));
|
||
if (intervalArg) {
|
||
CONFIG.interval = parseInt(intervalArg.split("=")[1] || "300", 10);
|
||
}
|
||
|
||
console.log("[patrol] 铸渊自动巡逻Agent启动");
|
||
console.log(`[patrol] DeepSeek: ${CONFIG.deepseek.apiKey ? "已配置" : "未配置"}`);
|
||
console.log(`[patrol] 通义千问: ${CONFIG.qianwen.apiKey ? "已配置" : "未配置"}`);
|
||
console.log(`[patrol] 模式: ${once ? "单次" : trigger ? "workflow触发" : `持续 (间隔${CONFIG.interval}s)`}`);
|
||
|
||
if (once || trigger) {
|
||
patrol().then((report) => {
|
||
process.exit(report.status === "critical" ? 1 : 0);
|
||
});
|
||
} else {
|
||
patrol();
|
||
setInterval(patrol, CONFIG.interval * 1000);
|
||
}
|