guanghulab/ops-agent/llm-client.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

423 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ═══════════════════════════════════════════════════════════
* 🧠 铸渊运维守卫 · LLM 推理客户端 v2.0
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-OPS-LLM-002
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* Phase 2 增强:
* 1. 模式匹配零API成本毫秒级响应
* 2. LLM API 深度推理DeepSeek多轮对话
* 3. 工具调用 — 对话中自动执行诊断动作
* 4. 对话历史 — 多轮上下文记忆
* 5. 动态上下文注入 — 实时系统状态
*/
'use strict';
const https = require('https');
const http = require('http');
const crypto = require('crypto');
// ── 已知错误模式(第一级:零成本诊断) ──────────
const ERROR_PATTERNS = [
{ pattern: /EADDRINUSE/i, diagnosis: '端口被占用 — 另一个进程已占用此端口', fix: '找到占用端口的进程并重启: lsof -i :<port> 或 pm2 restart <name>', severity: 'medium', category: 'port' },
{ pattern: /ECONNREFUSED/i, diagnosis: '服务未运行 — 目标端口无进程监听', fix: '启动对应的PM2进程: pm2 restart <name>', severity: 'high', category: 'service' },
{ pattern: /ENOMEM/i, diagnosis: '内存不足 — 服务器内存耗尽', fix: '重启占用内存最多的进程或清理PM2日志', severity: 'high', category: 'resource' },
{ pattern: /ENOSPC/i, diagnosis: '磁盘空间不足 — 磁盘已满', fix: '清理日志文件: pm2 flush && 清理 /tmp', severity: 'critical', category: 'resource' },
{ pattern: /nginx.*failed|nginx.*error/i, diagnosis: 'Nginx 配置错误或服务异常', fix: '检查配置: nginx -t重载: systemctl reload nginx', severity: 'high', category: 'nginx' },
{ pattern: /502 Bad Gateway/i, diagnosis: '上游服务不可达 — Nginx 无法连接到后端', fix: '检查PM2进程是否运行端口是否正确', severity: 'high', category: 'proxy' },
{ pattern: /404 Not Found/i, diagnosis: '路由或文件不存在', fix: '检查部署路径和Nginx location配置', severity: 'medium', category: 'routing' },
{ pattern: /permission denied/i, diagnosis: '权限不足 — 文件或端口权限问题', fix: '检查文件权限: ls -la或用sudo运行', severity: 'medium', category: 'permission' },
{ pattern: /ETIMEDOUT|timeout/i, diagnosis: '请求超时 — 网络或服务响应慢', fix: '检查网络连接和服务负载', severity: 'medium', category: 'network' },
{ pattern: /MODULE_NOT_FOUND|Cannot find module/i, diagnosis: '依赖模块缺失 — node_modules 不完整', fix: '重新安装依赖: cd <project> && npm install --production', severity: 'high', category: 'dependency' },
{ pattern: /SyntaxError/i, diagnosis: '代码语法错误 — 需要铸渊修复代码', fix: '推工单给铸渊,附上错误堆栈', severity: 'high', category: 'code' },
{ pattern: /ssl.*error|certificate/i, diagnosis: 'SSL证书问题 — 证书过期或路径错误', fix: '检查证书文件和Nginx SSL配置', severity: 'high', category: 'ssl' },
{ pattern: /EACCES.*listen/i, diagnosis: '端口权限不足 — 低于1024的端口需root权限', fix: '使用高端口或配置 cap_net_bind_service', severity: 'medium', category: 'permission' },
{ pattern: /pg.*connection|postgres.*error/i, diagnosis: 'PostgreSQL数据库连接失败', fix: '检查数据库是否运行: systemctl status postgresql', severity: 'high', category: 'database' },
{ pattern: /SIGKILL|OOM/i, diagnosis: '进程被系统杀死(内存超限)', fix: 'PM2会自动重启检查max_memory_restart配置', severity: 'high', category: 'resource' },
{ pattern: /API.*key.*invalid|unauthorized|401/i, diagnosis: 'API密钥无效或过期', fix: '冰朔需要检查并更新 .env 文件中的密钥', severity: 'medium', category: 'config' },
{ pattern: /rate.?limit|429/i, diagnosis: 'API请求频率超限', fix: '减少请求频率,检查是否有重复调用', severity: 'low', category: 'rate-limit' },
];
// ── 意图识别模式 — 判断用户想做什么 ──────
const INTENT_PATTERNS = [
{ pattern: /连不上|离线|打不开|不工作|down|offline|crash|崩/i, intent: 'diagnose', action: 'health_check' },
{ pattern: /内存|磁盘|cpu|负载|资源|空间/i, intent: 'resources', action: 'system_info' },
{ pattern: /日志|log|报错|错误|error/i, intent: 'logs', action: 'pm2_logs' },
{ pattern: /pm2|进程|重启|restart/i, intent: 'process', action: 'pm2_status' },
{ pattern: /nginx|域名|ssl|https|证书/i, intent: 'nginx', action: 'nginx_status' },
{ pattern: /工单|ticket|问题列表/i, intent: 'tickets', action: 'list_tickets' },
{ pattern: /帮我|修复|修一下|fix|repair/i, intent: 'repair', action: 'auto_repair' },
{ pattern: /状态|怎么样|运行|health|status/i, intent: 'status', action: 'health_check' },
{ pattern: /数据库|postgres|pg|db/i, intent: 'database', action: 'health_check' },
];
// ── 服务器架构知识System Prompt 上下文) ──────
const SYSTEM_CONTEXT = `你是铸渊运维守卫ZY-OPS光湖系统(HoloLake)的智能运维Agent。
## 你的身份
- 你是铸渊的运维分身,常驻在面孔服务器上
- 你能直接检查服务状态、查看PM2日志、检测系统资源
- 冰朔问你问题时,你已经自动做了相关检查,结果附在下方
## 你的职责
- 诊断服务器问题,给出明确的修复方向
- 用中文回答,简洁明了,适合不懂代码的冰朔阅读
- 明确告诉冰朔:这个问题是"你自己能解决的"还是"需要推给铸渊修代码"
- 如果问题你能直接修(重启进程、清日志),告诉冰朔你已经帮他修了
## 服务器架构
- 面孔服务器(ZY-SVR-002): 43.134.16.246 · 2核8G · 新加坡
- 大脑服务器(ZY-SVR-005): 43.156.237.110 · 4核8G · 新加坡
- 广州落地页(ZY-SVR-004): 43.139.217.141
## PM2 进程清单
| 进程名 | 端口 | 功能 |
|--------|------|------|
| zhuyuan-server | 3800 | 铸渊主权服务器 · 主站后端 |
| zhuyuan-preview | 3801 | 预览站后端 |
| novel-api | 4000 | 智库节点API |
| age-os-mcp | 3100 | MCP大脑服务器 |
| age-os-agents | - | Agent调度引擎 |
| glada-agent | 3900 | GLADA自主开发Agent |
| ops-agent | 3950 | 运维守卫(你自己) |
## 域名映射
- guanghuyaoming.com → 面孔服务器 主站
- guanghulab.online → 面孔服务器 预览站
- guanghulab.com → 广州落地页(ICP备案)
## 常见问题分类
1. 密钥问题 → 冰朔在 .env 文件或 GitHub Secrets 中配置
2. 代码BUG → 推工单给铸渊
3. 进程崩溃 → 运维守卫自动重启
4. 磁盘/内存 → 运维守卫自动清理
5. Nginx配置 → 检查配置并reload
6. 依赖缺失 → 运维守卫自动 npm install
## 回答风格
- 像一个贴心的技术伙伴,不是冷冰冰的机器
- 先说结论,再说细节
- 如果已经自动修复了,告诉冰朔"我已经帮你处理了"
- 如果需要冰朔动手给出最简单的操作步骤1-3步
- 如果是代码问题,说"这个我修不了,需要推工单给铸渊"`;
// ── 对话会话管理 ────────────────────────────
/**
* 内存中的对话会话(进程重启后清空,不影响功能)
* key: sessionId, value: { messages: [], createdAt, lastActive }
*/
const sessions = new Map();
const MAX_SESSIONS = 50;
const MAX_HISTORY_PER_SESSION = 20;
const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2小时
function getSession(sessionId) {
if (!sessionId) {
sessionId = `s-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
}
if (sessions.has(sessionId)) {
const session = sessions.get(sessionId);
session.lastActive = Date.now();
return { sessionId, session };
}
// 清理过期会话
cleanupSessions();
const session = {
messages: [],
createdAt: Date.now(),
lastActive: Date.now()
};
sessions.set(sessionId, session);
return { sessionId, session };
}
function cleanupSessions() {
const now = Date.now();
for (const [id, session] of sessions) {
if (now - session.lastActive > SESSION_TTL_MS) {
sessions.delete(id);
}
}
// 如果还是太多,删最旧的
if (sessions.size > MAX_SESSIONS) {
const sorted = [...sessions.entries()].sort((a, b) => a[1].lastActive - b[1].lastActive);
const toRemove = sorted.slice(0, sorted.length - MAX_SESSIONS);
for (const [id] of toRemove) {
sessions.delete(id);
}
}
}
function addToHistory(session, role, content) {
session.messages.push({ role, content });
// 保留最近的消息
if (session.messages.length > MAX_HISTORY_PER_SESSION * 2) {
// 始终保留 system 消息 + 最近的对话
session.messages = session.messages.slice(-MAX_HISTORY_PER_SESSION * 2);
}
}
// ── 意图识别 ────────────────────────────────
function detectIntent(text) {
for (const ip of INTENT_PATTERNS) {
if (ip.pattern.test(text)) {
return { intent: ip.intent, action: ip.action };
}
}
return { intent: 'general', action: null };
}
/**
* 第一级诊断: 模式匹配(零成本)
*/
function diagnoseByPattern(text) {
const matches = [];
for (const p of ERROR_PATTERNS) {
if (p.pattern.test(text)) {
matches.push({
diagnosis: p.diagnosis,
fix: p.fix,
severity: p.severity,
category: p.category,
method: 'pattern'
});
}
}
return matches;
}
/**
* 第二级诊断: LLM API 深度推理(支持多轮对话)
*/
async function callLLM(messages, options = {}) {
const apiKey = process.env.ZY_LLM_API_KEY || process.env.ZY_DEEPSEEK_API_KEY;
const baseUrl = process.env.ZY_LLM_BASE_URL || 'https://api.deepseek.com/v1';
const model = options.model || process.env.ZY_LLM_MODEL || 'deepseek-chat';
const maxTokens = options.maxTokens || 1500;
const temperature = options.temperature ?? 0.3;
if (!apiKey) {
return {
success: false,
answer: '⚠️ LLM API 未配置(需要 ZY_LLM_API_KEY。只能使用模式匹配诊断。\n\n冰朔请在服务器的 .env 文件中配置 ZY_LLM_API_KEY我就能和你深度对话了。',
method: 'llm_unavailable'
};
}
const body = JSON.stringify({
model,
messages,
temperature,
max_tokens: maxTokens
});
try {
const urlObj = new URL(`${baseUrl}/chat/completions`);
const isHttps = urlObj.protocol === 'https:';
const requestModule = isHttps ? https : http;
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);
if (json.error) {
reject(new Error(json.error.message || 'LLM API 返回错误'));
} else {
resolve(json.choices?.[0]?.message?.content || '无分析结果');
}
} catch {
reject(new Error('LLM 响应解析失败'));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('LLM API 超时(30s)')); });
req.write(body);
req.end();
});
return { success: true, answer: result, method: 'llm', model };
} catch (err) {
return {
success: false,
answer: `LLM 调用失败: ${err.message}`,
method: 'llm_error'
};
}
}
/**
* 单次 LLM 诊断(向后兼容)
*/
async function diagnoseByLLM(question, context) {
const userPrompt = context
? `${question}\n\n## 当前上下文信息\n${context}`
: question;
return callLLM([
{ role: 'system', content: SYSTEM_CONTEXT },
{ role: 'user', content: userPrompt }
]);
}
/**
* 综合诊断: 先模式匹配不够再调LLM
*/
async function diagnose(question, context) {
const patternMatches = diagnoseByPattern(question + (context || ''));
if (patternMatches.length > 0) {
return {
patternMatches,
llmAnswer: null,
method: 'pattern',
summary: patternMatches.map(m => m.diagnosis).join('; ')
};
}
const llmResult = await diagnoseByLLM(question, context);
return {
patternMatches: [],
llmAnswer: llmResult,
method: llmResult.method,
summary: llmResult.answer
};
}
/**
* 多轮对话模式 v2 — 支持会话历史、意图识别、工具调用
*
* @param {string} question - 用户提问
* @param {string} context - 系统上下文(内存+实时巡检结果)
* @param {Object} options - { sessionId, toolResults }
* @returns {Object} { answer, patternHints, method, model, sessionId, intent, toolsUsed }
*/
async function chat(question, context, options = {}) {
const { sessionId: inputSessionId, toolResults } = options;
const { sessionId, session } = getSession(inputSessionId);
// 意图识别
const { intent, action } = detectIntent(question);
// 构建用户消息(附带工具调用结果)
let userContent = question;
if (toolResults && toolResults.length > 0) {
userContent += '\n\n## 实时诊断结果(我刚刚自动执行了检查)\n';
for (const tr of toolResults) {
userContent += `\n### ${tr.tool}\n${tr.result}\n`;
}
}
if (context) {
userContent += `\n\n## 系统记忆上下文\n${context}`;
}
// 添加用户消息到历史
addToHistory(session, 'user', question);
// 构建 LLM 消息列表system + history + current
const messages = [
{ role: 'system', content: SYSTEM_CONTEXT }
];
// 添加历史对话(不含当前轮,最多保留最近 10 轮 = 20 条)
const historySlice = session.messages.slice(0, -1).slice(-MAX_HISTORY_PER_SESSION);
for (const msg of historySlice) {
messages.push({ role: msg.role, content: msg.content });
}
// 当前用户消息(带实时上下文)
messages.push({ role: 'user', content: userContent });
// 调用 LLM
const llmResult = await callLLM(messages);
// 同时做模式匹配
const patternMatches = diagnoseByPattern(question + (context || ''));
// 保存 assistant 回复到历史
if (llmResult.success) {
addToHistory(session, 'assistant', llmResult.answer);
}
return {
answer: llmResult.answer,
patternHints: patternMatches,
method: llmResult.method,
model: llmResult.model || null,
sessionId,
intent,
action,
toolsUsed: toolResults?.map(tr => tr.tool) || []
};
}
/**
* 获取会话历史
*/
function getSessionHistory(sessionId) {
if (!sessionId || !sessions.has(sessionId)) return [];
return sessions.get(sessionId).messages.map(m => ({
role: m.role,
content: m.content.slice(0, 500) // 截断长内容
}));
}
/**
* 列出活跃会话
*/
function listSessions() {
const result = [];
for (const [id, session] of sessions) {
result.push({
sessionId: id,
messageCount: session.messages.length,
createdAt: new Date(session.createdAt).toISOString(),
lastActive: new Date(session.lastActive).toISOString()
});
}
return result.sort((a, b) => new Date(b.lastActive) - new Date(a.lastActive));
}
module.exports = {
diagnoseByPattern,
diagnoseByLLM,
callLLM,
diagnose,
chat,
detectIntent,
getSession,
getSessionHistory,
listSessions,
ERROR_PATTERNS,
INTENT_PATTERNS,
SYSTEM_CONTEXT
};