#!/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); });