#!/usr/bin/env node // ═══════════════════════════════════════════════ // 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 // 📜 Copyright: 国作登字-2026-A-00037559 // ═══════════════════════════════════════════════ // server/proxy/service/llm-router.js // 🤖 AI大模型动态路由器 // // 核心原则 (冰朔指令): // - 不写死模型,系统自动检测+按需调用 // - 调用失败自动切换下一个 // - 所有模型都失败 → 推送邮件给冰朔 // // 使用仓库已配置的 ZY_LLM_API_KEY + ZY_LLM_BASE_URL // secrets-manifest.json 确认此KEY支持多模型动态路由 // ═══════════════════════════════════════════════ 'use strict'; const https = require('https'); const http = require('http'); // ── 模型优先级列表(自动降级)────────────────── // API网关会根据可用性自动路由,但我们仍按优先级尝试 const MODEL_PRIORITY = [ 'deepseek-chat', 'qwen-turbo', 'moonshot-v1-8k', 'glm-4-flash', 'claude-3-haiku-20240307', 'gpt-4o-mini' ]; // ── Claude模型识别 (需走硅谷中继) ────────────── const CLAUDE_MODELS = [ 'claude-3-haiku-20240307', 'claude-3-sonnet-20240229', 'claude-3-opus-20240229', 'claude-3-5-sonnet-20241022' ]; // 硅谷SSH隧道本地端口 (Claude API中继) // 新加坡服务器通过SSH隧道将Claude请求从硅谷IP出口 // 配置: server/proxy/config/claude-relay-config.json const CLAUDE_RELAY_PORT = parseInt(process.env.ZY_CLAUDE_RELAY_PORT || '18443', 10); const CLAUDE_RELAY_ENABLED = process.env.ZY_CLAUDE_RELAY_ENABLED === 'true'; function isClaudeModel(model) { return CLAUDE_MODELS.some(cm => model.includes(cm)) || model.includes('claude'); } // ── 路由器状态 ────────────────────────────── const routerState = { total_calls: 0, successful_calls: 0, failed_calls: 0, last_success: null, last_error: null, model_stats: {} // 每个模型的调用统计 }; /** * 调用LLM API(自动模型降级) * @param {string} prompt - 用户/系统提示 * @param {Object} options - 可选配置 * @param {string} options.systemPrompt - 系统提示 * @param {number} options.maxTokens - 最大token数 * @param {number} options.timeout - 超时时间(ms) * @param {string[]} options.preferModels - 优先尝试的模型列表 * @returns {Promise<{content: string, model: string, usage: Object}|null>} */ async function callLLM(prompt, options = {}) { const apiKey = process.env.ZY_LLM_API_KEY || ''; const baseUrl = process.env.ZY_LLM_BASE_URL || ''; if (!apiKey || !baseUrl) { console.log('[LLM路由器] API未配置 (ZY_LLM_API_KEY / ZY_LLM_BASE_URL)'); return null; } const { systemPrompt = '你是光湖语言世界VPN系统的AI助手。请简洁、准确地回答问题。', maxTokens = 500, timeout = 30000, preferModels = null } = options; const modelsToTry = preferModels || MODEL_PRIORITY; routerState.total_calls++; for (const model of modelsToTry) { try { const result = await _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout); if (result) { routerState.successful_calls++; routerState.last_success = { model, time: new Date().toISOString() }; // 更新模型统计 if (!routerState.model_stats[model]) { routerState.model_stats[model] = { success: 0, fail: 0 }; } routerState.model_stats[model].success++; return result; } } catch (err) { console.log(`[LLM路由器] 模型 ${model} 调用失败: ${err.message},尝试下一个...`); if (!routerState.model_stats[model]) { routerState.model_stats[model] = { success: 0, fail: 0 }; } routerState.model_stats[model].fail++; } } // 所有模型都失败 routerState.failed_calls++; routerState.last_error = { time: new Date().toISOString(), message: `所有模型(${modelsToTry.length}个)均调用失败` }; console.error(`[LLM路由器] ❌ 所有模型均不可用`); return null; } /** * 调用单个模型 * Claude模型自动走硅谷中继 (当 ZY_CLAUDE_RELAY_ENABLED=true) */ function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout) { return new Promise((resolve, reject) => { let urlObj; try { urlObj = new URL(baseUrl); } catch { // 如果baseUrl不含协议,加上https urlObj = new URL(`https://${baseUrl}`); } // Claude模型走硅谷中继 (SSH隧道 localhost:18443 → 硅谷 → api.anthropic.com) if (CLAUDE_RELAY_ENABLED && isClaudeModel(model)) { console.log(`[LLM路由器] Claude模型 ${model} → 硅谷中继 (localhost:${CLAUDE_RELAY_PORT})`); urlObj = new URL(`https://localhost:${CLAUDE_RELAY_PORT}`); } const postData = JSON.stringify({ model, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: maxTokens, temperature: 0.3 }); // 构建路径:如果baseUrl已包含chat/completions路径则使用 // 否则追加 /v1/chat/completions let apiPath = urlObj.pathname; if (!apiPath || apiPath === '/') { apiPath = '/v1/chat/completions'; } else if (!apiPath.includes('/chat/completions')) { apiPath = apiPath.replace(/\/+$/, '') + '/chat/completions'; } const options = { hostname: urlObj.hostname, port: urlObj.port || (urlObj.protocol === 'http:' ? 80 : 443), path: apiPath, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Content-Length': Buffer.byteLength(postData) }, timeout }; const httpModule = urlObj.protocol === 'http:' ? http : https; const req = httpModule.request(options, (res) => { let data = ''; res.on('data', (d) => { data += d; }); res.on('end', () => { try { if (res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode}: 请求失败`)); return; } const json = JSON.parse(data); const content = json.choices?.[0]?.message?.content; if (!content) { reject(new Error('响应中无content字段')); return; } resolve({ content, model: json.model || model, usage: json.usage || null }); } catch (e) { reject(new Error(`JSON解析失败: ${e.message}`)); } }); }); req.on('error', (err) => reject(err)); req.on('timeout', () => { req.destroy(); reject(new Error(`超时(${timeout}ms)`)); }); req.write(postData); req.end(); }); } /** * 获取路由器状态 */ function getRouterStatus() { return { ...routerState, api_configured: !!(process.env.ZY_LLM_API_KEY && process.env.ZY_LLM_BASE_URL), models_available: MODEL_PRIORITY }; } module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY, CLAUDE_MODELS, isClaudeModel };