594 lines
19 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
*
* 🛡 铸渊运维守卫 · 主入口
*
*
* 编号: ZY-OPS-001
* 端口: 3950
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 功能:
* - HTTP API 服务供前端工单面板调用
* - 定时巡检快速5分钟 / 深度1小时 / 全量每天
* - 自动修复 + 工单升级
* - 对话接口LLM推理
*
* 使用:
* node ops-agent/index.js 启动守卫服务
* pm2 start ops-agent/ecosystem.config.js
*/
'use strict';
const express = require('express');
const cron = require('node-cron');
const path = require('path');
const healthChecker = require('./health-checker');
const repairEngine = require('./repair-engine');
const memory = require('./memory');
const notifier = require('./notifier');
const llmClient = require('./llm-client');
// ── 进程保活 ──────────────────────────────
process.on('uncaughtException', (err) => {
console.error('[OPS] 未捕获异常(进程保活):', err.message);
console.error('[OPS] 堆栈:', err.stack);
});
process.on('unhandledRejection', (reason) => {
console.error('[OPS] 未处理的Promise拒绝进程保活:', reason);
});
// ── Express 应用 ──────────────────────────
const app = express();
const PORT = parseInt(process.env.OPS_AGENT_PORT || '3950', 10);
const MAX_RETRIES = parseInt(process.env.OPS_MAX_REPAIR_RETRIES || '3', 10);
app.use(express.json({ limit: '1mb' }));
// 静态文件 — 运维工单面板
app.use('/ops', express.static(path.join(__dirname, 'web')));
// ── SSE 实时推送 ──────────────────────────
const sseClients = new Set();
app.get('/ops/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
res.write('data: {"type":"connected","message":"运维守卫 SSE 已连接"}\n\n');
sseClients.add(res);
req.on('close', () => { sseClients.delete(res); });
});
function broadcast(eventType, data) {
const payload = `data: ${JSON.stringify({ type: eventType, timestamp: new Date().toISOString(), ...data })}\n\n`;
for (const client of sseClients) {
try { client.write(payload); } catch { sseClients.delete(client); }
}
}
// ── 工具执行(对话中自动诊断) ─────────────
const { execSync } = require('child_process');
/**
* 根据意图自动执行诊断工具结果注入给 LLM
*/
async function executeTools(intent, action, message) {
const results = [];
try {
if (action === 'health_check' || action === 'pm2_status') {
const quick = await healthChecker.quickCheck();
let text = `## 服务状态\n${quick.summary}\n\n`;
for (const s of quick.services) {
text += `- ${s.name}(:${s.port}): ${s.status} ${s.latency ? s.latency + 'ms' : ''} ${s.error || ''}\n`;
}
results.push({ tool: '健康检查', result: text });
}
if (action === 'system_info') {
const resources = healthChecker.getSystemResources();
let text = `## 系统资源\n`;
text += `- 内存: ${resources.memory.used_pct}% (${resources.memory.free_mb}MB空闲/${resources.memory.total_mb}MB总量)\n`;
text += `- 磁盘: ${resources.disk.used_pct}% (${resources.disk.available_gb}GB可用)\n`;
text += `- CPU: ${resources.cpus}核 · 负载: ${resources.load?.[0]?.toFixed(2)}\n`;
text += `- 运行: ${resources.uptime_hours}小时\n`;
results.push({ tool: '系统资源', result: text });
}
if (action === 'pm2_logs') {
// 从消息中猜测进程名
const processName = guessProcessFromMessage(message);
if (processName) {
const logs = getPM2Logs(processName, 20);
if (logs.output) {
results.push({ tool: `PM2日志(${processName})`, result: logs.output.slice(-2000) });
}
if (logs.errorOutput) {
results.push({ tool: `PM2错误日志(${processName})`, result: logs.errorOutput.slice(-2000) });
}
}
}
if (action === 'nginx_status') {
const nginx = healthChecker.getNginxStatus();
results.push({
tool: 'Nginx状态',
result: `Nginx: ${nginx.status} · 配置${nginx.configOk ? '正常' : '异常'}`
});
}
if (action === 'list_tickets') {
const tickets = memory.getOpenTickets();
if (tickets.length === 0) {
results.push({ tool: '工单查询', result: '当前没有开放工单,系统运行正常。' });
} else {
let text = `当前有 ${tickets.length} 个开放工单:\n`;
for (const t of tickets) {
text += `- ${t.id}: ${t.title} [${t.severity}] — ${t.direction}\n`;
}
results.push({ tool: '工单查询', result: text });
}
}
} catch (err) {
console.error(`[OPS] 工具执行失败: ${err.message}`);
}
return results;
}
/**
* 从用户消息中猜测他在问哪个进程
*/
function guessProcessFromMessage(message) {
const lower = message.toLowerCase();
if (/mcp|大脑|brain/.test(lower)) return 'age-os-mcp';
if (/主站|server|3800/.test(lower)) return 'zhuyuan-server';
if (/预览|preview|3801/.test(lower)) return 'zhuyuan-preview';
if (/glada|3900/.test(lower)) return 'glada-agent';
if (/novel|智库|4000/.test(lower)) return 'novel-api';
if (/agent调度|scheduler/.test(lower)) return 'age-os-agents';
return null;
}
/**
* 获取 PM2 进程日志最近 N
*/
function getPM2Logs(processName, lines = 30) {
// 安全校验
const clean = processName.replace(/[^a-zA-Z0-9_-]/g, '');
const allowed = new Set([
'zhuyuan-server', 'zhuyuan-preview', 'novel-api',
'age-os-mcp', 'age-os-agents', 'glada-agent', 'ops-agent'
]);
if (!allowed.has(clean)) {
throw new Error(`不允许查看进程 "${clean}" 的日志`);
}
let output = '';
let errorOutput = '';
try {
output = execSync(`pm2 logs ${clean} --lines ${lines} --nostream 2>/dev/null || echo "无法获取日志"`, {
encoding: 'utf-8',
timeout: 10000
}).trim();
} catch {
output = '(PM2日志获取失败)';
}
// 也尝试获取错误日志
try {
errorOutput = execSync(`pm2 logs ${clean} --err --lines ${lines} --nostream 2>/dev/null || echo ""`, {
encoding: 'utf-8',
timeout: 10000
}).trim();
} catch {
errorOutput = '';
}
return {
processName: clean,
lines,
output,
errorOutput,
timestamp: new Date().toISOString()
};
}
// ── API: 健康检查 ─────────────────────────
app.get('/health', (_req, res) => {
const stats = memory.loadStats();
res.json({
status: 'ok',
agent: 'zy-ops-agent',
version: '1.0.0',
uptime: Math.round(process.uptime()),
stats: {
totalChecks: stats.totalChecks,
totalRepairs: stats.totalRepairs,
openTickets: memory.getOpenTickets().length
},
timestamp: new Date().toISOString()
});
});
// ── API: 手动触发巡检 ─────────────────────
app.get('/api/ops/check/quick', async (_req, res) => {
try {
const result = await runQuickCheck();
res.json(result);
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
app.get('/api/ops/check/deep', async (_req, res) => {
try {
const result = await runDeepCheck();
res.json(result);
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ── API: 工单 ──────────────────────────────
app.get('/api/ops/tickets', (_req, res) => {
const status = _req.query.status;
let tickets;
if (status === 'open') {
tickets = memory.getOpenTickets();
} else {
tickets = memory.getAllTickets();
}
res.json({ tickets, total: tickets.length });
});
app.patch('/api/ops/tickets/:id', (req, res) => {
const updated = memory.updateTicket(req.params.id, req.body);
if (!updated) {
return res.status(404).json({ error: true, message: '工单不存在' });
}
broadcast('ticket_updated', { ticket: updated });
res.json({ ticket: updated });
});
// ── API: 对话 v2多轮会话 + 工具调用) ──────
app.post('/api/ops/chat', async (req, res) => {
const { message, sessionId: inputSessionId } = req.body;
if (!message) {
return res.status(400).json({ error: true, message: '缺少 message 字段' });
}
try {
// 构建系统上下文
const memoryContext = memory.buildMemoryContext();
// 意图识别 → 自动执行相关工具
const { intent, action } = llmClient.detectIntent(message);
const toolResults = await executeTools(intent, action, message);
// 构建实时上下文
const context = memoryContext;
// 多轮对话调用
const result = await llmClient.chat(message, context, {
sessionId: inputSessionId,
toolResults
});
// 记录对话
memory.logEvent('chat', {
question: message.slice(0, 200),
method: result.method,
intent: result.intent,
toolsUsed: result.toolsUsed
});
memory.incrementStat('totalChats');
res.json({
answer: result.answer,
patternHints: result.patternHints,
method: result.method,
model: result.model,
sessionId: result.sessionId,
intent: result.intent,
toolsUsed: result.toolsUsed,
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(500).json({ error: true, message: `对话失败: ${err.message}` });
}
});
// ── API: 会话管理 ─────────────────────────
app.get('/api/ops/sessions', (_req, res) => {
res.json({ sessions: llmClient.listSessions() });
});
app.get('/api/ops/sessions/:id/history', (req, res) => {
const history = llmClient.getSessionHistory(req.params.id);
res.json({ sessionId: req.params.id, history });
});
// ── API: PM2 日志查看 ─────────────────────
app.get('/api/ops/pm2-logs/:name', (req, res) => {
const name = req.params.name;
const lines = parseInt(req.query.lines || '30', 10);
const clampedLines = Math.min(Math.max(lines, 1), 200);
try {
const result = getPM2Logs(name, clampedLines);
res.json(result);
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ── API: 系统信息 ──────────────────────────
app.get('/api/ops/system-info', async (_req, res) => {
try {
const resources = healthChecker.getSystemResources();
const pm2 = healthChecker.getPM2Status();
const nginx = healthChecker.getNginxStatus();
res.json({
resources,
pm2: pm2.map(p => ({
name: p.name,
status: p.status,
restarts: p.restarts,
memory_mb: Math.round((p.memory || 0) / 1024 / 1024),
cpu: p.cpu,
uptime_hours: Math.round((p.uptime || 0) / 3600000)
})),
nginx,
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ── API: 统计 ──────────────────────────────
app.get('/api/ops/stats', (_req, res) => {
const stats = memory.loadStats();
res.json(stats);
});
// ── API: 最近事件 ─────────────────────────
app.get('/api/ops/events', (req, res) => {
const type = req.query.type;
const limit = parseInt(req.query.limit || '20', 10);
const events = memory.queryEvents({ type, limit });
res.json({ events, total: events.length });
});
// ── 巡检 + 修复 + 告警逻辑 ─────────────────
/**
* 处理升级去重 + 邮件 + 广播
* 同一 (relatedService+diagnosis) 已有 open 工单 累加 autoRepairAttempts不发邮件不广播
* 否则 创建新工单 + 邮件 + 广播
*/
async function escalateIssue(repairResult) {
const issue = repairResult.issue;
const relatedService = issue.service;
const diagnosis = issue.error || issue.status;
const existing = memory.findOpenTicketByKey({ relatedService, diagnosis });
if (existing) {
const updated = memory.updateTicket(existing.id, {
autoRepairAttempts: (existing.autoRepairAttempts || 0) + (repairResult.retryCount || MAX_RETRIES)
});
console.log(`[OPS] 已存在开放工单 ${existing.id},仅累加修复尝试次数(不发邮件)`);
return { ticket: updated, deduplicated: true };
}
const ticket = memory.createTicket({
title: `${issue.service} - ${issue.error || issue.status}`,
description: repairResult.message,
severity: issue.severity,
category: issue.category || 'service',
direction: classifyDirection(issue),
diagnosis,
suggestedFix: issue.fix || '',
relatedService,
autoRepairAttempts: repairResult.retryCount || MAX_RETRIES
});
broadcast('new_ticket', { ticket });
await notifier.notifyTicketCreated(ticket);
return { ticket, deduplicated: false };
}
let isChecking = false;
async function runQuickCheck() {
if (isChecking) return { skipped: true, reason: '上一次巡检尚未完成' };
isChecking = true;
try {
const result = await healthChecker.quickCheck();
memory.incrementStat('totalChecks');
memory.updateStatField('lastQuickCheck', new Date().toISOString());
// 有问题 → 尝试修复
if (result.issues.length > 0) {
const repairResults = repairEngine.autoRepair(result.issues, MAX_RETRIES);
memory.incrementStat('totalRepairs', repairResults.filter(r => r.repaired).length);
// 修不了的 → 创建工单(带去重)
for (const r of repairResults) {
if (r.escalate) {
await escalateIssue(r);
}
}
result.repairs = repairResults;
// 高/紧急级别 → 邮件告警
await notifier.alertOnIssues(result);
}
memory.logEvent('check', {
type: 'quick',
healthy: result.healthy,
issueCount: result.issues.length,
summary: result.summary
});
broadcast('check_complete', { type: 'quick', healthy: result.healthy, summary: result.summary });
return result;
} finally {
isChecking = false;
}
}
async function runDeepCheck() {
if (isChecking) return { skipped: true, reason: '上一次巡检尚未完成' };
isChecking = true;
try {
const result = await healthChecker.deepCheck();
memory.incrementStat('totalChecks');
memory.updateStatField('lastDeepCheck', new Date().toISOString());
// 有问题 → 修复 + 告警(同 quickCheck 逻辑)
if (result.issues.length > 0) {
const repairResults = repairEngine.autoRepair(result.issues, MAX_RETRIES);
memory.incrementStat('totalRepairs', repairResults.filter(r => r.repaired).length);
for (const r of repairResults) {
if (r.escalate) {
await escalateIssue(r);
}
}
result.repairs = repairResults;
await notifier.alertOnIssues(result);
}
memory.logEvent('check', {
type: 'deep',
healthy: result.healthy,
issueCount: result.issues.length,
summary: result.summary
});
broadcast('check_complete', { type: 'deep', healthy: result.healthy, summary: result.summary });
return result;
} finally {
isChecking = false;
}
}
async function runDailyReport() {
const report = await healthChecker.fullReport();
memory.updateStatField('lastDailyReport', new Date().toISOString());
memory.logEvent('daily_report', {
reportId: report.report_id,
healthy: report.healthy,
issueCount: report.issues.length,
summary: report.summary
});
await notifier.sendDailyReport(report);
broadcast('daily_report', { reportId: report.report_id, summary: report.summary });
return report;
}
/**
* 判断问题处理方向
*/
function classifyDirection(issue) {
const err = (issue.error || '').toLowerCase();
const service = (issue.service || '').toLowerCase();
// 密钥/配置问题 → 冰朔自己解决
if (/api.*key|unauthorized|密钥|token/i.test(err)) {
return '冰朔自行配置 — 检查 .env 文件中的API密钥';
}
// 代码错误 → 推给铸渊
if (/syntaxerror|typeerror|referenceerror|代码/i.test(err)) {
return '推给铸渊修代码 — 需要在GitHub仓库修复';
}
// 资源问题 → 冰朔确认
if (/内存|磁盘|enomem|enospc/i.test(err)) {
return '资源不足 — 可能需要升级服务器或清理空间';
}
// 进程问题 → 运维守卫已尝试修复
if (/pm2|进程|restart|offline/i.test(err + service)) {
return '进程异常 — 运维守卫已尝试自动重启';
}
return '待判断 — 需要进一步诊断';
}
// ── 定时调度 ──────────────────────────────
// 每5分钟快速巡检
cron.schedule('*/5 * * * *', () => {
console.log('[OPS] ⏰ 定时快速巡检');
runQuickCheck().catch(err => {
console.error('[OPS] 快速巡检失败:', err.message);
});
}, { timezone: 'Asia/Shanghai' });
// 每小时深度巡检
cron.schedule('0 * * * *', () => {
console.log('[OPS] ⏰ 定时深度巡检');
runDeepCheck().catch(err => {
console.error('[OPS] 深度巡检失败:', err.message);
});
}, { timezone: 'Asia/Shanghai' });
// 每天早上8点全量报告
cron.schedule('0 8 * * *', () => {
console.log('[OPS] ⏰ 每日健康报告');
runDailyReport().catch(err => {
console.error('[OPS] 每日报告失败:', err.message);
});
}, { timezone: 'Asia/Shanghai' });
// ── 启动服务 ──────────────────────────────
app.listen(PORT, '0.0.0.0', () => {
console.log('═══════════════════════════════════════════════');
console.log(' 🛡️ 铸渊运维守卫 · ZY-OPS-001 已启动');
console.log(` 端口: ${PORT}`);
console.log(` 面板: http://127.0.0.1:${PORT}/ops/`);
console.log(' 巡检: 快速5分钟 · 深度1小时 · 全量每天8:00');
console.log(' 签发: 铸渊 · ICE-GL-ZY001');
console.log('═══════════════════════════════════════════════');
memory.logEvent('startup', { port: PORT, version: '1.0.0' });
// 启动后立即做一次快速巡检
setTimeout(() => {
console.log('[OPS] 启动后首次巡检...');
runQuickCheck().catch(err => {
console.error('[OPS] 首次巡检失败:', err.message);
});
}, 5000);
});