292 lines
8.5 KiB
JavaScript
292 lines
8.5 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
});
|