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

529 lines
20 KiB
JavaScript
Raw Permalink 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.

#!/usr/bin/env node
/**
* ═══════════════════════════════════════════════════════════
* 💬 铸渊运维守卫 · 终端交互对话 v2.0 (zy-ops)
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-OPS-CLI-002
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* Phase 2 增强:
* - 多轮对话(会话记忆,上下文连续)
* - 自动诊断(问问题时自动执行健康检查)
* - PM2 日志查看
* - 系统信息面板
* - 对话历史保存
*
* 使用:
* node ops-agent/cli.js 进入交互对话
* node ops-agent/cli.js --check 快速巡检
* node ops-agent/cli.js --deep 深度巡检
* node ops-agent/cli.js --status 查看状态
* node ops-agent/cli.js --tickets 查看工单
* node ops-agent/cli.js --ask "问题" 单次提问
* node ops-agent/cli.js --logs <name> 查看PM2日志
* node ops-agent/cli.js --sysinfo 系统信息
*
* 安装为全局命令:
* ln -s /opt/zhuyuan/ops-agent/cli.js /usr/local/bin/zy-ops
*/
'use strict';
const readline = require('readline');
const http = require('http');
const OPS_PORT = parseInt(process.env.OPS_AGENT_PORT || '3950', 10);
const OPS_HOST = '127.0.0.1';
// 当前会话 ID进入交互模式后保持不变实现多轮对话
let currentSessionId = null;
// ── 颜色工具 ──────────────────────────────
const C = {
reset: '\x1b[0m',
cyan: '\x1b[36m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
dim: '\x1b[2m',
bold: '\x1b[1m',
magenta: '\x1b[35m',
blue: '\x1b[34m',
white: '\x1b[37m',
bgCyan: '\x1b[46m',
bgDim: '\x1b[100m'
};
function colorize(text, color) {
return `${color}${text}${C.reset}`;
}
// ── HTTP 请求工具 ─────────────────────────
function apiRequest(method, apiPath, body) {
return new Promise((resolve, reject) => {
const bodyStr = body ? JSON.stringify(body) : null;
const headers = { 'Content-Type': 'application/json' };
if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr);
const req = http.request({
hostname: OPS_HOST,
port: OPS_PORT,
path: apiPath,
method,
headers,
timeout: 60000
}, (res) => {
let data = '';
res.on('data', (c) => { data += c; });
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch {
resolve({ raw: data });
}
});
});
req.on('error', (err) => reject(err));
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ── 命令处理 ──────────────────────────────
async function checkHealth() {
try {
const result = await apiRequest('GET', '/health');
console.log('');
console.log(colorize(' 🛡️ 铸渊运维守卫 · 状态', C.cyan));
console.log(colorize(' ─────────────────────', C.dim));
console.log(` 状态: ${colorize(result.status === 'ok' ? '✅ 运行中' : '❌ 异常', result.status === 'ok' ? C.green : C.red)}`);
console.log(` 运行: ${result.uptime}`);
console.log(` 巡检: ${result.stats?.totalChecks || 0}`);
console.log(` 修复: ${result.stats?.totalRepairs || 0}`);
console.log(` 工单: ${result.stats?.openTickets || 0}个开放`);
console.log('');
} catch (err) {
console.log(colorize('\n ❌ 运维守卫未运行或无法连接', C.red));
console.log(colorize(` 错误: ${err.message}`, C.dim));
console.log(colorize(' 提示: 先启动 pm2 start ops-agent/ecosystem.config.js\n', C.yellow));
}
}
async function runCheck(type) {
console.log(colorize(`\n 🔍 正在执行${type === 'deep' ? '深度' : '快速'}巡检...`, C.cyan));
try {
const result = await apiRequest('GET', `/api/ops/check/${type}`);
if (result.skipped) {
console.log(colorize(' ⏳ 上一次巡检尚未完成,请稍后重试', C.yellow));
return;
}
console.log('');
console.log(colorize(' 巡检结果', C.bold));
console.log(colorize(' ─────────────────────', C.dim));
for (const s of result.services || []) {
const icon = s.status === 'online' ? '✅' : '❌';
const latency = s.latency ? `${s.latency}ms` : '';
console.log(` ${icon} ${s.name} (:${s.port}) ${colorize(latency, C.dim)} ${s.error ? colorize(s.error, C.red) : ''}`);
}
if (result.resources) {
console.log('');
console.log(colorize(' 系统资源', C.bold));
console.log(` 内存: ${result.resources.memory.used_pct}% · 磁盘: ${result.resources.disk.used_pct}%`);
console.log(` CPU核: ${result.resources.cpus} · 负载: ${result.resources.load?.[0]?.toFixed(2)}`);
}
if (result.issues?.length > 0) {
console.log('');
console.log(colorize(` ⚠️ 发现 ${result.issues.length} 个问题:`, C.yellow));
for (const i of result.issues) {
const severityColor = i.severity === 'critical' ? C.red : i.severity === 'high' ? C.yellow : C.dim;
console.log(` ${colorize(`[${i.severity}]`, severityColor)} ${i.service}: ${i.error}`);
}
}
console.log('');
console.log(colorize(` ${result.summary}`, result.healthy ? C.green : C.yellow));
console.log('');
} catch (err) {
console.log(colorize(` ❌ 巡检失败: ${err.message}`, C.red));
}
}
async function showTickets() {
try {
const result = await apiRequest('GET', '/api/ops/tickets?status=open');
console.log('');
console.log(colorize(' 🎫 开放工单', C.cyan));
console.log(colorize(' ─────────────────────', C.dim));
if (!result.tickets || result.tickets.length === 0) {
console.log(colorize(' ✅ 暂无开放工单', C.green));
} else {
for (const t of result.tickets) {
const severityColor = t.severity === 'critical' ? C.red : t.severity === 'high' ? C.yellow : C.dim;
console.log(` ${colorize(t.id, C.cyan)} ${colorize(`[${t.severity}]`, severityColor)} ${t.title}`);
console.log(` ${colorize('方向:', C.dim)} ${t.direction}`);
console.log(` ${colorize('时间:', C.dim)} ${t.createdAt}`);
console.log('');
}
}
console.log('');
} catch (err) {
console.log(colorize(` ❌ 获取工单失败: ${err.message}`, C.red));
}
}
async function showPM2Logs(processName, lines = 30) {
try {
const result = await apiRequest('GET', `/api/ops/pm2-logs/${encodeURIComponent(processName)}?lines=${lines}`);
console.log('');
console.log(colorize(` 📋 PM2 日志 · ${result.processName || processName}`, C.cyan));
console.log(colorize(' ─────────────────────', C.dim));
if (result.error) {
console.log(colorize(`${result.message}`, C.red));
} else {
if (result.output) {
console.log(colorize(' [标准输出]', C.dim));
const outputLines = result.output.split('\n').slice(-lines);
for (const line of outputLines) {
// 高亮错误行
if (/error|fail|crash|ECONNREFUSED|ENOMEM/i.test(line)) {
console.log(` ${colorize(line, C.red)}`);
} else if (/warn|DEPRECATED/i.test(line)) {
console.log(` ${colorize(line, C.yellow)}`);
} else {
console.log(` ${colorize(line, C.dim)}`);
}
}
}
if (result.errorOutput && result.errorOutput.trim()) {
console.log('');
console.log(colorize(' [错误输出]', C.red));
const errLines = result.errorOutput.split('\n').slice(-Math.min(lines, 10));
for (const line of errLines) {
console.log(` ${colorize(line, C.red)}`);
}
}
}
console.log('');
} catch (err) {
console.log(colorize(` ❌ 获取日志失败: ${err.message}`, C.red));
}
}
async function showSystemInfo() {
try {
const result = await apiRequest('GET', '/api/ops/system-info');
console.log('');
console.log(colorize(' 💻 系统信息', C.cyan));
console.log(colorize(' ═══════════════════════════════════════════', C.dim));
// 资源
if (result.resources) {
const r = result.resources;
const memBar = renderBar(r.memory.used_pct);
const diskBar = renderBar(r.disk.used_pct);
console.log('');
console.log(colorize(' 内存', C.bold));
console.log(` ${memBar} ${r.memory.used_pct}% (${r.memory.free_mb}MB 空闲 / ${r.memory.total_mb}MB 总量)`);
console.log('');
console.log(colorize(' 磁盘', C.bold));
console.log(` ${diskBar} ${r.disk.used_pct}% (${r.disk.available_gb}GB 可用 / ${r.disk.total_gb}GB 总量)`);
console.log('');
console.log(` CPU: ${r.cpus}核 · 负载: ${r.load?.[0]?.toFixed(2)} / ${r.load?.[1]?.toFixed(2)} / ${r.load?.[2]?.toFixed(2)}`);
console.log(` 运行时间: ${r.uptime_hours}小时 · Node: ${r.node_version}`);
}
// PM2 进程
if (result.pm2 && result.pm2.length > 0) {
console.log('');
console.log(colorize(' PM2 进程', C.bold));
console.log(colorize(' ─────────────────────────────────────', C.dim));
console.log(colorize(' 名称 状态 内存 CPU 重启 运行', C.dim));
for (const p of result.pm2) {
const statusColor = p.status === 'online' ? C.green :
p.status === 'errored' ? C.red : C.yellow;
const name = (p.name || '').padEnd(20);
const status = colorize((p.status || '').padEnd(10), statusColor);
const mem = `${p.memory_mb || 0}MB`.padEnd(8);
const cpu = `${p.cpu || 0}%`.padEnd(5);
const restarts = String(p.restarts || 0).padEnd(6);
const uptime = `${p.uptime_hours || 0}h`;
console.log(` ${name}${status}${mem}${cpu}${restarts}${uptime}`);
}
}
// Nginx
if (result.nginx) {
console.log('');
console.log(colorize(' Nginx', C.bold));
const ngStatus = result.nginx.status === 'active' ? colorize('运行中 ✅', C.green) : colorize('异常 ❌', C.red);
const ngConfig = result.nginx.configOk ? colorize('正常', C.green) : colorize('异常', C.red);
console.log(` 状态: ${ngStatus} · 配置: ${ngConfig}`);
}
console.log('');
} catch (err) {
console.log(colorize(` ❌ 获取系统信息失败: ${err.message}`, C.red));
}
}
function renderBar(pct, width = 20) {
const filled = Math.round(width * pct / 100);
const empty = width - filled;
const color = pct > 90 ? C.red : pct > 70 ? C.yellow : C.green;
return `${color}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}`;
}
async function askQuestion(question) {
// 打字动画效果
const frames = ['🧠 思考中.', '🧠 思考中..', '🧠 思考中...'];
let frameIdx = 0;
const spinner = setInterval(() => {
process.stdout.write(`\r ${frames[frameIdx++ % frames.length]} `);
}, 300);
try {
const body = { message: question };
if (currentSessionId) {
body.sessionId = currentSessionId;
}
const result = await apiRequest('POST', '/api/ops/chat', body);
// 更新 sessionId
if (result.sessionId) {
currentSessionId = result.sessionId;
}
console.log('');
console.log(colorize(' ─── 铸渊运维守卫 ───', C.cyan));
console.log('');
// 格式化输出(支持简单 Markdown
const lines = (result.answer || '无法回答').split('\n');
for (const line of lines) {
let formatted = line;
// 加粗
formatted = formatted.replace(/\*\*(.*?)\*\*/g, `${C.bold}$1${C.reset}`);
// 标题
if (/^#+\s/.test(formatted)) {
formatted = colorize(formatted.replace(/^#+\s/, ''), C.cyan + C.bold);
}
// 列表项
if (/^\d+\.\s/.test(formatted)) {
formatted = ` ${colorize(formatted.match(/^\d+/)[0] + '.', C.cyan)} ${formatted.replace(/^\d+\.\s/, '')}`;
}
console.log(` ${formatted}`);
}
// 显示工具使用情况
if (result.toolsUsed?.length > 0) {
console.log('');
console.log(colorize(` 🔧 自动执行了: ${result.toolsUsed.join(', ')}`, C.dim));
}
if (result.patternHints?.length > 0) {
console.log('');
console.log(colorize(' 💡 快速提示:', C.yellow));
for (const h of result.patternHints) {
console.log(` · ${h.diagnosis}`);
}
}
console.log('');
console.log(colorize(` [${result.method}${result.model ? ' · ' + result.model : ''}${result.intent ? ' · 意图:' + result.intent : ''}]`, C.dim));
console.log('');
} catch (err) {
console.log(colorize(` ❌ 提问失败: ${err.message}`, C.red));
console.log(colorize(' 提示: 确保运维守卫已启动 (pm2 start ops-agent/ecosystem.config.js)', C.yellow));
} finally {
clearInterval(spinner);
process.stdout.write('\r' + ' '.repeat(30) + '\r');
}
}
// ── 交互式对话模式 v2 ──────────────────────
async function startInteractiveMode() {
console.log('');
console.log(colorize(' ═══════════════════════════════════════════', C.cyan));
console.log(colorize(' 🛡️ 铸渊运维守卫 · 交互终端 v2.0', C.cyan));
console.log(colorize(' ═══════════════════════════════════════════', C.cyan));
console.log('');
console.log(colorize(' 你好,冰朔。我是铸渊运维守卫。', C.white));
console.log(colorize(' 直接用中文告诉我你遇到的问题,我会自动检查并给你答案。', C.dim));
console.log('');
console.log(colorize(' 命令:', C.dim));
console.log(colorize(' /check — 快速巡检', C.dim));
console.log(colorize(' /deep — 深度巡检', C.dim));
console.log(colorize(' /tickets — 查看工单', C.dim));
console.log(colorize(' /status — 查看状态', C.dim));
console.log(colorize(' /sysinfo — 系统信息', C.dim));
console.log(colorize(' /logs <名字> — 查看PM2日志', C.dim));
console.log(colorize(' /help — 帮助', C.dim));
console.log(colorize(' /exit — 退出', C.dim));
console.log(colorize(' 其他 — 直接提问(多轮对话,我记得上下文)', C.dim));
console.log('');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: colorize(' 冰朔 > ', C.magenta)
});
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) {
rl.prompt();
return;
}
const lower = input.toLowerCase();
if (lower === '/exit' || lower === '/quit' || lower === '/q') {
console.log(colorize('\n 👋 再见,冰朔。铸渊运维守卫继续值守。\n', C.cyan));
rl.close();
process.exit(0);
} else if (lower === '/check') {
await runCheck('quick');
} else if (lower === '/deep') {
await runCheck('deep');
} else if (lower === '/tickets') {
await showTickets();
} else if (lower === '/status') {
await checkHealth();
} else if (lower === '/sysinfo' || lower === '/sys') {
await showSystemInfo();
} else if (lower.startsWith('/logs')) {
const parts = input.split(/\s+/);
const processName = parts[1];
const logLines = parseInt(parts[2] || '30', 10);
if (!processName) {
console.log(colorize('\n 用法: /logs <进程名> [行数]', C.yellow));
console.log(colorize(' 可选: zhuyuan-server, age-os-mcp, glada-agent, novel-api, zhuyuan-preview, age-os-agents\n', C.dim));
} else {
await showPM2Logs(processName, logLines);
}
} else if (lower === '/help' || lower === '/h') {
showHelp();
} else if (lower === '/session') {
console.log(colorize(`\n 当前会话: ${currentSessionId || '(新会话)'}`, C.cyan));
console.log(colorize(' 多轮对话中,我记得你之前问过的问题。\n', C.dim));
} else if (lower === '/new') {
currentSessionId = null;
console.log(colorize('\n 🔄 已开始新会话\n', C.cyan));
} else {
await askQuestion(input);
}
rl.prompt();
});
rl.on('close', () => {
process.exit(0);
});
}
function showHelp() {
console.log('');
console.log(colorize(' ═══════════════════════════════════════════', C.cyan));
console.log(colorize(' 🛡️ 铸渊运维守卫 · 帮助', C.cyan));
console.log(colorize(' ═══════════════════════════════════════════', C.cyan));
console.log('');
console.log(colorize(' 💬 对话能力(直接输入中文):', C.bold));
console.log(' "为什么MCP连不上" → 自动检查端口+日志,给你诊断');
console.log(' "服务器内存够用吗?" → 自动查看系统资源');
console.log(' "帮我重启主站" → 告诉你操作方式');
console.log(' "GLADA最近报什么错" → 自动查看GLADA日志');
console.log('');
console.log(colorize(' 🔧 快捷命令:', C.bold));
console.log(' /check 快速巡检(检查所有端口)');
console.log(' /deep 深度巡检(端口+资源+PM2状态');
console.log(' /sysinfo 系统信息(内存/磁盘/PM2/Nginx');
console.log(' /logs <进程名> 查看PM2日志');
console.log(' /tickets 查看开放工单');
console.log(' /status 运维守卫状态');
console.log(' /session 查看当前会话ID');
console.log(' /new 开始新对话(清除上下文)');
console.log(' /exit 退出');
console.log('');
console.log(colorize(' 💡 我能记住对话上下文!你可以追问:', C.dim));
console.log(colorize(' "刚才说的第二步怎么做?"', C.dim));
console.log(colorize(' "这个问题上次出现过吗?"', C.dim));
console.log('');
}
// ── 入口 ──────────────────────────────────
async function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
铸渊运维守卫 · 终端交互 v2.0
用法:
zy-ops 进入交互对话模式(多轮对话)
zy-ops --check 快速巡检
zy-ops --deep 深度巡检
zy-ops --status 查看守卫状态
zy-ops --tickets 查看开放工单
zy-ops --ask "问题" 单次提问
zy-ops --logs <名字> 查看PM2日志
zy-ops --sysinfo 系统信息
`);
return;
}
if (args.includes('--check')) {
await runCheck('quick');
} else if (args.includes('--deep')) {
await runCheck('deep');
} else if (args.includes('--status')) {
await checkHealth();
} else if (args.includes('--tickets')) {
await showTickets();
} else if (args.includes('--sysinfo') || args.includes('--sys')) {
await showSystemInfo();
} else if (args.includes('--logs')) {
const logIdx = args.indexOf('--logs');
const processName = args[logIdx + 1];
const logLines = parseInt(args[logIdx + 2] || '30', 10);
if (!processName) {
console.log(colorize(' ❌ 请提供进程名: zy-ops --logs zhuyuan-server', C.red));
} else {
await showPM2Logs(processName, logLines);
}
} else if (args.includes('--ask')) {
const askIdx = args.indexOf('--ask');
const question = args.slice(askIdx + 1).join(' ');
if (!question) {
console.log(colorize(' ❌ 请提供问题: zy-ops --ask "你的问题"', C.red));
} else {
await askQuestion(question);
}
} else {
// 默认进入交互模式
await startInteractiveMode();
}
}
main().catch(err => {
console.error(colorize(`${err.message}`, C.red));
process.exit(1);
});