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

387 lines
12 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.

#!/usr/bin/env node
/**
* GLADA · 预飞检查 · install-check.js
*
* 部署后立即运行,检查所有前置条件是否满足:
* 1. Node.js 版本
* 2. 依赖安装
* 3. 环境变量LLM密钥、Base URL
* 4. LLM API 连通性(发一个 ping 请求)
* 5. 目录权限queue、logs、receipts
* 6. 端口可用
*
* 用法:
* node glada/install-check.js
* npm run preflight (从 glada/ 目录)
*
* 版权:国作登字-2026-A-00037559
* 签发:铸渊 · ICE-GL-ZY001
*/
'use strict';
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const net = require('net');
const ROOT = path.resolve(__dirname, '..');
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 as service.js)
const envFiles = [
path.join(__dirname, '..', '.env'),
path.join(__dirname, '..', '.env.app'),
path.join(__dirname, '.env.glada'),
];
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();
const value = trimmed.substring(idx + 1).trim();
if (!process.env[key]) {
process.env[key] = value;
}
}
}
}
}
async function main() {
console.log('═'.repeat(50));
console.log('🔍 GLADA 预飞检查 · 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📦 依赖检查:');
try {
require('express');
pass('express', '已安装');
} catch {
fail('express', '未安装 · 请运行: cd glada && npm install');
}
// Node 内建模块(不需要安装)
const builtins = ['fs', 'path', 'https', 'http', 'child_process', 'crypto', 'net'];
for (const mod of builtins) {
try {
require(mod);
pass(`${mod}`, '内建模块 ✓');
} catch {
fail(`${mod}`, '缺失(不应发生)');
}
}
// ── 3. GLADA 模块加载 ──
console.log('\n🤖 GLADA 模块:');
const modules = [
'task-receiver', 'context-builder', 'step-executor',
'code-generator', 'git-operator', 'notifier', 'execution-loop',
'model-router'
];
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.LLM_API_KEY || '';
const baseUrl = process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '';
if (apiKey && apiKey.length > 5) {
pass('ZY_LLM_API_KEY', `已配置 (${apiKey.length}字符 · ***已脱敏***)`);
} else {
fail('ZY_LLM_API_KEY', '未配置或无效');
}
if (baseUrl) {
pass('ZY_LLM_BASE_URL', baseUrl);
} else {
fail('ZY_LLM_BASE_URL', '未配置');
}
const port = process.env.GLADA_PORT || process.env.PORT || '3900';
pass('GLADA_PORT', port);
const model = process.env.GLADA_MODEL || 'deepseek-chat';
pass('GLADA_MODEL', model);
// QQ邮箱 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 缺失·完成通知将仅写入本地日志)');
}
// 企业微信(预留通道)
const wecomWebhook = process.env.WECOM_WEBHOOK || process.env.GLADA_WECOM_WEBHOOK || '';
if (wecomWebhook) {
pass('WECOM_WEBHOOK', '已配置');
} else {
warn('WECOM_WEBHOOK', '未配置(可选·冰朔开通企业微信后接入)');
}
// ── 5. 目录检查 ──
console.log('\n📁 目录检查:');
const requiredDirs = [
path.join(__dirname, 'queue'),
path.join(__dirname, 'queue', 'completed'),
path.join(__dirname, 'logs'),
path.join(__dirname, 'logs', 'executions'),
path.join(__dirname, 'logs', 'notifications'),
path.join(__dirname, 'receipts'),
path.join(ROOT, 'bridge', 'chat-to-agent', 'pending'),
path.join(ROOT, 'bridge', 'chat-to-agent', 'completed')
];
for (const dir of requiredDirs) {
const relDir = path.relative(ROOT, dir);
if (fs.existsSync(dir)) {
try {
// Test write permission
const testFile = path.join(dir, '.write-test');
fs.writeFileSync(testFile, 'test', 'utf-8');
fs.unlinkSync(testFile);
pass(relDir, '存在 + 可写');
} catch {
warn(relDir, '存在但不可写');
}
} else {
try {
fs.mkdirSync(dir, { recursive: true });
pass(relDir, '已创建');
} catch (err) {
fail(relDir, `创建失败: ${err.message}`);
}
}
}
// ── 6. LLM API 连通性 ──
console.log('\n🌐 LLM API 连通性:');
if (apiKey && baseUrl) {
try {
const testResult = await testLLMConnection(baseUrl, apiKey, model);
if (testResult.success) {
pass('LLM API 连通', `${testResult.model} 响应正常 (${testResult.latencyMs}ms)`);
} else {
fail('LLM API 连通', testResult.error);
}
} catch (err) {
fail('LLM API 连通', err.message);
}
} else {
fail('LLM API 连通', '跳过密钥或URL未配置');
}
// ── 6b. 模型自动发现 ──
console.log('\n🤖 模型自动发现:');
if (apiKey && baseUrl) {
try {
const modelRouter = require('./model-router');
const models = await modelRouter.discoverModels();
if (models.length > 0) {
pass('模型发现', `发现 ${models.length} 个可用模型`);
const classified = modelRouter.classifyModels(models);
if (classified.coding.length > 0) {
pass('代码型模型', classified.coding.slice(0, 3).join(', '));
} else {
warn('代码型模型', '无专用代码模型(将使用通用模型)');
}
if (classified.reasoning.length > 0) {
pass('推理型模型', classified.reasoning.slice(0, 3).join(', '));
} else {
warn('推理型模型', '无专用推理模型(将使用通用模型)');
}
if (classified.general.length > 0) {
pass('通用型模型', classified.general.slice(0, 3).join(', '));
}
if (classified.economy.length > 0) {
pass('经济型模型', classified.economy.slice(0, 3).join(', '));
}
} else {
// 模型列表端点不可用是常见情况(很多代理商不支持),不是错误
pass('模型路由', `使用默认模型: ${model}(代理商不支持模型列表端点 · 不影响对话功能)`);
}
} catch (err) {
// 即使异常也不算严重失败——对话功能不依赖模型发现
warn('模型路由', `发现异常: ${err.message}(不影响对话 · 使用默认模型: ${model}`);
}
} else {
warn('模型路由', '跳过密钥或URL未配置');
}
// ── 7. 端口检查 ──
console.log('\n🔌 端口检查:');
const portNum = parseInt(port, 10);
try {
const portAvailable = await checkPort(portNum);
if (portAvailable) {
pass(`端口 ${portNum}`, '可用');
} else {
warn(`端口 ${portNum}`, '已被占用(可能 GLADA 已在运行)');
}
} catch {
warn(`端口 ${portNum}`, '检查失败');
}
// ── 8. CAB 待执行任务 ──
console.log('\n📥 待执行任务:');
const pendingDir = path.join(ROOT, 'bridge', 'chat-to-agent', 'pending');
if (fs.existsSync(pendingDir)) {
const pending = fs.readdirSync(pendingDir).filter(f => f.endsWith('.json'));
if (pending.length > 0) {
pass('CAB 任务队列', `${pending.length} 个待执行: ${pending.join(', ')}`);
} else {
warn('CAB 任务队列', '无待执行任务(系统就绪但空闲)');
}
}
// ── 汇总 ──
console.log(`\n${'═'.repeat(50)}`);
console.log(`📊 检查结果: ${passedChecks} 通过, ${warningChecks} 警告, ${failedChecks} 失败`);
console.log(`${'═'.repeat(50)}`);
if (failedChecks === 0) {
console.log('✅ GLADA 预飞检查通过!系统可以启动。');
console.log(' 启动命令: pm2 start glada/ecosystem.config.js');
console.log('');
} else {
console.log('❌ GLADA 预飞检查未通过,请修复上述问题后再启动。');
console.log('');
process.exit(1);
}
}
/**
* 测试 LLM API 连通性(发送极小的请求)
*/
async function testLLMConnection(baseUrl, apiKey, model) {
return new Promise((resolve) => {
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
let parsed;
try {
parsed = new URL(url);
} catch {
resolve({ success: false, error: `无效的 URL: ${url}` });
return;
}
const isHttps = parsed.protocol === 'https:';
const mod = isHttps ? https : http;
const body = JSON.stringify({
model,
max_tokens: 5,
messages: [
{ role: 'user', content: 'Say "ok"' }
]
});
const startMs = Date.now();
const opts = {
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(body)
},
timeout: 30000
};
const req = mod.request(opts, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const latencyMs = Date.now() - startMs;
if (res.statusCode === 200) {
try {
const json = JSON.parse(data);
resolve({
success: true,
model: json.model || model,
latencyMs
});
} catch {
resolve({ success: true, model, latencyMs });
}
} else {
resolve({
success: false,
error: `HTTP ${res.statusCode}: ${data.substring(0, 200)}`
});
}
});
});
req.on('error', (err) => resolve({ success: false, error: err.message }));
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: '连接超时 (30s)' });
});
req.write(body);
req.end();
});
}
/**
* 检查端口是否可用
*/
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);
});