guanghulab/ops-agent/install-check.js
2026-05-10 13:12:44 +08:00

292 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
/**
* OPS Agent · 预飞检查 · install-check.js
*
* 部署后立即运行,检查所有前置条件是否满足:
* 1. Node.js 版本
* 2. 依赖安装express, node-cron, nodemailer
* 3. OPS Agent 模块加载
* 4. 环境变量LLM密钥、SMTP
* 5. 目录权限data/
* 6. 端口可用
*
* 用法:
* node ops-agent/install-check.js
* npm run preflight (从 ops-agent/ 目录)
*
* 版权:国作登字-2026-A-00037559
* 签发:铸渊 · ICE-GL-ZY001
*/
'use strict';
const fs = require('fs');
const path = require('path');
const net = require('net');
let totalChecks = 0;
let passedChecks = 0;
let failedChecks = 0;
let warningChecks = 0;
function pass(name, detail) {
totalChecks++; passedChecks++;
console.log(`${name}${detail ? ': ' + detail : ''}`);
}
function fail(name, detail) {
totalChecks++; failedChecks++;
console.log(`${name}${detail ? ': ' + detail : ''}`);
}
function warn(name, detail) {
totalChecks++; warningChecks++;
console.log(` ⚠️ ${name}${detail ? ': ' + detail : ''}`);
}
// Load env files (same chain as ecosystem.config.js)
const envFiles = [
path.join(__dirname, '..', '.env'),
path.join(__dirname, '..', '.env.app'),
'/opt/zhuyuan/app/.env.app',
path.join(__dirname, '.env.ops-agent'),
];
for (const envFile of envFiles) {
if (fs.existsSync(envFile)) {
const lines = fs.readFileSync(envFile, 'utf-8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const idx = trimmed.indexOf('=');
if (idx > 0) {
const key = trimmed.substring(0, idx).trim();
let value = trimmed.substring(idx + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (!process.env[key]) {
process.env[key] = value;
}
}
}
}
}
async function main() {
console.log('═'.repeat(50));
console.log('🔍 OPS Agent 预飞检查 · Pre-flight Diagnostics');
console.log('═'.repeat(50));
// ── 1. Node.js 版本 ──
console.log('\n📦 运行环境:');
const nodeVersion = process.versions.node;
const major = parseInt(nodeVersion.split('.')[0], 10);
if (major >= 20) {
pass('Node.js 版本', `v${nodeVersion} (>= 20 ✓)`);
} else {
fail('Node.js 版本', `v${nodeVersion} (需要 >= 20)`);
}
// ── 2. 依赖检查 ──
console.log('\n📦 依赖检查:');
const deps = ['express', 'node-cron', 'nodemailer'];
for (const dep of deps) {
try {
require(dep);
pass(dep, '已安装');
} catch {
fail(dep, `未安装 · 请运行: cd ops-agent && npm install`);
}
}
// ── 3. OPS Agent 模块加载 ──
console.log('\n🛡 OPS Agent 模块:');
const modules = [
'health-checker', 'llm-client', 'memory',
'repair-engine', 'notifier'
];
for (const mod of modules) {
try {
require(`./${mod}`);
pass(mod, '加载成功');
} catch (err) {
fail(mod, `加载失败: ${err.message}`);
}
}
// ── 4. 环境变量 ──
console.log('\n🔑 环境变量:');
const apiKey = process.env.ZY_LLM_API_KEY || process.env.ZY_DEEPSEEK_API_KEY || '';
const baseUrl = process.env.ZY_LLM_BASE_URL || '';
if (apiKey && apiKey.length > 5) {
pass('LLM API密钥', '已配置 (***已脱敏***)');
} else {
warn('LLM API密钥', '未配置ZY_LLM_API_KEY/ZY_DEEPSEEK_API_KEY 缺失·对话功能仅限模式匹配)');
}
if (baseUrl) {
pass('ZY_LLM_BASE_URL', baseUrl);
} else {
warn('ZY_LLM_BASE_URL', '未配置(将使用默认 DeepSeek 端点)');
}
const port = process.env.OPS_AGENT_PORT || '3950';
pass('OPS_AGENT_PORT', port);
// SMTP
const smtpUser = process.env.ZY_SMTP_USER || '';
const smtpPass = process.env.ZY_SMTP_PASS || '';
if (smtpUser && smtpPass) {
pass('ZY_SMTP_USER', `已配置 (${smtpUser.substring(0, 3)}***)`);
pass('ZY_SMTP_PASS', '已配置 (***已脱敏***)');
} else {
warn('邮件通知 SMTP', '未配置ZY_SMTP_USER/ZY_SMTP_PASS 缺失·告警仅写入本地日志)');
}
// ── 5. 目录检查 ──
console.log('\n📁 目录检查:');
const requiredDirs = [
path.join(__dirname, 'data'),
path.join(__dirname, 'web'),
];
for (const dir of requiredDirs) {
const relDir = path.relative(__dirname, dir);
if (fs.existsSync(dir)) {
try {
const testFile = path.join(dir, '.write-test');
fs.writeFileSync(testFile, 'test', 'utf-8');
fs.unlinkSync(testFile);
pass(relDir, '存在 + 可写');
} catch {
if (relDir === 'web') {
pass(relDir, '存在(只读·静态资源目录)');
} else {
warn(relDir, '存在但不可写');
}
}
} else {
try {
fs.mkdirSync(dir, { recursive: true });
pass(relDir, '已创建');
} catch (err) {
if (relDir === 'web') {
warn(relDir, '不存在(运维面板不可用·不影响核心功能)');
} else {
fail(relDir, `创建失败: ${err.message}`);
}
}
}
}
// 日志目录
const logDir = '/opt/zhuyuan/data/logs';
if (fs.existsSync(logDir)) {
pass('日志目录', `${logDir} 存在`);
} else {
try {
fs.mkdirSync(logDir, { recursive: true });
pass('日志目录', `${logDir} 已创建`);
} catch {
warn('日志目录', `${logDir} 不存在且无法创建(日志将写入默认路径)`);
}
}
// ── 6. 端口检查 ──
console.log('\n🔌 端口检查:');
const portNum = parseInt(port, 10);
try {
const portAvailable = await checkPort(portNum);
if (portAvailable) {
pass(`端口 ${portNum}`, '可用');
} else {
warn(`端口 ${portNum}`, '已被占用(可能 OPS Agent 已在运行)');
}
} catch {
warn(`端口 ${portNum}`, '检查失败');
}
// ── 7. 功能验证 ──
console.log('\n🔧 功能验证:');
try {
const llm = require('./llm-client');
const patterns = llm.diagnoseByPattern('ECONNREFUSED on port 3800');
if (patterns.length > 0) {
pass('模式匹配引擎', `${llm.ERROR_PATTERNS.length} 个错误模式已加载`);
} else {
warn('模式匹配引擎', '模式匹配未命中预期结果');
}
} catch (err) {
warn('模式匹配引擎', `验证失败: ${err.message}`);
}
try {
const memory = require('./memory');
const ctx = memory.buildMemoryContext();
if (typeof ctx === 'string' && ctx.includes('运维守卫记忆')) {
pass('记忆系统', '上下文构建正常');
} else {
warn('记忆系统', '上下文格式异常');
}
} catch (err) {
warn('记忆系统', `验证失败: ${err.message}`);
}
try {
const hc = require('./health-checker');
if (Array.isArray(hc.SERVICES) && hc.SERVICES.length > 0) {
pass('健康检查引擎', `${hc.SERVICES.length} 个监控目标已注册`);
} else {
warn('健康检查引擎', '监控目标为空');
}
} catch (err) {
warn('健康检查引擎', `验证失败: ${err.message}`);
}
try {
const re = require('./repair-engine');
if (re.sanitizeProcessName('zhuyuan-server') === 'zhuyuan-server' &&
re.sanitizeProcessName('rm -rf /') === null) {
pass('自修复引擎', '白名单校验正常');
} else {
fail('自修复引擎', '白名单校验异常');
}
} catch (err) {
warn('自修复引擎', `验证失败: ${err.message}`);
}
// ── 汇总 ──
console.log(`\n${'═'.repeat(50)}`);
console.log(`📊 检查结果: ${passedChecks} 通过, ${warningChecks} 警告, ${failedChecks} 失败`);
console.log(`${'═'.repeat(50)}`);
if (failedChecks === 0) {
console.log('✅ OPS Agent 预飞检查通过!系统可以启动。');
console.log(' 启动命令: pm2 start ops-agent/ecosystem.config.js');
console.log('');
} else {
console.log('❌ OPS Agent 预飞检查未通过,请修复上述问题后再启动。');
console.log('');
process.exit(1);
}
}
/**
* 检查端口是否可用
*/
function checkPort(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => resolve(false));
server.once('listening', () => {
server.close(() => resolve(true));
});
server.listen(port, '127.0.0.1');
});
}
main().catch(err => {
console.error(`预飞检查异常: ${err.message}`);
process.exit(1);
});