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