1 line
164 KiB
JSON
1 line
164 KiB
JSON
{"repo": "18388853038/guoshaowen-blog", "files": {"backend/modules/ai-engine.js": {"a0e92cca8533660972d0a16d2874b571292e53fa": {"t": 1785746685.1581972, "n": 26468, "h": "8ecba8cd01bb4033", "c": "/**\n * eCompany AI Engine\n * 多模型 AI 引擎 - 支持 DeepSeek / OpenAI / Anthropic / Ollama 等\n * 注入:多模型故障切换、流式输出、高级工具调用\n */\n\n// ========== 模型配置 ==========\nconst PROVIDERS = {\n \"ollama\": {\n \"baseUrl\": process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434/v1/chat/completions\",\n \"apiKeyEnv\": \"OLLAMA_API_KEY\",\n \"defaultModel\": \"qwen3.5:9b\",\n \"noApiKey\": true,\n \"models\": [\n { \"id\": \"qwen3.5:9b\", \"label\": \"Qwen 3.5 9B (本地)\", \"tags\": [\"通用\", \"推理\", \"本地\"], \"contextWindow\": 32768 },\n { \"id\": \"deepseek-coder-v2:16b\", \"label\": \"DeepSeek Coder V2 16B (本地)\", \"tags\": [\"代码\", \"推理\", \"本地\"], \"contextWindow\": 32768 }\n ]\n },\n \"ernie\": {\n \"baseUrl\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions\",\n \"apiKeyEnv\": \"ERNIE_API_KEY\",\n \"defaultModel\": \"ernie-4.0-8k\",\n \"models\": [\n { \"id\": \"ernie-4.0-8k\", \"label\": \"文心4.0 8K\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-4.0-32k\", \"label\": \"文心4.0 32K\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 32000 },\n { \"id\": \"ernie-3.5-8k\", \"label\": \"文心3.5 8K\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-speed-128k\", \"label\": \"文心 Speed 128K\", \"tags\": [\"快速\", \"长文本\", \"经济\"], \"contextWindow\": 128000 },\n { \"id\": \"ernie-speed-8k\", \"label\": \"文心 Speed 8K\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-lite-8k\", \"label\": \"文心 Lite 8K\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 8000 }\n ]\n },\n \"yi\": {\n \"baseUrl\": \"https://api.01.ai/v1/chat/completions\",\n \"apiKeyEnv\": \"YI_API_KEY\",\n \"defaultModel\": \"yi-large\",\n \"models\": [\n { \"id\": \"yi-large\", \"label\": \"Yi Large\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 160000 },\n { \"id\": \"yi-large-turbo\", \"label\": \"Yi Large Turbo\", \"tags\": [\"推理\", \"快速\"], \"contextWindow\": 32000 },\n { \"id\": \"yi-medium\", \"label\": \"Yi Medium\", \"tags\": [\"通用\", \"均衡\"], \"contextWindow\": 32000 },\n { \"id\": \"yi-medium-turbo\", \"label\": \"Yi Medium Turbo\", \"tags\": [\"均衡\", \"快速\"], \"contextWindow\": 16000 },\n { \"id\": \"yi-spark\", \"label\": \"Yi Spark\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 16000 },\n { \"id\": \"yi-lightning\", \"label\": \"Yi Lightning\", \"tags\": [\"快速\", \"轻量\"], \"contextWindow\": 16000 }\n ]\n },\n \"deepseek\": {\n \"baseUrl\": \"https://api.deepseek.com/v1/chat/completions\",\n \"apiKeyEnv\": \"DEEPSEEK_API_KEY\",\n \"defaultModel\": \"deepseek-chat\",\n \"models\": [\n { \"id\": \"deepseek-chat\", \"label\": \"DeepSeek Chat V3(⚠️ 2026/07/24 弃用)\", \"tags\": [\"通用\", \"快速\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"deepseek-reasoner\", \"label\": \"DeepSeek R1(⚠️ 2026/07/24 弃用)\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 64000 },\n { \"id\": \"deepseek-v4-flash\", \"label\": \"DeepSeek V4 Flash(轻量版)🌟 推荐\", \"tags\": [\"快速\", \"低成本\", \"轻量\"], \"contextWindow\": 1000000 },\n { \"id\": \"deepseek-v4-pro\", \"label\": \"DeepSeek V4 Pro(专业版)\", \"tags\": [\"推理\", \"深度思考\", \"代码\", \"数学\"], \"contextWindow\": 1000000 }\n ]\n },\n \"openai\": {\n \"baseUrl\": \"https://api.openai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENAI_API_KEY\",\n \"defaultModel\": \"gpt-4o\",\n \"models\": [\n { \"id\": \"gpt-4o\", \"label\": \"GPT-4o\", \"tags\": [\"通用\", \"视觉\", \"推理\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4o-mini\", \"label\": \"GPT-4o Mini\", \"tags\": [\"快速\", \"低成本\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4o-mini-high\", \"label\": \"GPT-4o Mini High\", \"tags\": [\"低成本\", \"高精度\"], \"contextWindow\": 128000 },\n { \"id\": \"o1\", \"label\": \"O1\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 200000 },\n { \"id\": \"o3-mini\", \"label\": \"O3 Mini\", \"tags\": [\"推理\", \"快速\"], \"contextWindow\": 200000 },\n { \"id\": \"gpt-4-turbo\", \"label\": \"GPT-4 Turbo\", \"tags\": [\"通用\", \"高精度\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4\", \"label\": \"GPT-4\", \"tags\": [\"通用\", \"稳定\"], \"contextWindow\": 8192 }\n ]\n },\n \"openrouter\": {\n \"baseUrl\": \"https://openrouter.ai/api/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENROUTER_API_KEY\",\n \"defaultModel\": \"openrouter/auto\",\n \"models\": [\n { \"id\": \"openrouter/auto\", \"label\": \"OpenRouter Auto\", \"tags\": [\"自动选择\"], \"contextWindow\": 128000 },\n { \"id\": \"openrouter/deepseek\", \"label\": \"DeepSeek via OpenRouter\", \"tags\": [\"推理\"], \"contextWindow\": 128000 }\n ]\n },\n \"anthropic\": {\n \"baseUrl\": \"https://api.anthropic.com/v1/messages\",\n \"apiKeyEnv\": \"ANTHROPIC_API_KEY\",\n \"defaultModel\": \"claude-sonnet-4-20250514\",\n \"models\": [\n { \"id\": \"claude-sonnet-4-20250514\", \"label\": \"Claude Sonnet 4\", \"tags\": [\"推理\", \"通用\", \"代码\", \"长文本\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-5-sonnet-20241022\", \"label\": \"Claude 3.5 Sonnet\", \"tags\": [\"推理\", \"通用\", \"代码\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-opus-4-20250514\", \"label\": \"Claude Opus 4\", \"tags\": [\"深度思考\", \"推理\", \"高精度\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-5-haiku-20241022\", \"label\": \"Claude 3.5 Haiku\", \"tags\": [\"快速\", \"低成本\", \"轻量\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-haiku-20240307\", \"label\": \"Claude 3 Haiku\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 200000 }\n ]\n },\n \"google\": {\n \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n \"apiKeyEnv\": \"GOOGLE_API_KEY\",\n \"defaultModel\": \"gemini-2.5-flash\",\n \"models\": [\n { \"id\": \"gemini-2.5-flash\", \"label\": \"Gemini 2.5 Flash\", \"tags\": [\"快速\", \"多模态\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.5-pro\", \"label\": \"Gemini 2.5 Pro\", \"tags\": [\"推理\", \"多模态\", \"深度思考\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.0-flash\", \"label\": \"Gemini 2.0 Flash\", \"tags\": [\"快速\", \"多模态\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-1.5-pro\", \"label\": \"Gemini 1.5 Pro\", \"tags\": [\"推理\", \"稳定\"], \"contextWindow\": 2000000 },\n { \"id\": \"gemini-1.5-flash\", \"label\": \"Gemini 1.5 Flash\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 1000000 }\n ]\n },\n \"gemini\": {\n \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n \"apiKeyEnv\": \"GEMINI_API_KEY\",\n \"defaultModel\": \"gemini-2.5-flash\",\n \"models\": [\n { \"id\": \"gemini-2.5-flash\", \"label\": \"Gemini 2.5 Flash\", \"tags\": [\"快速\", \"多模态\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.5-pro\", \"label\": \"Gemini 2.5 Pro\", \"tags\": [\"推理\", \"多模态\", \"深度思考\"], \"contextWindow\": 1000000 }\n ]\n },\n \"moonshot\": {\n \"baseUrl\": \"https://api.moonshot.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"MOONSHOT_API_KEY\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"models\": [\n { \"id\": \"moonshot-v1-8k\", \"label\": \"Moonshot 8K\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"moonshot-v1-32k\", \"label\": \"Moonshot 32K\", \"tags\": [\"通用\", \"长文本\"], \"contextWindow\": 32000 },\n { \"id\": \"moonshot-v1-128k\", \"label\": \"Moonshot 128K\", \"tags\": [\"长文本\", \"深度\"], \"contextWindow\": 128000 }\n ]\n },\n \"tongyi\": {\n \"baseUrl\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n \"apiKeyEnv\": \"DASHSCOPE_API_KEY\",\n \"defaultModel\": \"qwen3.7-max\",\n \"models\": [\n { \"id\": \"qwen3.7-max\", \"label\": \"Qwen 3.7 Max(最新旗舰)\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.7-plus\", \"label\": \"Qwen 3.7 Plus(均衡型)\", \"tags\": [\"均衡\", \"通用\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.6-flash\", \"label\": \"Qwen 3.6 Flash(快速型)\", \"tags\": [\"快速\", \"低成本\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.6-flash-lite\", \"label\": \"Qwen 3.6 Flash Lite\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-max\", \"label\": \"通义千问 Max(稳定版)\", \"tags\": [\"推理\", \"稳定\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"qwen-plus\", \"label\": \"通义千问 Plus\", \"tags\": [\"均衡\", \"通用\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-turbo\", \"label\": \"通义千问 Turbo\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-vl-max\", \"label\": \"通义千问 VL Max(视觉多模态)\", \"tags\": [\"视觉\", \"多模态\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"qwen2.5-72b-instruct\", \"label\": \"Qwen 2.5 72B\", \"tags\": [\"推理\", \"开源\", \"中文\"], \"contextWindow\": 131072 }\n ]\n },\n \"zhipu\": {\n \"baseUrl\": \"https://open.bigmodel.cn/api/paas/v4/chat/completions\",\n \"apiKeyEnv\": \"ZHIPUAI_API_KEY\",\n \"defaultModel\": \"glm-4-plus\",\n \"models\": [\n { \"id\": \"glm-4-plus\", \"label\": \"GLM-4 Plus\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4\", \"label\": \"GLM-4\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-flash\", \"label\": \"GLM-4 Flash\", \"tags\": [\"快速\", \"经济\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-flashx\", \"label\": \"GLM-4 FlashX\", \"tags\": [\"快速\", \"轻量\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-air\", \"label\": \"GLM-4 Air\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-long\", \"label\": \"GLM-4 Long(长文本)\", \"tags\": [\"长文本\", \"深度\"], \"contextWindow\": 1048576 },\n { \"id\": \"glm-4v-plus\", \"label\": \"GLM-4V Plus(视觉)\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 128000 }\n ]\n },\n \"siliconflow\": {\n \"baseUrl\": \"https://api.siliconflow.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"SILICONFLOW_API_KEY\",\n \"defaultModel\": \"Pro/DeepSeek-V3\",\n \"models\": [\n { \"id\": \"Pro/DeepSeek-V3\", \"label\": \"DeepSeek V3\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 64000 },\n { \"id\": \"Pro/DeepSeek-R1\", \"label\": \"DeepSeek R1\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 64000 },\n { \"id\": \"Qwen/Qwen2.5-72B-Instruct\", \"label\": \"Qwen 2.5 72B\", \"tags\": [\"推理\", \"开源\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-32B-Instruct\", \"label\": \"Qwen 2.5 32B\", \"tags\": [\"均衡\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-14B-Instruct\", \"label\": \"Qwen 2.5 14B\", \"tags\": [\"快速\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-7B-Instruct\", \"label\": \"Qwen 2.5 7B\", \"tags\": [\"轻量\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"THUDM/glm-4-9b-chat\", \"label\": \"GLM-4 9B\", \"tags\": [\"中文\", \"开源\"], \"contextWindow\": 128000 },\n { \"id\": \"internlm/internlm2_5-7b-chat\", \"label\": \"InternLM 2.5 7B\", \"tags\": [\"中文\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"BAAI/bge-large-zh-v1.5\", \"label\": \"BGE Large(嵌入)\", \"tags\": [\"嵌入\", \"中文\"], \"contextWindow\": 512 },\n { \"id\": \"meta-llama/Meta-Llama-3.1-8B-Instruct\", \"label\": \"Llama 3.1 8B\", \"tags\": [\"通用\", \"开源\"], \"contextWindow\": 128000 },\n { \"id\": \"google/gemma-2-9b-it\", \"label\": \"Gemma 2 9B\", \"tags\": [\"通用\", \"开源\"], \"contextWindow\": 8192 }\n ]\n },\n \"baichuan\": {\n \"baseUrl\": \"https://api.baichuan-ai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"BAICHUAN_API_KEY\",\n \"defaultModel\": \"baichuan-4\",\n \"models\": [\n { \"id\": \"baichuan-4\", \"label\": \"百川 4\", \"tags\": [\"通用\", \"推理\", \"中文\"], \"contextWindow\": 96000 },\n { \"id\": \"baichuan-3-turbo\", \"label\": \"百川 3 Turbo\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 32768 }\n ]\n },\n \"minimax\": {\n \"baseUrl\": \"https://api.minimax.chat/v1/text/chatcompletion_v2\",\n \"apiKeyEnv\": \"MINIMAX_API_KEY\",\n \"defaultModel\": \"MiniMax-M2.5\",\n \"models\": [\n { \"id\": \"MiniMax-M2.5\", \"label\": \"MiniMax M2.5\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 1048576 },\n { \"id\": \"minimax-text-01\", \"label\": \"MiniMax Text 01\", \"tags\": [\"通用\", \"中文\", \"长文本\"], \"contextWindow\": 1048576 },\n { \"id\": \"abab-5.5s\", \"label\": \"Abab 5.5s\", \"tags\": [\"推理\", \"中文\"], \"contextWindow\": 8192 },\n { \"id\": \"abab-5.5\", \"label\": \"Abab 5.5\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8192 }\n ]\n },\n \"doubao\": {\n \"baseUrl\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n \"apiKeyEnv\": \"DOUBAO_API_KEY\",\n \"defaultModel\": \"doubao-pro-32k\",\n \"models\": [\n { \"id\": \"doubao-pro-32k\", \"label\": \"豆包 Pro 32K\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"doubao-pro-128k\", \"label\": \"豆包 Pro 128K\", \"tags\": [\"推理\", \"长文本\"], \"contextWindow\": 128000 },\n { \"id\": \"doubao-pro-256k\", \"label\": \"豆包 Pro 256K\", \"tags\": [\"推理\", \"超长文本\"], \"contextWindow\": 256000 },\n { \"id\": \"doubao-lite-32k\", \"label\": \"豆包 Lite 32K\", \"tags\": [\"轻量\", \"经济\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"doubao-lite-128k\", \"label\": \"豆包 Lite 128K\", \"tags\": [\"经济\", \"长文本\"], \"contextWindow\": 128000 },\n { \"id\": \"ep-20250312144904-5lzbr\", \"label\": \"豆包 1.5 Pro 256K\", \"tags\": [\"旗舰\", \"推理\", \"超长文本\"], \"contextWindow\": 256000 }\n ]\n },\n \"step\": {\n \"baseUrl\": \"https://api.stepfun.com/v1/chat/completions\",\n \"apiKeyEnv\": \"STEP_API_KEY\",\n \"defaultModel\": \"step-2-16k\",\n \"models\": [\n { \"id\": \"step-2-16k\", \"label\": \"阶跃星辰 Step-2\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 16000 },\n { \"id\": \"step-1-32k\", \"label\": \"阶跃星辰 Step-1 32K\", \"tags\": [\"均衡\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"step-1-flash\", \"label\": \"阶跃星辰 Step-1 Flash\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 8000 },\n { \"id\": \"step-2v-32k\", \"label\": \"阶跃星辰 Step-2V(视觉)\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 32000 }\n ]\n },\n \"hunyuan\": {\n \"baseUrl\": \"https://api.hunyuan.cloud.tencent.com/v1/chat/completions\",\n \"apiKeyEnv\": \"HUNYUAN_API_KEY\",\n \"defaultModel\": \"hunyuan-pro\",\n \"models\": [\n { \"id\": \"hunyuan-pro\", \"label\": \"混元 Pro\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-standard\", \"label\": \"混元 Standard\", \"tags\": [\"均衡\", \"通用\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-lite\", \"label\": \"混元 Lite\", \"tags\": [\"轻量\", \"经济\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-vision\", \"label\": \"混元 Vision\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-code\", \"label\": \"混元代码\", \"tags\": [\"代码\", \"推理\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-turbo\", \"label\": \"混元 Turbo\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 32768 }\n ]\n }\n};;\n\n// ========== 读取 AI 配置(支持备用提供商) ==========\nfunction readAIProviderConfig() {\n const fs = require('fs');\n const path = require('path');\n const BASE = __dirname;\n const cfgPath = path.join(BASE, '..', 'ai-provider.json');\n const config = { provider: 'ollama', fallbackProvider: null, fallbackModel: null };\n try {\n if (fs.existsSync(cfgPath)) {\n const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (raw.provider) config.provider = raw.provider;\n if (raw.fallbackProvider) config.fallbackProvider = raw.fallbackProvider;\n if (raw.fallbackModel) config.fallbackModel = raw.fallbackModel;\n // 仅当环境变量未设置时,才从配置文件读取作为兼容降级\n if (raw.apiKey) {\n const envKey = PROVIDERS[raw.provider]?.apiKeyEnv || 'DEEPSEEK_API_KEY';\n if (!process.env[envKey]) {\n let keyValue = raw.apiKey;\n // 解密 enc: 前缀的密钥\n if (keyValue && keyValue.startsWith('enc:')) {\n try {\n const { decrypt } = require('./key-vault');\n const fs = require('fs');\n const path = require('path');\n const masterKeyPath = path.join(BASE, '..', '.master-key');\n if (fs.existsSync(masterKeyPath)) {\n const masterKey = fs.readFileSync(masterKeyPath, 'utf8').trim();\n keyValue = decrypt(keyValue, masterKey);\n }\n } catch(ex) {\n console.error('[ai-engine] 解密失败:', ex.message);\n keyValue = '';\n }\n }\n if (keyValue) process.env[envKey] = keyValue;\n }\n }\n }\n } catch(e) { /* ignore */ }\n // Check env vars for fallback\n if (process.env.AI_FALLBACK_PROVIDER) config.fallbackProvider = process.env.AI_FALLBACK_PROVIDER;\n return config;\n}\n\n// ========== 执行一次 API 调用 ==========\nasync function callProviderOnce(providerName, messages, options) {\n // 每次调用前重新读取并解密 provider-keys.json\n try {\n const fs = require('fs');\n const path = require('path');\n const crypto = require('crypto');\n const pkPath = path.join(__dirname, '..', 'provider-keys.json');\n if (fs.existsSync(pkPath)) {\n const allKeys = JSON.parse(fs.readFileSync(pkPath, 'utf8'));\n const MASTER_KEY_PATH = path.join(__dirname, '..', '.master-key');\n let masterKey = null;\n if (fs.existsSync(MASTER_KEY_PATH)) masterKey = fs.readFileSync(MASTER_KEY_PATH, 'utf8').trim();\n for (const [provider, encryptedKey] of Object.entries(allKeys)) {\n if (!encryptedKey) continue;\n let decryptedKey = '';\n if (encryptedKey.startsWith('enc:') && masterKey) {\n try {\n const ENCRYPTION_KEY = crypto.createHash('sha256').update(masterKey).digest();\n const parts = encryptedKey.replace('enc:', '').split(':');\n const iv = Buffer.from(parts[0], 'hex');\n const encrypted = Buffer.from(parts[1], 'hex');\n const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv, { authTagLength: 16 });\n const authTag = encrypted.slice(-16);\n decipher.setAuthTag(authTag);\n const ciphertext = encrypted.slice(0, -16);\n decryptedKey = decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');\n } catch(ex) { continue; }\n } else if (!encryptedKey.startsWith('enc:')) {\n decryptedKey = encryptedKey;\n }\n if (decryptedKey) {\n const envKey = PROVIDERS[provider]?.apiKeyEnv || (provider.toUpperCase() + '_API_KEY');\n process.env[envKey] = decryptedKey;\n }\n }\n }\n } catch(e) { /* ignore */ }\n \n const { toolDefs, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n // 调试日志\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] providerName=' + providerName + '\\\\n'); } catch(ex) {}\n \n const providerCfg = PROVIDERS[providerName.toLowerCase()];\n if (!providerCfg) throw new Error(`不支持的 AI 提供商: ${providerName}`);\n \n // 调试日志:显示环境变量名和值(前10字符)\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] apiKeyEnv=' + providerCfg.apiKeyEnv + ' envValue=' + (process.env[providerCfg.apiKeyEnv] ? process.env[providerCfg.apiKeyEnv].substring(0,10) + '...' : '(empty)') + '\\\\n'); } catch(ex) {}\n \n const apiKey = process.env[providerCfg.apiKeyEnv];\n // 本地模型(如 Ollama)不需要 API Key\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv} 环境变量`);\n\n const body = {\n model: model || providerCfg.defaultModel,\n messages,\n temperature,\n max_tokens: maxTokens\n };\n if (toolDefs && toolDefs.length) {\n body.tools = toolDefs;\n body.tool_choice = 'auto';\n }\n\n var _retries = 0;\n var _lastErr = null;\n while (_retries < 3) {\n _retries++;\n if (_retries > 1) await new Promise(function(r) { setTimeout(r, (_retries - 1) * 1500); });\n try {\n const controller = new AbortController();\n var rt = setTimeout(function() { controller.abort(); }, timeout);\n // Provider-specific request adaptation\n var _url = providerCfg.baseUrl;\n var _headers = { 'Content-Type': 'application/json' };\n var _body = JSON.stringify(body);\n var pn = providerName.toLowerCase();\n // 本地模型(Ollama)不需要 Authorization 头\n if (!providerCfg.noApiKey) {\n if (pn === 'anthropic') {\n _headers['x-api-key'] = apiKey;\n _headers['anthropic-version'] = '2023-06-01';\n var sysMsg = '', anthMsgs = [];\n for (var _m of messages) {\n if (_m.role === 'system') { sysMsg = _m.content; continue; }\n if (_m.role === 'tool') continue;\n anthMsgs.push({ role: _m.role, content: _m.content });\n }\n var anthBody = { model: body.model, max_tokens: body.max_tokens, messages: anthMsgs };\n if (body.temperature !== undefined) anthBody.temperature = body.temperature;\n if (sysMsg) anthBody.system = sysMsg;\n _body = JSON.stringify(anthBody);\n } else if (pn === 'ernie') {\n var keyParts = apiKey.split('|');\n var cid = keyParts[0], csec = keyParts[1] || keyParts[0];\n var tokenRes = await fetch('https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + encodeURIComponent(cid) + '&client_secret=' + encodeURIComponent(csec), { signal: controller.signal });\n var tokenData = await tokenRes.json();\n if (tokenData.access_token) _url = providerCfg.baseUrl + '?access_token=' + tokenData.access_token;\n } else if (pn === 'zhipu' || pn === 'minimax' || pn === 'hunyuan') {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n } else {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n }\n }\n const res = await fetch(_url, {\n method: 'POST',\n headers: _headers,\n body: _body,\n signal: controller.signal\n });\n clearTimeout(rt);\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`${providerName} API ${res.status}: ${errText.substring(0, 200)}`);\n }\n return await res.json();\n } catch (err) {\n clearTimeout(timer);\n _lastErr = err;\n clearTimeout(timer);\n continue;\n }\n }\n throw _lastErr || new Error(providerName + ' 调用失败(已重试 3 次)');\n}\n\n// ========== AI 调用(带多提供商遍历降级) ==========\nasync function aiChat(messages, options = {}) {\n const { toolDefs, provider, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n\n const config = readAIProviderConfig();\n const activeProvider = provider || config.provider || process.env.AI_PROVIDER || 'ollama';\n\n // 第一优先级:显式指定的 provider\n if (provider) {\n return await callProviderOnce(provider, messages, options);\n }\n\n // 第二优先级:尝试主提供商\n try {\n const result = await callProviderOnce(activeProvider, messages, options);\n return result;\n } catch (primaryErr) {\n // 主提供商失败,尝试备用提供商\n const fallbackProvider = config.fallbackProvider || process.env.AI_FALLBACK_PROVIDER;\n if (fallbackProvider && fallbackProvider !== activeProvider) {\n try {\n const fallbackOptions = { ...options };\n if (config.fallbackModel) fallbackOptions.model = config.fallbackModel;\n const result = await callProviderOnce(fallbackProvider, messages, fallbackOptions);\n return result;\n } catch (fallbackErr) {\n throw new Error(`主提供商(${activeProvider})和备用提供商(${fallbackProvider})均失败: ${fallbackErr.message}`);\n }\n }\n // 主提供商失败,遍历所有其它已配置的提供商\n for (var _p of Object.keys(PROVIDERS)) {\n if (_p === activeProvider) continue;\n if (_p === fallbackProvider) continue;\n try {\n var _envKey = PROVIDERS[_p].apiKeyEnv;\n if (process.env[_envKey] || PROVIDERS[_p].noApiKey) {\n return await callProviderOnce(_p, messages, options);\n }\n } catch(_e) { /* try next */ }\n }\n throw primaryErr;\n }\n}\n\n// ========== 多轮对话(含工具调用) ==========\n// 延迟加载 execCEOTool(由 server-modern.js 注入全局)\nvar _execCEOTool = null;\ntry {\n var execTools = require('./executor-tools');\n if (execTools && typeof execTools.execCEOTool === 'function') {\n _execCEOTool = execTools.execCEOTool;\n }\n} catch(e) { /* fallback below */ }\nvar _execCEOToolFn = _execCEOTool || (typeof globalThis !== 'undefined' && globalThis.execCEOTool) || null;\n\nasync function aiChatWithTools(messages, tools, options = {}) {\n // 第一轮:调用模型(可能返回工具调用)\n const response = await aiChat(messages, { ...options, toolDefs: tools });\n if (!response.choices?.length) throw new Error('API 返回为空');\n\n const choice = response.choices[0];\n\n // 如果没有工具调用,直接返回\n if (choice.finish_reason !== 'tool_calls' || !choice.message?.tool_calls) {\n return { reply: choice.message?.content || '', toolCalls: [] };\n }\n\n // 执行工具调用\n const secondMessages = [...messages, choice.message];\n const toolResults = [];\n\n for (const tc of choice.message.tool_calls) {\n if (tc.type === 'function') {\n const funcName = tc.function.name;\n let funcArgs = {};\n try { funcArgs = JSON.parse(tc.function.arguments); } catch (e) {}\n\n toolResults.push({ name: funcName, args: funcArgs });\n\n // 执行实际工具函数\n var toolResult = '';\n try {\n if (_execCEOToolFn) {\n var raw = await Promise.resolve(_execCEOToolFn(funcName, funcArgs));\n if (raw && typeof raw === 'object') {\n toolResult = JSON.stringify(raw, null, 2);\n } else if (raw !== undefined) {\n toolResult = String(raw);\n } else {\n toolResult = 'null';\n }\n } else {\n toolResult = JSON.stringify({ note: 'execCEOTool不可用', args: funcArgs, funcName: funcName });\n }\n } catch(e) {\n toolResult = JSON.stringify({ error: e.message, args: funcArgs });\n }\n\n secondMessages.push({\n role: 'tool',\n tool_call_id: tc.id,\n content: toolResult\n });\n }\n }\n\n // 第二轮:获取最终回复\n const secondResponse = await aiChat(secondMessages, { ...options, toolDefs: tools });\n const finalReply = secondResponse.choices?.[0]?.message?.content || '';\n\n return { reply: finalReply, toolCalls: toolResults };\n}\n\n// ========== 流式输出 ==========\nasync function* aiChatStream(messages, options = {}) {\n const { provider, model } = options;\n const activeProvider = provider || process.env.AI_PROVIDER || 'ollama';\n const providerCfg = PROVIDERS[activeProvider];\n if (!providerCfg) throw new Error(`不支持的提供商: ${activeProvider}`);\n\n const apiKey = process.env[providerCfg.apiKeyEnv];\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv}`);\n\n const headers = { 'Content-Type': 'application/json' };\n if (!providerCfg.noApiKey) headers['Authorization'] = `Bearer ${apiKey}`;\n\n const res = await fetch(providerCfg.baseUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n model: model || providerCfg.defaultModel,\n messages,\n stream: true,\n temperature: options.temperature || 0.7,\n max_tokens: options.maxTokens || 4096\n })\n });\n\n if (!res.ok) throw new Error(`API ${res.status}`);\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6).trim();\n if (data === '[DONE]') return;\n try {\n const parsed = JSON.parse(data);\n const content = parsed.choices?.[0]?.delta?.content || '';\n if (content) yield content;\n } catch (e) { /* 跳过解析错误 */ }\n }\n }\n }\n}\n\nmodule.exports = {\n PROVIDERS,\n aiChat,\n aiChatWithTools,\n aiChatStream\n};\n"}, "b039c2bbf630c1d25b2d82a47e3f33c95f9e9fe5": {"t": 1785766820.6773844, "n": 26468, "h": "8ecba8cd01bb4033", "c": "/**\n * eCompany AI Engine\n * 多模型 AI 引擎 - 支持 DeepSeek / OpenAI / Anthropic / Ollama 等\n * 注入:多模型故障切换、流式输出、高级工具调用\n */\n\n// ========== 模型配置 ==========\nconst PROVIDERS = {\n \"ollama\": {\n \"baseUrl\": process.env.OLLAMA_BASE_URL || \"http://127.0.0.1:11434/v1/chat/completions\",\n \"apiKeyEnv\": \"OLLAMA_API_KEY\",\n \"defaultModel\": \"qwen3.5:9b\",\n \"noApiKey\": true,\n \"models\": [\n { \"id\": \"qwen3.5:9b\", \"label\": \"Qwen 3.5 9B (本地)\", \"tags\": [\"通用\", \"推理\", \"本地\"], \"contextWindow\": 32768 },\n { \"id\": \"deepseek-coder-v2:16b\", \"label\": \"DeepSeek Coder V2 16B (本地)\", \"tags\": [\"代码\", \"推理\", \"本地\"], \"contextWindow\": 32768 }\n ]\n },\n \"ernie\": {\n \"baseUrl\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions\",\n \"apiKeyEnv\": \"ERNIE_API_KEY\",\n \"defaultModel\": \"ernie-4.0-8k\",\n \"models\": [\n { \"id\": \"ernie-4.0-8k\", \"label\": \"文心4.0 8K\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-4.0-32k\", \"label\": \"文心4.0 32K\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 32000 },\n { \"id\": \"ernie-3.5-8k\", \"label\": \"文心3.5 8K\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-speed-128k\", \"label\": \"文心 Speed 128K\", \"tags\": [\"快速\", \"长文本\", \"经济\"], \"contextWindow\": 128000 },\n { \"id\": \"ernie-speed-8k\", \"label\": \"文心 Speed 8K\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-lite-8k\", \"label\": \"文心 Lite 8K\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 8000 }\n ]\n },\n \"yi\": {\n \"baseUrl\": \"https://api.01.ai/v1/chat/completions\",\n \"apiKeyEnv\": \"YI_API_KEY\",\n \"defaultModel\": \"yi-large\",\n \"models\": [\n { \"id\": \"yi-large\", \"label\": \"Yi Large\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 160000 },\n { \"id\": \"yi-large-turbo\", \"label\": \"Yi Large Turbo\", \"tags\": [\"推理\", \"快速\"], \"contextWindow\": 32000 },\n { \"id\": \"yi-medium\", \"label\": \"Yi Medium\", \"tags\": [\"通用\", \"均衡\"], \"contextWindow\": 32000 },\n { \"id\": \"yi-medium-turbo\", \"label\": \"Yi Medium Turbo\", \"tags\": [\"均衡\", \"快速\"], \"contextWindow\": 16000 },\n { \"id\": \"yi-spark\", \"label\": \"Yi Spark\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 16000 },\n { \"id\": \"yi-lightning\", \"label\": \"Yi Lightning\", \"tags\": [\"快速\", \"轻量\"], \"contextWindow\": 16000 }\n ]\n },\n \"deepseek\": {\n \"baseUrl\": \"https://api.deepseek.com/v1/chat/completions\",\n \"apiKeyEnv\": \"DEEPSEEK_API_KEY\",\n \"defaultModel\": \"deepseek-chat\",\n \"models\": [\n { \"id\": \"deepseek-chat\", \"label\": \"DeepSeek Chat V3(⚠️ 2026/07/24 弃用)\", \"tags\": [\"通用\", \"快速\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"deepseek-reasoner\", \"label\": \"DeepSeek R1(⚠️ 2026/07/24 弃用)\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 64000 },\n { \"id\": \"deepseek-v4-flash\", \"label\": \"DeepSeek V4 Flash(轻量版)🌟 推荐\", \"tags\": [\"快速\", \"低成本\", \"轻量\"], \"contextWindow\": 1000000 },\n { \"id\": \"deepseek-v4-pro\", \"label\": \"DeepSeek V4 Pro(专业版)\", \"tags\": [\"推理\", \"深度思考\", \"代码\", \"数学\"], \"contextWindow\": 1000000 }\n ]\n },\n \"openai\": {\n \"baseUrl\": \"https://api.openai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENAI_API_KEY\",\n \"defaultModel\": \"gpt-4o\",\n \"models\": [\n { \"id\": \"gpt-4o\", \"label\": \"GPT-4o\", \"tags\": [\"通用\", \"视觉\", \"推理\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4o-mini\", \"label\": \"GPT-4o Mini\", \"tags\": [\"快速\", \"低成本\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4o-mini-high\", \"label\": \"GPT-4o Mini High\", \"tags\": [\"低成本\", \"高精度\"], \"contextWindow\": 128000 },\n { \"id\": \"o1\", \"label\": \"O1\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 200000 },\n { \"id\": \"o3-mini\", \"label\": \"O3 Mini\", \"tags\": [\"推理\", \"快速\"], \"contextWindow\": 200000 },\n { \"id\": \"gpt-4-turbo\", \"label\": \"GPT-4 Turbo\", \"tags\": [\"通用\", \"高精度\"], \"contextWindow\": 128000 },\n { \"id\": \"gpt-4\", \"label\": \"GPT-4\", \"tags\": [\"通用\", \"稳定\"], \"contextWindow\": 8192 }\n ]\n },\n \"openrouter\": {\n \"baseUrl\": \"https://openrouter.ai/api/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENROUTER_API_KEY\",\n \"defaultModel\": \"openrouter/auto\",\n \"models\": [\n { \"id\": \"openrouter/auto\", \"label\": \"OpenRouter Auto\", \"tags\": [\"自动选择\"], \"contextWindow\": 128000 },\n { \"id\": \"openrouter/deepseek\", \"label\": \"DeepSeek via OpenRouter\", \"tags\": [\"推理\"], \"contextWindow\": 128000 }\n ]\n },\n \"anthropic\": {\n \"baseUrl\": \"https://api.anthropic.com/v1/messages\",\n \"apiKeyEnv\": \"ANTHROPIC_API_KEY\",\n \"defaultModel\": \"claude-sonnet-4-20250514\",\n \"models\": [\n { \"id\": \"claude-sonnet-4-20250514\", \"label\": \"Claude Sonnet 4\", \"tags\": [\"推理\", \"通用\", \"代码\", \"长文本\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-5-sonnet-20241022\", \"label\": \"Claude 3.5 Sonnet\", \"tags\": [\"推理\", \"通用\", \"代码\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-opus-4-20250514\", \"label\": \"Claude Opus 4\", \"tags\": [\"深度思考\", \"推理\", \"高精度\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-5-haiku-20241022\", \"label\": \"Claude 3.5 Haiku\", \"tags\": [\"快速\", \"低成本\", \"轻量\"], \"contextWindow\": 200000 },\n { \"id\": \"claude-3-haiku-20240307\", \"label\": \"Claude 3 Haiku\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 200000 }\n ]\n },\n \"google\": {\n \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n \"apiKeyEnv\": \"GOOGLE_API_KEY\",\n \"defaultModel\": \"gemini-2.5-flash\",\n \"models\": [\n { \"id\": \"gemini-2.5-flash\", \"label\": \"Gemini 2.5 Flash\", \"tags\": [\"快速\", \"多模态\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.5-pro\", \"label\": \"Gemini 2.5 Pro\", \"tags\": [\"推理\", \"多模态\", \"深度思考\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.0-flash\", \"label\": \"Gemini 2.0 Flash\", \"tags\": [\"快速\", \"多模态\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-1.5-pro\", \"label\": \"Gemini 1.5 Pro\", \"tags\": [\"推理\", \"稳定\"], \"contextWindow\": 2000000 },\n { \"id\": \"gemini-1.5-flash\", \"label\": \"Gemini 1.5 Flash\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 1000000 }\n ]\n },\n \"gemini\": {\n \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n \"apiKeyEnv\": \"GEMINI_API_KEY\",\n \"defaultModel\": \"gemini-2.5-flash\",\n \"models\": [\n { \"id\": \"gemini-2.5-flash\", \"label\": \"Gemini 2.5 Flash\", \"tags\": [\"快速\", \"多模态\", \"低成本\"], \"contextWindow\": 1000000 },\n { \"id\": \"gemini-2.5-pro\", \"label\": \"Gemini 2.5 Pro\", \"tags\": [\"推理\", \"多模态\", \"深度思考\"], \"contextWindow\": 1000000 }\n ]\n },\n \"moonshot\": {\n \"baseUrl\": \"https://api.moonshot.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"MOONSHOT_API_KEY\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"models\": [\n { \"id\": \"moonshot-v1-8k\", \"label\": \"Moonshot 8K\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"moonshot-v1-32k\", \"label\": \"Moonshot 32K\", \"tags\": [\"通用\", \"长文本\"], \"contextWindow\": 32000 },\n { \"id\": \"moonshot-v1-128k\", \"label\": \"Moonshot 128K\", \"tags\": [\"长文本\", \"深度\"], \"contextWindow\": 128000 }\n ]\n },\n \"tongyi\": {\n \"baseUrl\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n \"apiKeyEnv\": \"DASHSCOPE_API_KEY\",\n \"defaultModel\": \"qwen3.7-max\",\n \"models\": [\n { \"id\": \"qwen3.7-max\", \"label\": \"Qwen 3.7 Max(最新旗舰)\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.7-plus\", \"label\": \"Qwen 3.7 Plus(均衡型)\", \"tags\": [\"均衡\", \"通用\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.6-flash\", \"label\": \"Qwen 3.6 Flash(快速型)\", \"tags\": [\"快速\", \"低成本\", \"中文\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen3.6-flash-lite\", \"label\": \"Qwen 3.6 Flash Lite\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-max\", \"label\": \"通义千问 Max(稳定版)\", \"tags\": [\"推理\", \"稳定\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"qwen-plus\", \"label\": \"通义千问 Plus\", \"tags\": [\"均衡\", \"通用\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-turbo\", \"label\": \"通义千问 Turbo\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 131072 },\n { \"id\": \"qwen-vl-max\", \"label\": \"通义千问 VL Max(视觉多模态)\", \"tags\": [\"视觉\", \"多模态\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"qwen2.5-72b-instruct\", \"label\": \"Qwen 2.5 72B\", \"tags\": [\"推理\", \"开源\", \"中文\"], \"contextWindow\": 131072 }\n ]\n },\n \"zhipu\": {\n \"baseUrl\": \"https://open.bigmodel.cn/api/paas/v4/chat/completions\",\n \"apiKeyEnv\": \"ZHIPUAI_API_KEY\",\n \"defaultModel\": \"glm-4-plus\",\n \"models\": [\n { \"id\": \"glm-4-plus\", \"label\": \"GLM-4 Plus\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4\", \"label\": \"GLM-4\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-flash\", \"label\": \"GLM-4 Flash\", \"tags\": [\"快速\", \"经济\", \"中文\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-flashx\", \"label\": \"GLM-4 FlashX\", \"tags\": [\"快速\", \"轻量\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-air\", \"label\": \"GLM-4 Air\", \"tags\": [\"轻量\", \"经济\"], \"contextWindow\": 128000 },\n { \"id\": \"glm-4-long\", \"label\": \"GLM-4 Long(长文本)\", \"tags\": [\"长文本\", \"深度\"], \"contextWindow\": 1048576 },\n { \"id\": \"glm-4v-plus\", \"label\": \"GLM-4V Plus(视觉)\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 128000 }\n ]\n },\n \"siliconflow\": {\n \"baseUrl\": \"https://api.siliconflow.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"SILICONFLOW_API_KEY\",\n \"defaultModel\": \"Pro/DeepSeek-V3\",\n \"models\": [\n { \"id\": \"Pro/DeepSeek-V3\", \"label\": \"DeepSeek V3\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 64000 },\n { \"id\": \"Pro/DeepSeek-R1\", \"label\": \"DeepSeek R1\", \"tags\": [\"推理\", \"深度思考\"], \"contextWindow\": 64000 },\n { \"id\": \"Qwen/Qwen2.5-72B-Instruct\", \"label\": \"Qwen 2.5 72B\", \"tags\": [\"推理\", \"开源\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-32B-Instruct\", \"label\": \"Qwen 2.5 32B\", \"tags\": [\"均衡\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-14B-Instruct\", \"label\": \"Qwen 2.5 14B\", \"tags\": [\"快速\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"Qwen/Qwen2.5-7B-Instruct\", \"label\": \"Qwen 2.5 7B\", \"tags\": [\"轻量\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"THUDM/glm-4-9b-chat\", \"label\": \"GLM-4 9B\", \"tags\": [\"中文\", \"开源\"], \"contextWindow\": 128000 },\n { \"id\": \"internlm/internlm2_5-7b-chat\", \"label\": \"InternLM 2.5 7B\", \"tags\": [\"中文\", \"开源\"], \"contextWindow\": 32768 },\n { \"id\": \"BAAI/bge-large-zh-v1.5\", \"label\": \"BGE Large(嵌入)\", \"tags\": [\"嵌入\", \"中文\"], \"contextWindow\": 512 },\n { \"id\": \"meta-llama/Meta-Llama-3.1-8B-Instruct\", \"label\": \"Llama 3.1 8B\", \"tags\": [\"通用\", \"开源\"], \"contextWindow\": 128000 },\n { \"id\": \"google/gemma-2-9b-it\", \"label\": \"Gemma 2 9B\", \"tags\": [\"通用\", \"开源\"], \"contextWindow\": 8192 }\n ]\n },\n \"baichuan\": {\n \"baseUrl\": \"https://api.baichuan-ai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"BAICHUAN_API_KEY\",\n \"defaultModel\": \"baichuan-4\",\n \"models\": [\n { \"id\": \"baichuan-4\", \"label\": \"百川 4\", \"tags\": [\"通用\", \"推理\", \"中文\"], \"contextWindow\": 96000 },\n { \"id\": \"baichuan-3-turbo\", \"label\": \"百川 3 Turbo\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 32768 }\n ]\n },\n \"minimax\": {\n \"baseUrl\": \"https://api.minimax.chat/v1/text/chatcompletion_v2\",\n \"apiKeyEnv\": \"MINIMAX_API_KEY\",\n \"defaultModel\": \"MiniMax-M2.5\",\n \"models\": [\n { \"id\": \"MiniMax-M2.5\", \"label\": \"MiniMax M2.5\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 1048576 },\n { \"id\": \"minimax-text-01\", \"label\": \"MiniMax Text 01\", \"tags\": [\"通用\", \"中文\", \"长文本\"], \"contextWindow\": 1048576 },\n { \"id\": \"abab-5.5s\", \"label\": \"Abab 5.5s\", \"tags\": [\"推理\", \"中文\"], \"contextWindow\": 8192 },\n { \"id\": \"abab-5.5\", \"label\": \"Abab 5.5\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8192 }\n ]\n },\n \"doubao\": {\n \"baseUrl\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n \"apiKeyEnv\": \"DOUBAO_API_KEY\",\n \"defaultModel\": \"doubao-pro-32k\",\n \"models\": [\n { \"id\": \"doubao-pro-32k\", \"label\": \"豆包 Pro 32K\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"doubao-pro-128k\", \"label\": \"豆包 Pro 128K\", \"tags\": [\"推理\", \"长文本\"], \"contextWindow\": 128000 },\n { \"id\": \"doubao-pro-256k\", \"label\": \"豆包 Pro 256K\", \"tags\": [\"推理\", \"超长文本\"], \"contextWindow\": 256000 },\n { \"id\": \"doubao-lite-32k\", \"label\": \"豆包 Lite 32K\", \"tags\": [\"轻量\", \"经济\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"doubao-lite-128k\", \"label\": \"豆包 Lite 128K\", \"tags\": [\"经济\", \"长文本\"], \"contextWindow\": 128000 },\n { \"id\": \"ep-20250312144904-5lzbr\", \"label\": \"豆包 1.5 Pro 256K\", \"tags\": [\"旗舰\", \"推理\", \"超长文本\"], \"contextWindow\": 256000 }\n ]\n },\n \"step\": {\n \"baseUrl\": \"https://api.stepfun.com/v1/chat/completions\",\n \"apiKeyEnv\": \"STEP_API_KEY\",\n \"defaultModel\": \"step-2-16k\",\n \"models\": [\n { \"id\": \"step-2-16k\", \"label\": \"阶跃星辰 Step-2\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 16000 },\n { \"id\": \"step-1-32k\", \"label\": \"阶跃星辰 Step-1 32K\", \"tags\": [\"均衡\", \"中文\"], \"contextWindow\": 32000 },\n { \"id\": \"step-1-flash\", \"label\": \"阶跃星辰 Step-1 Flash\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 8000 },\n { \"id\": \"step-2v-32k\", \"label\": \"阶跃星辰 Step-2V(视觉)\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 32000 }\n ]\n },\n \"hunyuan\": {\n \"baseUrl\": \"https://api.hunyuan.cloud.tencent.com/v1/chat/completions\",\n \"apiKeyEnv\": \"HUNYUAN_API_KEY\",\n \"defaultModel\": \"hunyuan-pro\",\n \"models\": [\n { \"id\": \"hunyuan-pro\", \"label\": \"混元 Pro\", \"tags\": [\"推理\", \"高精度\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-standard\", \"label\": \"混元 Standard\", \"tags\": [\"均衡\", \"通用\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-lite\", \"label\": \"混元 Lite\", \"tags\": [\"轻量\", \"经济\", \"中文\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-vision\", \"label\": \"混元 Vision\", \"tags\": [\"视觉\", \"多模态\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-code\", \"label\": \"混元代码\", \"tags\": [\"代码\", \"推理\"], \"contextWindow\": 32768 },\n { \"id\": \"hunyuan-turbo\", \"label\": \"混元 Turbo\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 32768 }\n ]\n }\n};;\n\n// ========== 读取 AI 配置(支持备用提供商) ==========\nfunction readAIProviderConfig() {\n const fs = require('fs');\n const path = require('path');\n const BASE = __dirname;\n const cfgPath = path.join(BASE, '..', 'ai-provider.json');\n const config = { provider: 'ollama', fallbackProvider: null, fallbackModel: null };\n try {\n if (fs.existsSync(cfgPath)) {\n const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (raw.provider) config.provider = raw.provider;\n if (raw.fallbackProvider) config.fallbackProvider = raw.fallbackProvider;\n if (raw.fallbackModel) config.fallbackModel = raw.fallbackModel;\n // 仅当环境变量未设置时,才从配置文件读取作为兼容降级\n if (raw.apiKey) {\n const envKey = PROVIDERS[raw.provider]?.apiKeyEnv || 'DEEPSEEK_API_KEY';\n if (!process.env[envKey]) {\n let keyValue = raw.apiKey;\n // 解密 enc: 前缀的密钥\n if (keyValue && keyValue.startsWith('enc:')) {\n try {\n const { decrypt } = require('./key-vault');\n const fs = require('fs');\n const path = require('path');\n const masterKeyPath = path.join(BASE, '..', '.master-key');\n if (fs.existsSync(masterKeyPath)) {\n const masterKey = fs.readFileSync(masterKeyPath, 'utf8').trim();\n keyValue = decrypt(keyValue, masterKey);\n }\n } catch(ex) {\n console.error('[ai-engine] 解密失败:', ex.message);\n keyValue = '';\n }\n }\n if (keyValue) process.env[envKey] = keyValue;\n }\n }\n }\n } catch(e) { /* ignore */ }\n // Check env vars for fallback\n if (process.env.AI_FALLBACK_PROVIDER) config.fallbackProvider = process.env.AI_FALLBACK_PROVIDER;\n return config;\n}\n\n// ========== 执行一次 API 调用 ==========\nasync function callProviderOnce(providerName, messages, options) {\n // 每次调用前重新读取并解密 provider-keys.json\n try {\n const fs = require('fs');\n const path = require('path');\n const crypto = require('crypto');\n const pkPath = path.join(__dirname, '..', 'provider-keys.json');\n if (fs.existsSync(pkPath)) {\n const allKeys = JSON.parse(fs.readFileSync(pkPath, 'utf8'));\n const MASTER_KEY_PATH = path.join(__dirname, '..', '.master-key');\n let masterKey = null;\n if (fs.existsSync(MASTER_KEY_PATH)) masterKey = fs.readFileSync(MASTER_KEY_PATH, 'utf8').trim();\n for (const [provider, encryptedKey] of Object.entries(allKeys)) {\n if (!encryptedKey) continue;\n let decryptedKey = '';\n if (encryptedKey.startsWith('enc:') && masterKey) {\n try {\n const ENCRYPTION_KEY = crypto.createHash('sha256').update(masterKey).digest();\n const parts = encryptedKey.replace('enc:', '').split(':');\n const iv = Buffer.from(parts[0], 'hex');\n const encrypted = Buffer.from(parts[1], 'hex');\n const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv, { authTagLength: 16 });\n const authTag = encrypted.slice(-16);\n decipher.setAuthTag(authTag);\n const ciphertext = encrypted.slice(0, -16);\n decryptedKey = decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');\n } catch(ex) { continue; }\n } else if (!encryptedKey.startsWith('enc:')) {\n decryptedKey = encryptedKey;\n }\n if (decryptedKey) {\n const envKey = PROVIDERS[provider]?.apiKeyEnv || (provider.toUpperCase() + '_API_KEY');\n process.env[envKey] = decryptedKey;\n }\n }\n }\n } catch(e) { /* ignore */ }\n \n const { toolDefs, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n // 调试日志\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] providerName=' + providerName + '\\\\n'); } catch(ex) {}\n \n const providerCfg = PROVIDERS[providerName.toLowerCase()];\n if (!providerCfg) throw new Error(`不支持的 AI 提供商: ${providerName}`);\n \n // 调试日志:显示环境变量名和值(前10字符)\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] apiKeyEnv=' + providerCfg.apiKeyEnv + ' envValue=' + (process.env[providerCfg.apiKeyEnv] ? process.env[providerCfg.apiKeyEnv].substring(0,10) + '...' : '(empty)') + '\\\\n'); } catch(ex) {}\n \n const apiKey = process.env[providerCfg.apiKeyEnv];\n // 本地模型(如 Ollama)不需要 API Key\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv} 环境变量`);\n\n const body = {\n model: model || providerCfg.defaultModel,\n messages,\n temperature,\n max_tokens: maxTokens\n };\n if (toolDefs && toolDefs.length) {\n body.tools = toolDefs;\n body.tool_choice = 'auto';\n }\n\n var _retries = 0;\n var _lastErr = null;\n while (_retries < 3) {\n _retries++;\n if (_retries > 1) await new Promise(function(r) { setTimeout(r, (_retries - 1) * 1500); });\n try {\n const controller = new AbortController();\n var rt = setTimeout(function() { controller.abort(); }, timeout);\n // Provider-specific request adaptation\n var _url = providerCfg.baseUrl;\n var _headers = { 'Content-Type': 'application/json' };\n var _body = JSON.stringify(body);\n var pn = providerName.toLowerCase();\n // 本地模型(Ollama)不需要 Authorization 头\n if (!providerCfg.noApiKey) {\n if (pn === 'anthropic') {\n _headers['x-api-key'] = apiKey;\n _headers['anthropic-version'] = '2023-06-01';\n var sysMsg = '', anthMsgs = [];\n for (var _m of messages) {\n if (_m.role === 'system') { sysMsg = _m.content; continue; }\n if (_m.role === 'tool') continue;\n anthMsgs.push({ role: _m.role, content: _m.content });\n }\n var anthBody = { model: body.model, max_tokens: body.max_tokens, messages: anthMsgs };\n if (body.temperature !== undefined) anthBody.temperature = body.temperature;\n if (sysMsg) anthBody.system = sysMsg;\n _body = JSON.stringify(anthBody);\n } else if (pn === 'ernie') {\n var keyParts = apiKey.split('|');\n var cid = keyParts[0], csec = keyParts[1] || keyParts[0];\n var tokenRes = await fetch('https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + encodeURIComponent(cid) + '&client_secret=' + encodeURIComponent(csec), { signal: controller.signal });\n var tokenData = await tokenRes.json();\n if (tokenData.access_token) _url = providerCfg.baseUrl + '?access_token=' + tokenData.access_token;\n } else if (pn === 'zhipu' || pn === 'minimax' || pn === 'hunyuan') {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n } else {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n }\n }\n const res = await fetch(_url, {\n method: 'POST',\n headers: _headers,\n body: _body,\n signal: controller.signal\n });\n clearTimeout(rt);\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`${providerName} API ${res.status}: ${errText.substring(0, 200)}`);\n }\n return await res.json();\n } catch (err) {\n clearTimeout(timer);\n _lastErr = err;\n clearTimeout(timer);\n continue;\n }\n }\n throw _lastErr || new Error(providerName + ' 调用失败(已重试 3 次)');\n}\n\n// ========== AI 调用(带多提供商遍历降级) ==========\nasync function aiChat(messages, options = {}) {\n const { toolDefs, provider, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n\n const config = readAIProviderConfig();\n const activeProvider = provider || config.provider || process.env.AI_PROVIDER || 'ollama';\n\n // 第一优先级:显式指定的 provider\n if (provider) {\n return await callProviderOnce(provider, messages, options);\n }\n\n // 第二优先级:尝试主提供商\n try {\n const result = await callProviderOnce(activeProvider, messages, options);\n return result;\n } catch (primaryErr) {\n // 主提供商失败,尝试备用提供商\n const fallbackProvider = config.fallbackProvider || process.env.AI_FALLBACK_PROVIDER;\n if (fallbackProvider && fallbackProvider !== activeProvider) {\n try {\n const fallbackOptions = { ...options };\n if (config.fallbackModel) fallbackOptions.model = config.fallbackModel;\n const result = await callProviderOnce(fallbackProvider, messages, fallbackOptions);\n return result;\n } catch (fallbackErr) {\n throw new Error(`主提供商(${activeProvider})和备用提供商(${fallbackProvider})均失败: ${fallbackErr.message}`);\n }\n }\n // 主提供商失败,遍历所有其它已配置的提供商\n for (var _p of Object.keys(PROVIDERS)) {\n if (_p === activeProvider) continue;\n if (_p === fallbackProvider) continue;\n try {\n var _envKey = PROVIDERS[_p].apiKeyEnv;\n if (process.env[_envKey] || PROVIDERS[_p].noApiKey) {\n return await callProviderOnce(_p, messages, options);\n }\n } catch(_e) { /* try next */ }\n }\n throw primaryErr;\n }\n}\n\n// ========== 多轮对话(含工具调用) ==========\n// 延迟加载 execCEOTool(由 server-modern.js 注入全局)\nvar _execCEOTool = null;\ntry {\n var execTools = require('./executor-tools');\n if (execTools && typeof execTools.execCEOTool === 'function') {\n _execCEOTool = execTools.execCEOTool;\n }\n} catch(e) { /* fallback below */ }\nvar _execCEOToolFn = _execCEOTool || (typeof globalThis !== 'undefined' && globalThis.execCEOTool) || null;\n\nasync function aiChatWithTools(messages, tools, options = {}) {\n // 第一轮:调用模型(可能返回工具调用)\n const response = await aiChat(messages, { ...options, toolDefs: tools });\n if (!response.choices?.length) throw new Error('API 返回为空');\n\n const choice = response.choices[0];\n\n // 如果没有工具调用,直接返回\n if (choice.finish_reason !== 'tool_calls' || !choice.message?.tool_calls) {\n return { reply: choice.message?.content || '', toolCalls: [] };\n }\n\n // 执行工具调用\n const secondMessages = [...messages, choice.message];\n const toolResults = [];\n\n for (const tc of choice.message.tool_calls) {\n if (tc.type === 'function') {\n const funcName = tc.function.name;\n let funcArgs = {};\n try { funcArgs = JSON.parse(tc.function.arguments); } catch (e) {}\n\n toolResults.push({ name: funcName, args: funcArgs });\n\n // 执行实际工具函数\n var toolResult = '';\n try {\n if (_execCEOToolFn) {\n var raw = await Promise.resolve(_execCEOToolFn(funcName, funcArgs));\n if (raw && typeof raw === 'object') {\n toolResult = JSON.stringify(raw, null, 2);\n } else if (raw !== undefined) {\n toolResult = String(raw);\n } else {\n toolResult = 'null';\n }\n } else {\n toolResult = JSON.stringify({ note: 'execCEOTool不可用', args: funcArgs, funcName: funcName });\n }\n } catch(e) {\n toolResult = JSON.stringify({ error: e.message, args: funcArgs });\n }\n\n secondMessages.push({\n role: 'tool',\n tool_call_id: tc.id,\n content: toolResult\n });\n }\n }\n\n // 第二轮:获取最终回复\n const secondResponse = await aiChat(secondMessages, { ...options, toolDefs: tools });\n const finalReply = secondResponse.choices?.[0]?.message?.content || '';\n\n return { reply: finalReply, toolCalls: toolResults };\n}\n\n// ========== 流式输出 ==========\nasync function* aiChatStream(messages, options = {}) {\n const { provider, model } = options;\n const activeProvider = provider || process.env.AI_PROVIDER || 'ollama';\n const providerCfg = PROVIDERS[activeProvider];\n if (!providerCfg) throw new Error(`不支持的提供商: ${activeProvider}`);\n\n const apiKey = process.env[providerCfg.apiKeyEnv];\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv}`);\n\n const headers = { 'Content-Type': 'application/json' };\n if (!providerCfg.noApiKey) headers['Authorization'] = `Bearer ${apiKey}`;\n\n const res = await fetch(providerCfg.baseUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n model: model || providerCfg.defaultModel,\n messages,\n stream: true,\n temperature: options.temperature || 0.7,\n max_tokens: options.maxTokens || 4096\n })\n });\n\n if (!res.ok) throw new Error(`API ${res.status}`);\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6).trim();\n if (data === '[DONE]') return;\n try {\n const parsed = JSON.parse(data);\n const content = parsed.choices?.[0]?.delta?.content || '';\n if (content) yield content;\n } catch (e) { /* 跳过解析错误 */ }\n }\n }\n }\n}\n\nmodule.exports = {\n PROVIDERS,\n aiChat,\n aiChatWithTools,\n aiChatStream\n};\n"}, "31e81b020b05869ec1d80e262adc3d1f3b7530c6": {"t": 1785766823.9041302, "n": 21975, "h": "cb36b849c896e616", "c": "/**\n * eCompany AI Engine\n * 多模型 AI 引擎 - 支持 DeepSeek / OpenAI / Anthropic / Ollama 等\n * 注入:多模型故障切换、流式输出、高级工具调用\n */\n\n// ========== 模型配置 ==========\nconst PROVIDERS = {\n \"ollama\": {\n \"baseUrl\": \"http://127.0.0.1:11434/v1/chat/completions\",\n \"apiKeyEnv\": \"OLLAMA_API_KEY\",\n \"defaultModel\": \"qwen3.5:9b\",\n \"noApiKey\": true,\n \"models\": [\n { \"id\": \"qwen3.5:9b\", \"label\": \"Qwen 3.5 9B (本地)\", \"tags\": [\"通用\", \"推理\", \"本地\"], \"contextWindow\": 32768 },\n { \"id\": \"deepseek-coder-v2:16b\", \"label\": \"DeepSeek Coder V2 16B (本地)\", \"tags\": [\"代码\", \"推理\", \"本地\"], \"contextWindow\": 32768 }\n ]\n },\n \"ernie\": {\n \"baseUrl\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions\",\n \"apiKeyEnv\": \"ERNIE_API_KEY\",\n \"defaultModel\": \"ernie-4.0-8k\",\n \"models\": [\n { \"id\": \"ernie-4.0-8k\", \"label\": \"文心4.0 8K\", \"tags\": [\"推理\", \"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-4.0-32k\", \"label\": \"文心4.0 32K\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 32000 },\n { \"id\": \"ernie-bot\", \"label\": \"文心一言3.5\", \"tags\": [\"通用\", \"中文\"], \"contextWindow\": 8000 },\n { \"id\": \"ernie-speed-8k\", \"label\": \"文心 Speed 8K\", \"tags\": [\"快速\", \"推理\"], \"contextWindow\": 8000 }\n ]\n },\n \"yi\": {\n \"baseUrl\": \"https://api.01.ai/v1/chat/completions\",\n \"apiKeyEnv\": \"YI_API_KEY\",\n \"defaultModel\": \"yi-large\",\n \"models\": [\n { \"id\": \"yi-large\", \"label\": \"Yi Large\", \"tags\": [\"推理\", \"通用\", \"长文本\"], \"contextWindow\": 160000 },\n { \"id\": \"yi-medium\", \"label\": \"Yi Medium\", \"tags\": [\"通用\", \"均衡\"], \"contextWindow\": 32000 },\n { \"id\": \"yi-spark\", \"label\": \"Yi Spark\", \"tags\": [\"快速\", \"经济\"], \"contextWindow\": 16000 }\n ]\n },\n\"deepseek\": {\n \"baseUrl\": \"https://api.deepseek.com/v1/chat/completions\",\n \"apiKeyEnv\": \"DEEPSEEK_API_KEY\",\n \"defaultModel\": \"deepseek-v4-flash\",\n \"models\": [\n {\n \"id\": \"deepseek-v4-flash\",\n \"label\": \"DeepSeek V4 Flash(快速版)\",\n \"tags\": [\"通用\", \"快速\", \"低成本\"],\n \"contextWindow\": 128000\n },\n {\n \"id\": \"deepseek-v4-pro\",\n \"label\": \"DeepSeek V4 Pro(专业版)\",\n \"tags\": [\"推理\", \"深度思考\", \"代码\", \"数学\"],\n \"contextWindow\": 128000\n }\n ]\n },\n \"openai\": {\n \"baseUrl\": \"https://api.openai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENAI_API_KEY\",\n \"defaultModel\": \"gpt-4o\",\n \"models\": [\n {\n \"id\": \"gpt-4o\",\n \"label\": \"GPT-4o\",\n \"tags\": [\n \"通用\",\n \"视觉\",\n \"推理\"\n ],\n \"contextWindow\": 128000\n },\n {\n \"id\": \"gpt-4o-mini\",\n \"label\": \"GPT-4o Mini\",\n \"tags\": [\n \"快速\",\n \"低成本\"\n ],\n \"contextWindow\": 128000\n },\n {\n \"id\": \"o1\",\n \"label\": \"O1\",\n \"tags\": [\n \"推理\",\n \"深度思考\"\n ],\n \"contextWindow\": 200000\n }\n ]\n },\n \"openrouter\": {\n \"baseUrl\": \"https://openrouter.ai/api/v1/chat/completions\",\n \"apiKeyEnv\": \"OPENROUTER_API_KEY\",\n \"defaultModel\": \"openrouter/auto\",\n \"models\": [\n {\n \"id\": \"openrouter/auto\",\n \"label\": \"OpenRouter Auto\",\n \"tags\": [\n \"自动选择\"\n ]\n },\n {\n \"id\": \"openrouter/deepseek\",\n \"label\": \"DeepSeek via OpenRouter\",\n \"tags\": [\n \"推理\"\n ]\n }\n ]\n },\n \"anthropic\": {\n \"baseUrl\": \"https://api.anthropic.com/v1/messages\",\n \"apiKeyEnv\": \"ANTHROPIC_API_KEY\",\n \"defaultModel\": \"claude-sonnet-4-20250514\",\n \"models\": [\n {\n \"id\": \"claude-sonnet-4-20250514\",\n \"label\": \"Claude Sonnet 4\",\n \"tags\": [\n \"推理\",\n \"代码\",\n \"长文本\"\n ],\n \"contextWindow\": 200000\n },\n {\n \"id\": \"claude-3-5-sonnet-20241022\",\n \"label\": \"Claude 3.5 Sonnet\",\n \"tags\": [\n \"推理\",\n \"代码\",\n \"视觉\"\n ],\n \"contextWindow\": 200000\n },\n {\n \"id\": \"claude-opus-4-20250514\",\n \"label\": \"Claude Opus 4\",\n \"tags\": [\n \"强推理\",\n \"复杂\"\n ],\n \"contextWindow\": 200000\n }\n ]\n },\n \"google\": {\n \"baseUrl\": \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n \"apiKeyEnv\": \"GEMINI_API_KEY\",\n \"defaultModel\": \"gemini-2.5-flash\",\n \"models\": [\n {\n \"id\": \"gemini-2.5-flash\",\n \"label\": \"Gemini 2.5 Flash\",\n \"tags\": [\n \"快速\",\n \"视觉\",\n \"多模态\"\n ],\n \"contextWindow\": 1000000\n },\n {\n \"id\": \"gemini-2.5-pro\",\n \"label\": \"Gemini 2.5 Pro\",\n \"tags\": [\n \"推理\",\n \"长文本\",\n \"多模态\"\n ],\n \"contextWindow\": 1000000\n }\n ]\n },\n \"moonshot\": {\n \"baseUrl\": \"https://api.moonshot.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"MOONSHOT_API_KEY\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"models\": [\n {\n \"id\": \"moonshot-v1-8k\",\n \"label\": \"Moonshot 8K\",\n \"tags\": [\n \"快速\",\n \"文本\"\n ]\n },\n {\n \"id\": \"moonshot-v1-32k\",\n \"label\": \"Moonshot 32K\",\n \"tags\": [\n \"通用\",\n \"文本\"\n ]\n },\n {\n \"id\": \"moonshot-v1-128k\",\n \"label\": \"Moonshot 128K\",\n \"tags\": [\n \"长文本\"\n ]\n }\n ]\n },\n \"tongyi\": {\n \"baseUrl\": \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n \"apiKeyEnv\": \"TONGYI_API_KEY\",\n \"defaultModel\": \"qwen3.7-max\",\n \"visionModel\": \"qwen-vl-max\",\n \"models\": [\n {\n \"id\": \"qwen3.7-max\",\n \"label\": \"Qwen 3.7 Max(最新版)\",\n \"tags\": [\"推理\", \"通用\", \"最新\"],\n \"contextWindow\": 128000\n },\n {\n \"id\": \"qwen-max\",\n \"label\": \"通义千问 Max(稳定版)\",\n \"tags\": [\"推理\", \"通用\"],\n \"contextWindow\": 128000\n },\n {\n \"id\": \"qwen-plus\",\n \"label\": \"通义千问 Plus\",\n \"tags\": [\"通用\", \"均衡\"],\n \"contextWindow\": 64000\n },\n {\n \"id\": \"qwen-turbo\",\n \"label\": \"通义千问 Turbo\",\n \"tags\": [\"快速\", \"低成本\"],\n \"contextWindow\": 32000\n },\n {\n \"id\": \"qwen-vl-max\",\n \"label\": \"通义千问 VL Max(视觉)\",\n \"tags\": [\"视觉\", \"多模态\", \"推理\"],\n \"contextWindow\": 64000\n }\n ]\n },\n \"zhipu\": {\n \"baseUrl\": \"https://open.bigmodel.cn/api/paas/v4/chat/completions\",\n \"apiKeyEnv\": \"ZHIPU_API_KEY\",\n \"defaultModel\": \"glm-4\",\n \"models\": [\n {\n \"id\": \"glm-4\",\n \"label\": \"GLM-4\",\n \"tags\": [\n \"通用\",\n \"推理\"\n ]\n },\n {\n \"id\": \"glm-4-plus\",\n \"label\": \"GLM-4 Plus\",\n \"tags\": [\n \"强推理\",\n \"复杂\"\n ]\n },\n {\n \"id\": \"glm-4-flash\",\n \"label\": \"GLM-4 Flash\",\n \"tags\": [\n \"快速\",\n \"经济\"\n ]\n }\n ]\n },\n \"minimax\": {\n \"baseUrl\": \"https://api.minimaxi.com/v1/text/chatcompletion\",\n \"apiKeyEnv\": \"MINIMAX_API_KEY\",\n \"defaultModel\": \"minimax-text-01\",\n \"models\": [\n {\n \"id\": \"minimax-text-01\",\n \"label\": \"MiniMax Text 01\",\n \"tags\": [\n \"通用\",\n \"长文本\"\n ]\n },\n {\n \"id\": \"MiniMax-M2.5\",\n \"label\": \"MiniMax M2.5\",\n \"tags\": [\n \"推理\",\n \"代码\"\n ]\n }\n ]\n },\n \"baichuan\": {\n \"baseUrl\": \"https://api.baichuan-ai.com/v1/chat/completions\",\n \"apiKeyEnv\": \"BAICHUAN_API_KEY\",\n \"defaultModel\": \"baichuan-4\",\n \"models\": [\n {\n \"id\": \"baichuan-4\",\n \"label\": \"百川 4\",\n \"tags\": [\n \"通用\",\n \"推理\"\n ]\n }\n ]\n },\n \"doubao\": {\n \"baseUrl\": \"https://ark.cn-beijing.volces.com/api/v3/chat/completions\",\n \"apiKeyEnv\": \"DOUBAO_API_KEY\",\n \"defaultModel\": \"doubao-pro-32k\",\n \"models\": [\n {\n \"id\": \"doubao-pro-32k\",\n \"label\": \"豆包 Pro 32K\",\n \"tags\": [\n \"通用\",\n \"推理\"\n ]\n },\n {\n \"id\": \"doubao-pro-128k\",\n \"label\": \"豆包 Pro 128K\",\n \"tags\": [\n \"长文本\",\n \"通用\"\n ]\n },\n {\n \"id\": \"doubao-lite-32k\",\n \"label\": \"豆包 Lite 32K\",\n \"tags\": [\n \"快速\",\n \"经济\"\n ]\n }\n ]\n },\n \"siliconflow\": {\n \"baseUrl\": \"https://api.siliconflow.cn/v1/chat/completions\",\n \"apiKeyEnv\": \"SILICONFLOW_API_KEY\",\n \"defaultModel\": \"Pro/DeepSeek-V3\",\n \"models\": [\n {\n \"id\": \"Pro/DeepSeek-V3\",\n \"label\": \"DeepSeek V3\",\n \"tags\": [\n \"通用\",\n \"推理\"\n ]\n },\n {\n \"id\": \"Pro/DeepSeek-R1\",\n \"label\": \"DeepSeek R1\",\n \"tags\": [\n \"推理\",\n \"深度思考\"\n ]\n },\n {\n \"id\": \"Qwen/Qwen2.5-72B-Instruct\",\n \"label\": \"Qwen 2.5 72B\",\n \"tags\": [\n \"通用\",\n \"代码\"\n ]\n }\n ]\n },\n \"step\": {\n \"baseUrl\": \"https://api.stepfun.com/v1/chat/completions\",\n \"apiKeyEnv\": \"STEP_API_KEY\",\n \"defaultModel\": \"step-2-16k\",\n \"models\": [\n {\n \"id\": \"step-2-16k\",\n \"label\": \"阶跃星辰 Step-2\",\n \"tags\": [\n \"通用\",\n \"推理\"\n ]\n }\n ]\n },\n \"hunyuan\": {\n \"baseUrl\": \"https://api.hunyuan.cloud.tencent.com/v1/chat/completions\",\n \"apiKeyEnv\": \"HUNYUAN_API_KEY\",\n \"defaultModel\": \"hunyuan-pro\",\n \"visionModel\": \"hunyuan-vision\",\n \"models\": [\n {\n \"id\": \"hunyuan-pro\",\n \"label\": \"混元 Pro\",\n \"tags\": [\"推理\",\"通用\"],\n \"contextWindow\": 256000\n },\n {\n \"id\": \"hunyuan-vision\",\n \"label\": \"混元 Vision\",\n \"tags\": [\"视觉\",\"多模态\"],\n \"contextWindow\": 256000\n },\n {\n \"id\": \"hunyuan-standard\",\n \"label\": \"混元 Standard\",\n \"tags\": [\"快速\",\"经济\"],\n \"contextWindow\": 256000\n },\n {\n \"id\": \"hunyuan-lite\",\n \"label\": \"混元 Lite\",\n \"tags\": [\"快速\",\"低成本\"],\n \"contextWindow\": 256000\n },\n {\n \"id\": \"hunyuan-code\",\n \"label\": \"混元代码\",\n \"tags\": [\"代码\",\"推理\"],\n \"contextWindow\": 256000\n }\n ]\n }\n};;\n\n// ========== 读取 AI 配置(支持备用提供商) ==========\nfunction readAIProviderConfig() {\n const fs = require('fs');\n const path = require('path');\n const BASE = __dirname;\n const cfgPath = path.join(BASE, '..', 'ai-provider.json');\n const config = { provider: 'ollama', fallbackProvider: null, fallbackModel: null };\n try {\n if (fs.existsSync(cfgPath)) {\n const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (raw.provider) config.provider = raw.provider;\n if (raw.fallbackProvider) config.fallbackProvider = raw.fallbackProvider;\n if (raw.fallbackModel) config.fallbackModel = raw.fallbackModel;\n // 仅当环境变量未设置时,才从配置文件读取作为兼容降级\n if (raw.apiKey) {\n const envKey = PROVIDERS[raw.provider]?.apiKeyEnv || 'DEEPSEEK_API_KEY';\n if (!process.env[envKey]) {\n let keyValue = raw.apiKey;\n // 解密 enc: 前缀的密钥\n if (keyValue && keyValue.startsWith('enc:')) {\n try {\n const { decrypt } = require('./key-vault');\n const fs = require('fs');\n const path = require('path');\n const masterKeyPath = path.join(BASE, '..', '.master-key');\n if (fs.existsSync(masterKeyPath)) {\n const masterKey = fs.readFileSync(masterKeyPath, 'utf8').trim();\n keyValue = decrypt(keyValue, masterKey);\n }\n } catch(ex) {\n console.error('[ai-engine] 解密失败:', ex.message);\n keyValue = '';\n }\n }\n if (keyValue) process.env[envKey] = keyValue;\n }\n }\n }\n } catch(e) { /* ignore */ }\n // Check env vars for fallback\n if (process.env.AI_FALLBACK_PROVIDER) config.fallbackProvider = process.env.AI_FALLBACK_PROVIDER;\n return config;\n}\n\n// ========== 执行一次 API 调用 ==========\nasync function callProviderOnce(providerName, messages, options) {\n // 每次调用前重新读取并解密 provider-keys.json\n try {\n const fs = require('fs');\n const path = require('path');\n const crypto = require('crypto');\n const pkPath = path.join(__dirname, '..', 'provider-keys.json');\n if (fs.existsSync(pkPath)) {\n const allKeys = JSON.parse(fs.readFileSync(pkPath, 'utf8'));\n const MASTER_KEY_PATH = path.join(__dirname, '..', '.master-key');\n let masterKey = null;\n if (fs.existsSync(MASTER_KEY_PATH)) masterKey = fs.readFileSync(MASTER_KEY_PATH, 'utf8').trim();\n for (const [provider, encryptedKey] of Object.entries(allKeys)) {\n if (!encryptedKey) continue;\n let decryptedKey = '';\n if (encryptedKey.startsWith('enc:') && masterKey) {\n try {\n const ENCRYPTION_KEY = crypto.createHash('sha256').update(masterKey).digest();\n const parts = encryptedKey.replace('enc:', '').split(':');\n const iv = Buffer.from(parts[0], 'hex');\n const encrypted = Buffer.from(parts[1], 'hex');\n const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv, { authTagLength: 16 });\n const authTag = encrypted.slice(-16);\n decipher.setAuthTag(authTag);\n const ciphertext = encrypted.slice(0, -16);\n decryptedKey = decipher.update(ciphertext, null, 'utf8') + decipher.final('utf8');\n } catch(ex) { continue; }\n } else if (!encryptedKey.startsWith('enc:')) {\n decryptedKey = encryptedKey;\n }\n if (decryptedKey) {\n const envKey = PROVIDERS[provider]?.apiKeyEnv || (provider.toUpperCase() + '_API_KEY');\n process.env[envKey] = decryptedKey;\n }\n }\n }\n } catch(e) { /* ignore */ }\n \n const { toolDefs, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n // 调试日志\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] providerName=' + providerName + '\\\\n'); } catch(ex) {}\n \n const providerCfg = PROVIDERS[providerName.toLowerCase()];\n if (!providerCfg) throw new Error(`不支持的 AI 提供商: ${providerName}`);\n \n // 调试日志:显示环境变量名和值(前10字符)\n try { fs.appendFileSync('F:\\\\eCompany-Dev\\\\backend\\\\logs\\\\ai-engine-debug.log', new Date().toISOString() + ' [callProviderOnce] apiKeyEnv=' + providerCfg.apiKeyEnv + ' envValue=' + (process.env[providerCfg.apiKeyEnv] ? process.env[providerCfg.apiKeyEnv].substring(0,10) + '...' : '(empty)') + '\\\\n'); } catch(ex) {}\n \n const apiKey = process.env[providerCfg.apiKeyEnv];\n // 本地模型(如 Ollama)不需要 API Key\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv} 环境变量`);\n\n const body = {\n model: model || providerCfg.defaultModel,\n messages,\n temperature,\n max_tokens: maxTokens\n };\n if (toolDefs && toolDefs.length) {\n body.tools = toolDefs;\n body.tool_choice = 'auto';\n }\n\n var _retries = 0;\n var _lastErr = null;\n while (_retries < 3) {\n _retries++;\n if (_retries > 1) await new Promise(function(r) { setTimeout(r, (_retries - 1) * 1500); });\n try {\n const controller = new AbortController();\n var rt = setTimeout(function() { controller.abort(); }, timeout);\n // Provider-specific request adaptation\n var _url = providerCfg.baseUrl;\n var _headers = { 'Content-Type': 'application/json' };\n var _body = JSON.stringify(body);\n var pn = providerName.toLowerCase();\n // 本地模型(Ollama)不需要 Authorization 头\n if (!providerCfg.noApiKey) {\n if (pn === 'anthropic') {\n _headers['x-api-key'] = apiKey;\n _headers['anthropic-version'] = '2023-06-01';\n var sysMsg = '', anthMsgs = [];\n for (var _m of messages) {\n if (_m.role === 'system') { sysMsg = _m.content; continue; }\n if (_m.role === 'tool') continue;\n anthMsgs.push({ role: _m.role, content: _m.content });\n }\n var anthBody = { model: body.model, max_tokens: body.max_tokens, messages: anthMsgs };\n if (body.temperature !== undefined) anthBody.temperature = body.temperature;\n if (sysMsg) anthBody.system = sysMsg;\n _body = JSON.stringify(anthBody);\n } else if (pn === 'ernie') {\n var keyParts = apiKey.split('|');\n var cid = keyParts[0], csec = keyParts[1] || keyParts[0];\n var tokenRes = await fetch('https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + encodeURIComponent(cid) + '&client_secret=' + encodeURIComponent(csec), { signal: controller.signal });\n var tokenData = await tokenRes.json();\n if (tokenData.access_token) _url = providerCfg.baseUrl + '?access_token=' + tokenData.access_token;\n } else if (pn === 'zhipu' || pn === 'minimax' || pn === 'hunyuan') {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n } else {\n _headers['Authorization'] = 'Bearer ' + apiKey;\n }\n }\n const res = await fetch(_url, {\n method: 'POST',\n headers: _headers,\n body: _body,\n signal: controller.signal\n });\n clearTimeout(rt);\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`${providerName} API ${res.status}: ${errText.substring(0, 200)}`);\n }\n return await res.json();\n } catch (err) {\n clearTimeout(timer);\n _lastErr = err;\n clearTimeout(timer);\n continue;\n }\n }\n throw _lastErr || new Error(providerName + ' 调用失败(已重试 3 次)');\n}\n\n// ========== AI 调用(带多提供商遍历降级) ==========\nasync function aiChat(messages, options = {}) {\n const { toolDefs, provider, model, temperature = 0.7, maxTokens = 4096, timeout = 30000 } = options;\n\n const config = readAIProviderConfig();\n const activeProvider = provider || config.provider || process.env.AI_PROVIDER || 'ollama';\n\n // 第一优先级:显式指定的 provider\n if (provider) {\n return await callProviderOnce(provider, messages, options);\n }\n\n // 第二优先级:尝试主提供商\n try {\n const result = await callProviderOnce(activeProvider, messages, options);\n return result;\n } catch (primaryErr) {\n // 主提供商失败,尝试备用提供商\n const fallbackProvider = config.fallbackProvider || process.env.AI_FALLBACK_PROVIDER;\n if (fallbackProvider && fallbackProvider !== activeProvider) {\n try {\n const fallbackOptions = { ...options };\n if (config.fallbackModel) fallbackOptions.model = config.fallbackModel;\n const result = await callProviderOnce(fallbackProvider, messages, fallbackOptions);\n return result;\n } catch (fallbackErr) {\n throw new Error(`主提供商(${activeProvider})和备用提供商(${fallbackProvider})均失败: ${fallbackErr.message}`);\n }\n }\n // 主提供商失败,遍历所有其它已配置的提供商\n for (var _p of Object.keys(PROVIDERS)) {\n if (_p === activeProvider) continue;\n if (_p === fallbackProvider) continue;\n try {\n var _envKey = PROVIDERS[_p].apiKeyEnv;\n if (process.env[_envKey] || PROVIDERS[_p].noApiKey) {\n return await callProviderOnce(_p, messages, options);\n }\n } catch(_e) { /* try next */ }\n }\n throw primaryErr;\n }\n}\n\n// ========== 多轮对话(含工具调用) ==========\nasync function aiChatWithTools(messages, tools, options = {}) {\n // 第一轮:调用模型(可能返回工具调用)\n const response = await aiChat(messages, { ...options, toolDefs: tools });\n if (!response.choices?.length) throw new Error('API 返回为空');\n\n const choice = response.choices[0];\n\n // 如果没有工具调用,直接返回\n if (choice.finish_reason !== 'tool_calls' || !choice.message?.tool_calls) {\n return { reply: choice.message?.content || '', toolCalls: [] };\n }\n\n // 执行工具调用\n const secondMessages = [...messages, choice.message];\n const toolResults = [];\n\n for (const tc of choice.message.tool_calls) {\n if (tc.type === 'function') {\n const funcName = tc.function.name;\n let funcArgs = {};\n try { funcArgs = JSON.parse(tc.function.arguments); } catch (e) {}\n\n toolResults.push({ name: funcName, args: funcArgs });\n\n secondMessages.push({\n role: 'tool',\n tool_call_id: tc.id,\n content: JSON.stringify({ result: `工具 ${funcName} 已执行`, args: funcArgs })\n });\n }\n }\n\n // 第二轮:获取最终回复\n const secondResponse = await aiChat(secondMessages, { ...options, toolDefs: tools });\n const finalReply = secondResponse.choices?.[0]?.message?.content || '';\n\n return { reply: finalReply, toolCalls: toolResults };\n}\n\n// ========== 流式输出 ==========\nasync function* aiChatStream(messages, options = {}) {\n const { provider, model } = options;\n const activeProvider = provider || process.env.AI_PROVIDER || 'ollama';\n const providerCfg = PROVIDERS[activeProvider];\n if (!providerCfg) throw new Error(`不支持的提供商: ${activeProvider}`);\n\n const apiKey = process.env[providerCfg.apiKeyEnv];\n if (!apiKey && !providerCfg.noApiKey) throw new Error(`未配置 ${providerCfg.apiKeyEnv}`);\n\n const headers = { 'Content-Type': 'application/json' };\n if (!providerCfg.noApiKey) headers['Authorization'] = `Bearer ${apiKey}`;\n\n const res = await fetch(providerCfg.baseUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n model: model || providerCfg.defaultModel,\n messages,\n stream: true,\n temperature: options.temperature || 0.7,\n max_tokens: options.maxTokens || 4096\n })\n });\n\n if (!res.ok) throw new Error(`API ${res.status}`);\n\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n const data = line.slice(6).trim();\n if (data === '[DONE]') return;\n try {\n const parsed = JSON.parse(data);\n const content = parsed.choices?.[0]?.delta?.content || '';\n if (content) yield content;\n } catch (e) { /* 跳过解析错误 */ }\n }\n }\n }\n}\n\nmodule.exports = {\n PROVIDERS,\n aiChat,\n aiChatWithTools,\n aiChatStream\n};\n"}}, "backend/modules/model-router.js": {"a0e92cca8533660972d0a16d2874b571292e53fa": {"t": 1785746692.0841644, "n": 29521, "h": "774a67eeef3c8f91", "c": "/**\n * eCompany Multi-Model Router v1.0\n * 多模型智能路由 — 根据任务类型自动选择最优模型\n * \n * 路由策略:\n * - 主模型: Ollama 本地 (Qwen 3.5 9B / DeepSeek Coder V2)\n * - 备用: DeepSeek V4 Flash 云端\n * - 兜底: Qwen 免费云端\n */\nconst fs = require('fs');\nconst path = require('path');\nconst BASE = path.join(__dirname, '..');\n\n// ========== 默认模型配置 ==========\nconst DEFAULT_PROVIDERS = {\n ollama: {\n name: 'Ollama (本地)',\n baseUrl: process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434',\n apiFormat: '/v1/chat/completions',\n noApiKey: true,\n models: {\n 'qwen3.5:9b': {\n name: 'Qwen 3.5 9B',\n capabilities: ['simple', 'search', 'translate', 'summarize', 'analysis', 'code', 'chat'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-coder-v2:16b': {\n name: 'DeepSeek Coder V2 16B',\n capabilities: ['code', 'debug', 'analysis', 'reasoning'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'normal'\n }\n }\n },\n deepseek: {\n name: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com',\n apiFormat: '/v1/chat/completions',\n models: {\n 'deepseek-v4-flash': {\n name: 'DeepSeek V4 Flash',\n capabilities: ['simple', 'search', 'translate', 'summarize', 'chat', 'creative'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-v4-pro': {\n name: 'DeepSeek V4 Pro',\n capabilities: ['analysis', 'code', 'reasoning', 'complex', 'math'],\n cost: { input: 1.74, output: 3.48 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'normal'\n },\n 'deepseek-chat': {\n name: 'DeepSeek Chat (V3, 最新默认对话模型)',\n capabilities: ['simple', 'chat', 'translate', 'analysis', 'code', 'reasoning', 'search', 'creative', 'complex'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: true,\n speed: 'fast',\n legacy: false\n },\n 'deepseek-reasoner': {\n name: 'DeepSeek Reasoner (R1, 深度推理模型)',\n capabilities: ['reasoning', 'analysis', 'math', 'complex', 'code', 'debug'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 262144,\n maxTokens: 8192,\n reasoning: true,\n speed: 'normal',\n legacy: false\n }\n }\n },\n qwen: {\n name: 'Qwen (阿里云)',\n baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n apiFormat: '',\n models: {\n 'qwen3.7-max': {\n name: 'Qwen 3.7 Max(最新旗舰)',\n capabilities: ['complex', 'analysis', 'reasoning', 'code', 'creative'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: true,\n speed: 'fast'\n },\n 'qwen3.7-plus': {\n name: 'Qwen 3.7 Plus(均衡型)',\n capabilities: ['simple', 'chat', 'translate', 'analysis', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: false,\n speed: 'fast'\n },\n 'qwen3.6-flash': {\n name: 'Qwen 3.6 Flash(快速型)',\n capabilities: ['simple', 'chat', 'translate', 'summarize'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: false,\n speed: 'fast'\n },\n 'qwen3-max-2026-01-23': {\n name: 'Qwen3 Max(稳定版)',\n capabilities: ['complex', 'analysis', 'reasoning', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 262144,\n maxTokens: 65536,\n reasoning: false,\n speed: 'normal'\n }\n }\n }\n};\n\n// ========== 任务类型识别 ==========\n\nconst TASK_PATTERNS = {\n simple: [\n /^(你好|hi|hello|在吗|早安|晚安|你是谁|你叫什么)/i,\n /^(天气|搜索|翻译)/i,\n /^(谢谢|好的|明白|知道了|ok|yes|no|是|否)/i,\n ],\n search: [\n /^(?!.*(趋势|报表|数据|统计|排行|系统|分析)).*(搜索|查找|找一下|搜一下|查一下|search)/i,\n ],\n code: [\n /写一个|写一段|代码|编程|实现|函数|class|function|bug|debug|修复.*问题/,\n /api|接口|api|API|路由|端点|endpoint/,\n /docker|dockerfile|deploy|部署|ci|cd/,\n /登录|注册|认证|auth|token|jwt/i,\n ],\n analysis: [\n /分析|统计|对比|比较|评估|review|总结|归纳|汇报|报告|报表/,\n /为什么|如何|原理|机制|架构|设计方案/,\n /数据|图表|趋势|维度|dashboard/,\n /^查(一下|一?下)?(系统|趋势|数据|报表|统计|排行)/i,\n ],\n creative: [\n /写(一|篇|首|段)|创作|创意|文案|策划|脚本|方案/,\n /故事|文章|文章|内容|描述|形容/,\n ],\n complex: [\n /系统设计|架构设计|技术方案|整体方案/,\n /多轮|复杂|大型|综合/,\n /计划|规划|路线图|roadmap/,\n ]\n};\n\nfunction classifyTask(text) {\n if (!text) return 'simple';\n for (const [type, patterns] of Object.entries(TASK_PATTERNS)) {\n for (const p of patterns) {\n if (p.test(text)) return type;\n }\n }\n if (text.length > 500) return 'analysis';\n if (text.length > 200) return 'complex';\n return 'simple';\n}\n\n// ========== 路由表 (DeepSeek 云端为主, Ollama 本地为备用) ==========\n// 策略说明:云端模型有更好的质量/速度比,本地模型仅在离线/无 Key 时使用\n\nconst ROUTING_TABLE = {\n simple: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速响应' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n search: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端搜索问答' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n code: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端专业代码' },\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速备用' },\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地代码备用' },\n ],\n analysis: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度分析' },\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n creative: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端创意写作' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n complex: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度处理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端快速备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地兜底' },\n ],\n reasoning: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度推理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地深度推理备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地通用兜底' },\n ]\n};\n\n// ========== 配置管理 ==========\n\nlet providerConfigs = {};\nlet usageStats = { calls: 0, byProvider: {}, totalCost: 0, todayCost: 0 };\nlet strategyConfig = { mode: 'speed-first', backupModels: [] };\nlet roundRobinIndex = {};\n\n\n// === Strategy config ===\nfunction loadStrategy() {\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n if (fs.existsSync(cfgPath)) {\n var cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n strategyConfig.mode = cfg.strategy || 'speed-first';\n strategyConfig.backupModels = cfg.backupModels || [];\n }\n } catch(e) {\n strategyConfig = { mode: 'speed-first', backupModels: [] };\n }\n}\n\nfunction setStrategy(mode, backupModels) {\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n var cfg = {};\n try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); } catch(e) {}\n cfg.strategy = mode || cfg.strategy || 'speed-first';\n if (backupModels) cfg.backupModels = backupModels;\n fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));\n // Update in-memory state\n strategyConfig.mode = cfg.strategy;\n strategyConfig.backupModels = cfg.backupModels || [];\n return true;\n } catch(e) { return false; }\n}\nfunction loadConfig() {\n loadStrategy();\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n if (fs.existsSync(cfgPath)) {\n const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n providerConfigs = cfg.providers || {};\n // Merge apiKeys from provider-keys.json into providerConfigs\n try {\n var pkPath = path.join(BASE, 'provider-keys.json');\n if (fs.existsSync(pkPath)) {\n var pk = JSON.parse(fs.readFileSync(pkPath, 'utf-8'));\n Object.keys(pk).forEach(function(k) {\n if (providerConfigs[k] && pk[k]) {\n providerConfigs[k].apiKey = pk[k];\n } else if (pk[k] && !providerConfigs[k]) {\n if (['deepseek','openai','claude','gemini','tongyi','zhipu','siliconflow','ernie','yi','moonshot','doubao','hunyuan','step','minimax','baichuan','openrouter'].indexOf(k) >= 0) {\n providerConfigs[k] = { enabled: true, apiKey: pk[k], models: {} };\n }\n }\n });\n }\n } catch(pkE) {}\n // credential-store 覆盖(最高优先级)\n try {\n var credStore = require('./credential-store');\n Object.keys(providerConfigs).forEach(function(k) {\n var activeKey = credStore.getApiKey(k);\n if (activeKey) {\n providerConfigs[k].apiKey = activeKey;\n }\n });\n } catch(credE) {\n console.error('[model-router] credential-store 读取失败:', credE.message);\n }\n if (cfg.routingOverride) {\n Object.assign(ROUTING_TABLE, cfg.routingOverride);\n }\n if (cfg.defaults) {\n Object.keys(providerConfigs).forEach(p => {\n const def = cfg.defaults;\n if (!providerConfigs[p].models) providerConfigs[p].models = {};\n Object.keys(DEFAULT_PROVIDERS[p]?.models || {}).forEach(m => {\n if (!providerConfigs[p].models[m]) {\n providerConfigs[p].models[m] = { enabled: true };\n }\n });\n });\n }\n } else {\n const defaultCfg = generateDefaultConfig();\n fs.writeFileSync(cfgPath, JSON.stringify(defaultCfg, null, 2), 'utf-8');\n providerConfigs = defaultCfg.providers;\n }\n } catch(e) {\n console.error('[ModelRouter] Config load error:', e.message);\n providerConfigs = {};\n }\n}\n\nfunction generateDefaultConfig() {\n return {\n enabled: true,\n strategy: 'speed-first',\n providers: {\n ollama: {\n enabled: true,\n noApiKey: true,\n apiKey: '',\n models: {\n 'qwen3.5:9b': { enabled: true, priority: 1 },\n 'deepseek-coder-v2:16b': { enabled: true, priority: 2 }\n }\n },\n deepseek: {\n enabled: true,\n apiKey: process.env.DEEPSEEK_API_KEY || '',\n models: {\n 'deepseek-v4-flash': { enabled: true, priority: 1 },\n 'deepseek-v4-pro': { enabled: true, priority: 2 },\n 'deepseek-chat': { enabled: true, priority: 3 },\n 'deepseek-reasoner': { enabled: true, priority: 4 }\n }\n },\n qwen: {\n enabled: true,\n apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '',\n models: {\n 'qwen3.7-max': { enabled: true, priority: 1 },\n 'qwen3.5-plus': { enabled: true, priority: 2 },\n 'qwen3-max-2026-01-23': { enabled: false, priority: 3 },\n 'qwen3-coder-plus': { enabled: false, priority: 4 }\n }\n }\n },\n routingOverride: {},\n createdAt: new Date().toISOString()\n };\n}\n\n// ========== 核心路由函数 ==========\n\nfunction selectModel(taskText, options = {}) {\n // 如果用户明确指定了模型名(如 deepseek-reasoner / deepseek-chat),直接返回精确路由\n if (options.preferredModel) {\n var prefModel = options.preferredModel;\n // 匹配 deepseek 提供者下的所有模型\n var provMap = { 'deepseek': 'deepseek', 'ds': 'deepseek' };\n var provider = 'deepseek';\n var rawModel = prefModel;\n // 支持 'deepseek/deepseek-reasoner' 或 'ds/deepseek-chat' 格式\n if (prefModel.indexOf('/') >= 0) {\n var parts = prefModel.split('/');\n provider = provMap[parts[0]] || parts[0];\n rawModel = parts[1];\n }\n loadConfigIfNeeded();\n var provCfg = providerConfigs[provider];\n var apiKey = (provCfg && provCfg.apiKey) || '';\n // 优先从 provider-keys.json 读取最信配置,再 fallback 到 env\n if (!apiKey) {\n try { var pkf = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, '..', 'provider-keys.json'), 'utf-8')); if (pkf[provider]) apiKey = pkf[provider]; } catch(e) {}\n }\n if (!apiKey) {\n apiKey = process.env[provider.toUpperCase() + '_API_KEY'] || '';\n }\n if (!apiKey && provider !== 'ollama') {\n // 没 API Key 时仍然返回配置但标记空 key,让调用方处理\n }\n return {\n provider: provider,\n model: rawModel,\n apiKey: apiKey,\n apiBase: provCfg ? getApiBase(provider, rawModel, provCfg) : 'https://api.deepseek.com/v1/chat/completions',\n taskType: 'preferred',\n reason: '用户显式选择模型: ' + rawModel,\n cost: DEFAULT_PROVIDERS[provider]?.models[rawModel]?.cost || { input: 0.14, output: 0.28 },\n contextWindow: DEFAULT_PROVIDERS[provider]?.models[rawModel]?.contextWindow || 131072\n };\n }\n\n const taskType = options.taskType || classifyTask(taskText);\n const routes = ROUTING_TABLE[taskType] || ROUTING_TABLE.simple;\n\n loadConfigIfNeeded();\n\n \n // === Strategy-aware routing ===\n var strategy = options.strategy || strategyConfig.mode || 'speed-first';\n var backupModels = options.backupModels || strategyConfig.backupModels || [];\n\n // Get all enabled providers for current routing\n function getEnabledRoutes(specificType) {\n var type = specificType || taskType;\n var rts = ROUTING_TABLE[type] || ROUTING_TABLE.simple;\n return rts.filter(function(rt) {\n var provCfg = providerConfigs[rt.provider];\n if (!provCfg || !provCfg.enabled) return false;\n var modelCfg = provCfg.models && provCfg.models[rt.model];\n if (modelCfg && modelCfg.enabled === false) return false;\n if (!provCfg.noApiKey) {\n var ak = provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '';\n if (!ak) return false;\n }\n return true;\n });\n }\n\n // === Fixed mode: always use first enabled model ===\n if (strategy === 'fixed') {\n var enabledRoutes = getEnabledRoutes();\n if (enabledRoutes.length) {\n var rt = enabledRoutes[0];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: '固定模型: ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n return getLegacyFallback();\n }\n\n // === Fallback mode: try primary, then backup models ===\n if (strategy === 'fallback') {\n var enabledRoutes = getEnabledRoutes();\n for (var i = 0; i < enabledRoutes.length; i++) {\n var rt = enabledRoutes[i];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: 'fallback 主模型: ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n // Try backup models if any\n if (backupModels && backupModels.length) {\n for (var bi = 0; bi < backupModels.length; bi++) {\n var bm = backupModels[bi];\n for (var pi in providerConfigs) {\n var provCfg = providerConfigs[pi];\n if (!provCfg || !provCfg.enabled) continue;\n var mCfg = provCfg.models && provCfg.models[bm];\n if (mCfg && mCfg.enabled !== false) {\n var ak = provCfg.apiKey || process.env[pi.toUpperCase() + '_API_KEY'] || '';\n if (provCfg.noApiKey || ak) {\n return {\n provider: pi,\n model: bm,\n apiKey: ak,\n apiBase: getApiBase(pi, bm, provCfg),\n taskType: taskType,\n reason: 'fallback 备用: ' + bm,\n cost: DEFAULT_PROVIDERS[pi]?.models[bm]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[pi]?.models[bm]?.contextWindow || 131072\n };\n }\n }\n }\n }\n }\n return getLegacyFallback();\n }\n\n // === RoundRobin mode: cycle through enabled models ===\n if (strategy === 'roundrobin') {\n var enabledRoutes = getEnabledRoutes();\n if (!roundRobinIndex[taskType]) roundRobinIndex[taskType] = 0;\n if (enabledRoutes.length) {\n var idx = roundRobinIndex[taskType] % enabledRoutes.length;\n roundRobinIndex[taskType] = (idx + 1) % Math.max(1, enabledRoutes.length);\n var rt = enabledRoutes[idx];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: '轮询 (' + (idx+1) + '/' + enabledRoutes.length + '): ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n return getLegacyFallback();\n }\n\n // === Smart mode (default): task-type aware + cost-aware ===\nfor (const route of routes) {\n const provCfg = providerConfigs[route.provider];\n if (!provCfg || !provCfg.enabled) continue;\n\n const modelCfg = provCfg.models && provCfg.models[route.model];\n if (modelCfg && modelCfg.enabled === false) continue;\n\n // 本地模型(Ollama)不需要 API Key\n if (!provCfg.noApiKey) {\n const apiKey = provCfg.apiKey || process.env[route.provider.toUpperCase() + '_API_KEY'] || '';\n if (!apiKey) continue;\n }\n\n return {\n provider: route.provider,\n model: route.model,\n apiKey: provCfg.apiKey || process.env[route.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(route.provider, route.model, provCfg),\n taskType: taskType,\n reason: route.reason,\n cost: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.contextWindow || 131072\n };\n }\n\n return getLegacyFallback();\n}\n\nfunction getApiBase(provider, model, provCfg) {\n const defaults = DEFAULT_PROVIDERS[provider];\n if (provCfg && provCfg.baseUrl) {\n let base = provCfg.baseUrl.replace(/\\/+$/, '');\n if (provCfg.apiFormat) base += provCfg.apiFormat;\n else base += defaults?.apiFormat || '/v1/chat/completions';\n return base;\n }\n var envUrl = process.env.OLLAMA_BASE_URL;\n if (!envUrl && defaults) return (defaults.baseUrl + (defaults.apiFormat || '/v1/chat/completions'));\n return envUrl || 'http://127.0.0.1:11434/v1/chat/completions';\n}\n\nlet configLoaded = false;\nfunction loadConfigIfNeeded() {\n if (!configLoaded) { loadConfig(); configLoaded = true; }\n}\n\nfunction getLegacyFallback() {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'ai-provider.json'), 'utf-8'));\n const p = cfg.provider || 'ollama';\n return {\n provider: p,\n model: cfg.model || 'qwen3.5:9b',\n apiKey: cfg.apiKey || '',\n apiBase: (function(){ if(cfg.apiBase) return cfg.apiBase; var d=DEFAULT_PROVIDERS[p]; return d ? d.baseUrl+(d.apiFormat||'/v1/chat/completions') : 'http://127.0.0.1:11434/v1/chat/completions'; })(),\n taskType: 'fallback',\n reason: 'Legacy config fallback',\n cost: { input: 0, output: 0 }\n };\n } catch(e) {\n return {\n provider: 'ollama',\n model: 'qwen3.5:9b',\n apiKey: '',\n apiBase: process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434/v1/chat/completions',\n taskType: 'fallback',\n reason: 'Ultimate fallback',\n cost: { input: 0, output: 0 }\n };\n }\n}\n\n// ========== 用量统计 ==========\n\nfunction recordUsage(route, inputTokens, outputTokens) {\n usageStats.calls++;\n const key = route.provider + '/' + route.model;\n if (!usageStats.byProvider[key]) usageStats.byProvider[key] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };\n usageStats.byProvider[key].calls++;\n usageStats.byProvider[key].inputTokens += inputTokens || 0;\n usageStats.byProvider[key].outputTokens += outputTokens || 0;\n const cost = (route.cost?.input || 0) * (inputTokens || 0) / 1000000 + (route.cost?.output || 0) * (outputTokens || 0) / 1000000;\n usageStats.byProvider[key].cost += cost;\n usageStats.totalCost += cost;\n usageStats.todayCost += cost;\n}\n\nfunction getUsageStats() {\n return usageStats;\n}\n\nfunction resetDailyStats() {\n usageStats.todayCost = 0;\n}\n\n// ========== 路由测试 ==========\n\nfunction testRoute(text) {\n const taskType = classifyTask(text);\n const route = selectModel(text);\n return {\n input: text.substring(0, 100),\n classified: taskType,\n selected: route\n };\n}\n\n// ========== 注册HTTP路由 ==========\n\nfunction registerRouterRoutes(registerRoute, parseBody, json) {\n registerRoute(['POST'], /^\\/api\\/router\\/select$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = body.taskType || classifyTask(text);\n const route = selectModel(text, { taskType, strategy: body.strategy });\n json(res, { ok: true, taskType, route });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/classify$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = classifyTask(text);\n json(res, { ok: true, taskType, input: text.substring(0, 200) });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/stats$/, (req, res) => {\n json(res, { ok: true, ...usageStats });\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/config$/, (req, res) => {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'model-router.json'), 'utf-8'));\n Object.keys(cfg.providers || {}).forEach(p => {\n if (cfg.providers[p].apiKey) cfg.providers[p].apiKey = cfg.providers[p].apiKey.slice(0,8) + '...';\n });\n json(res, { ok: true, config: cfg, routingTable: ROUTING_TABLE });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/config$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const cfgPath = path.join(BASE, 'model-router.json');\n const current = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (body.providers) {\n Object.keys(body.providers).forEach(p => {\n if (!current.providers[p]) current.providers[p] = {};\n Object.assign(current.providers[p], body.providers[p]);\n });\n }\n if (body.strategy) current.strategy = body.strategy;\n fs.writeFileSync(cfgPath, JSON.stringify(current, null, 2), 'utf-8');\n configLoaded = false;\n loadConfig();\n json(res, { ok: true, message: '配置已更新' });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n}\n\nloadConfig();\n\n\n// === Save enabled models from frontend settings ===\n\n// === Get config summary for frontend ===\nfunction getConfigSummary() {\n loadConfigIfNeeded();\n return {\n providers: providerConfigs\n };\n}\n\nfunction getModelList() {\n loadConfigIfNeeded();\n var models = [];\n Object.keys(providerConfigs).forEach(function(p) {\n var pc = providerConfigs[p];\n if (pc.enabled === false || !pc.models) return;\n Object.keys(pc.models).forEach(function(m) {\n var mc = pc.models[m];\n if (mc.enabled === false) return;\n models.push({\n provider: p,\n model: m,\n name: mc.name || (p + ' · ' + m)\n });\n });\n });\n return { models: models };\n}\n\nfunction setEnabledModels(provider, enabledMap) {\n loadConfigIfNeeded();\n if (!providerConfigs[provider]) return false;\n var provCfg = providerConfigs[provider];\n if (!provCfg.models) provCfg.models = {};\n for (var modelId in enabledMap) {\n if (provCfg.models[modelId]) {\n provCfg.models[modelId].enabled = enabledMap[modelId] !== false;\n } else {\n provCfg.models[modelId] = { enabled: enabledMap[modelId] !== false, priority: 99 };\n }\n }\n // Persist to file (merge with default)\n var cfgPath = path.join(BASE, 'model-router.json');\n try {\n var config = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (!config.providers) config.providers = {};\n if (!config.providers[provider]) config.providers[provider] = {};\n if (!config.providers[provider].models) config.providers[provider].models = {};\n for (var modelId in enabledMap) {\n if (!config.providers[provider].models[modelId]) {\n config.providers[provider].models[modelId] = {};\n }\n config.providers[provider].models[modelId].enabled = enabledMap[modelId] !== false;\n }\n fs.writeFileSync(cfgPath, JSON.stringify(config, null, 2));\n return true;\n } catch(e) {\n console.error('[ModelRouter] Failed to save config:', e.message);\n return false;\n }\n}\n\n\n// ========== 模型自动切换(带健康检查 + fallback链) ==========\n\n/**\n * 返回当前可用模型的排序列表(按健康度),供调用者做自动 Fallback。\n * 返回格式:[{ provider, model, apiKey, apiBase, reason }, ...]\n * 会按配置的优先级排序,并跳过无 API Key 的供应商。\n */\nfunction selectModelWithFallback(taskText, options = {}) {\n loadConfigIfNeeded();\n\n const taskType = options.taskType || classifyTask(taskText);\n const routes = ROUTING_TABLE[taskType] || ROUTING_TABLE.simple;\n\n // 收集所有可用路由 + 备用模型\n var candidates = [];\n var seen = {};\n\n // 1. 主路由表的路由\n function addCandidate(rt) {\n var provCfg = providerConfigs[rt.provider];\n if (!provCfg || provCfg.enabled === false) return;\n var modelCfg = provCfg.models && provCfg.models[rt.model];\n if (modelCfg && modelCfg.enabled === false) return;\n var ak = provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '';\n if (!ak && !provCfg.noApiKey) return;\n var key = rt.provider + '/' + rt.model;\n if (seen[key]) return;\n seen[key] = true;\n candidates.push({\n provider: rt.provider,\n model: rt.model,\n apiKey: ak,\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: rt.reason || ('候选模型: ' + rt.model),\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n });\n }\n\n routes.forEach(addCandidate);\n\n // 2. 备用模型(来自配置文件的 backupModels)\n if (options.backupModels) {\n options.backupModels.forEach(function(bm) {\n // 按 provider 匹配\n if (bm.indexOf('/') > 0) {\n var parts = bm.split('/');\n addCandidate({ provider: parts[0], model: parts[1], reason: '备用: ' + parts[1] });\n } else {\n // 无 provider 前缀,尝试所有有 API Key 的 provider\n Object.keys(providerConfigs).forEach(function(p) {\n var pc = providerConfigs[p];\n var ak = pc.apiKey || process.env[p.toUpperCase() + '_API_KEY'] || '';\n if (!ak && !pc.noApiKey) return;\n var mc = pc.models && pc.models[bm];\n if (mc && mc.enabled !== false) {\n addCandidate({ provider: p, model: bm, reason: '备用: ' + p + '/' + bm });\n }\n });\n }\n });\n }\n\n // 3. 策略排序\n var strategy = options.strategy || 'speed-first';\n if (strategy === 'speed-first') {\n // 速度优先:同一 provider 的先排\n candidates.sort(function(a, b) { return (a.cost?.output || 99) - (b.cost?.output || 99); });\n } else if (strategy === 'cost-first') {\n candidates.sort(function(a, b) { return (a.cost?.output || 99) - (b.cost?.output || 99); });\n } else if (strategy === 'quality-first') {\n candidates.sort(function(a, b) { return ((b.cost?.output || 0) - (a.cost?.output || 0)); });\n }\n\n return candidates;\n}\n\n// 用于 P95 等无感知降级场景\nfunction createFallbackChain(taskText, options = {}) {\n var list = selectModelWithFallback(taskText, options);\n var idx = 0;\n\n /**\n * 自动遍历候选链:当前一个失败,自动尝试下一个。\n * @param {Function} caller - async (model) => reply,模型参数就是候选对象\n * @returns {Object} { reply, model, fallbacks: number }\n */\n async function execute(caller) {\n var lastErr = null;\n for (; idx < list.length; idx++) {\n var m = list[idx];\n try {\n var reply = await caller(m);\n if (reply) {\n return { reply: reply, model: m.provider + '/' + m.model, fallbacks: idx };\n }\n } catch (e) {\n lastErr = e;\n console.log('[Fallback] ' + m.provider + '/' + m.model + ' 失败: ' + e.message + ',尝试下一个... (' + (list.length - idx - 1) + ' 个剩余)');\n }\n }\n throw lastErr || new Error('所有模型都失败');\n }\n\n return { execute: execute, list: list };\n}\n\n// === Strategy API ===\nmodule.exports = {\n classifyTask,\n selectModel,\n recordUsage,\n getUsageStats,\n resetDailyStats,\n testRoute,\n registerRouterRoutes,\n getConfigSummary,\n getModelList,\n setEnabledModels,\n loadStrategy,\n setStrategy,\n selectModelWithFallback,\n createFallbackChain\n};\n"}, "b039c2bbf630c1d25b2d82a47e3f33c95f9e9fe5": {"t": 1785766819.1337442, "n": 29521, "h": "774a67eeef3c8f91", "c": "/**\n * eCompany Multi-Model Router v1.0\n * 多模型智能路由 — 根据任务类型自动选择最优模型\n * \n * 路由策略:\n * - 主模型: Ollama 本地 (Qwen 3.5 9B / DeepSeek Coder V2)\n * - 备用: DeepSeek V4 Flash 云端\n * - 兜底: Qwen 免费云端\n */\nconst fs = require('fs');\nconst path = require('path');\nconst BASE = path.join(__dirname, '..');\n\n// ========== 默认模型配置 ==========\nconst DEFAULT_PROVIDERS = {\n ollama: {\n name: 'Ollama (本地)',\n baseUrl: process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434',\n apiFormat: '/v1/chat/completions',\n noApiKey: true,\n models: {\n 'qwen3.5:9b': {\n name: 'Qwen 3.5 9B',\n capabilities: ['simple', 'search', 'translate', 'summarize', 'analysis', 'code', 'chat'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-coder-v2:16b': {\n name: 'DeepSeek Coder V2 16B',\n capabilities: ['code', 'debug', 'analysis', 'reasoning'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'normal'\n }\n }\n },\n deepseek: {\n name: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com',\n apiFormat: '/v1/chat/completions',\n models: {\n 'deepseek-v4-flash': {\n name: 'DeepSeek V4 Flash',\n capabilities: ['simple', 'search', 'translate', 'summarize', 'chat', 'creative'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-v4-pro': {\n name: 'DeepSeek V4 Pro',\n capabilities: ['analysis', 'code', 'reasoning', 'complex', 'math'],\n cost: { input: 1.74, output: 3.48 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'normal'\n },\n 'deepseek-chat': {\n name: 'DeepSeek Chat (V3, 最新默认对话模型)',\n capabilities: ['simple', 'chat', 'translate', 'analysis', 'code', 'reasoning', 'search', 'creative', 'complex'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: true,\n speed: 'fast',\n legacy: false\n },\n 'deepseek-reasoner': {\n name: 'DeepSeek Reasoner (R1, 深度推理模型)',\n capabilities: ['reasoning', 'analysis', 'math', 'complex', 'code', 'debug'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 262144,\n maxTokens: 8192,\n reasoning: true,\n speed: 'normal',\n legacy: false\n }\n }\n },\n qwen: {\n name: 'Qwen (阿里云)',\n baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n apiFormat: '',\n models: {\n 'qwen3.7-max': {\n name: 'Qwen 3.7 Max(最新旗舰)',\n capabilities: ['complex', 'analysis', 'reasoning', 'code', 'creative'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: true,\n speed: 'fast'\n },\n 'qwen3.7-plus': {\n name: 'Qwen 3.7 Plus(均衡型)',\n capabilities: ['simple', 'chat', 'translate', 'analysis', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: false,\n speed: 'fast'\n },\n 'qwen3.6-flash': {\n name: 'Qwen 3.6 Flash(快速型)',\n capabilities: ['simple', 'chat', 'translate', 'summarize'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: false,\n speed: 'fast'\n },\n 'qwen3-max-2026-01-23': {\n name: 'Qwen3 Max(稳定版)',\n capabilities: ['complex', 'analysis', 'reasoning', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 262144,\n maxTokens: 65536,\n reasoning: false,\n speed: 'normal'\n }\n }\n }\n};\n\n// ========== 任务类型识别 ==========\n\nconst TASK_PATTERNS = {\n simple: [\n /^(你好|hi|hello|在吗|早安|晚安|你是谁|你叫什么)/i,\n /^(天气|搜索|翻译)/i,\n /^(谢谢|好的|明白|知道了|ok|yes|no|是|否)/i,\n ],\n search: [\n /^(?!.*(趋势|报表|数据|统计|排行|系统|分析)).*(搜索|查找|找一下|搜一下|查一下|search)/i,\n ],\n code: [\n /写一个|写一段|代码|编程|实现|函数|class|function|bug|debug|修复.*问题/,\n /api|接口|api|API|路由|端点|endpoint/,\n /docker|dockerfile|deploy|部署|ci|cd/,\n /登录|注册|认证|auth|token|jwt/i,\n ],\n analysis: [\n /分析|统计|对比|比较|评估|review|总结|归纳|汇报|报告|报表/,\n /为什么|如何|原理|机制|架构|设计方案/,\n /数据|图表|趋势|维度|dashboard/,\n /^查(一下|一?下)?(系统|趋势|数据|报表|统计|排行)/i,\n ],\n creative: [\n /写(一|篇|首|段)|创作|创意|文案|策划|脚本|方案/,\n /故事|文章|文章|内容|描述|形容/,\n ],\n complex: [\n /系统设计|架构设计|技术方案|整体方案/,\n /多轮|复杂|大型|综合/,\n /计划|规划|路线图|roadmap/,\n ]\n};\n\nfunction classifyTask(text) {\n if (!text) return 'simple';\n for (const [type, patterns] of Object.entries(TASK_PATTERNS)) {\n for (const p of patterns) {\n if (p.test(text)) return type;\n }\n }\n if (text.length > 500) return 'analysis';\n if (text.length > 200) return 'complex';\n return 'simple';\n}\n\n// ========== 路由表 (DeepSeek 云端为主, Ollama 本地为备用) ==========\n// 策略说明:云端模型有更好的质量/速度比,本地模型仅在离线/无 Key 时使用\n\nconst ROUTING_TABLE = {\n simple: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速响应' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n search: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端搜索问答' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n code: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端专业代码' },\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速备用' },\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地代码备用' },\n ],\n analysis: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度分析' },\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端快速备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n creative: [\n { provider: 'deepseek', model: 'deepseek-chat', reason: '云端创意写作' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n ],\n complex: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度处理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端快速备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地兜底' },\n ],\n reasoning: [\n { provider: 'deepseek', model: 'deepseek-v4-pro', reason: '云端深度推理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地深度推理备用' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地通用兜底' },\n ]\n};\n\n// ========== 配置管理 ==========\n\nlet providerConfigs = {};\nlet usageStats = { calls: 0, byProvider: {}, totalCost: 0, todayCost: 0 };\nlet strategyConfig = { mode: 'speed-first', backupModels: [] };\nlet roundRobinIndex = {};\n\n\n// === Strategy config ===\nfunction loadStrategy() {\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n if (fs.existsSync(cfgPath)) {\n var cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n strategyConfig.mode = cfg.strategy || 'speed-first';\n strategyConfig.backupModels = cfg.backupModels || [];\n }\n } catch(e) {\n strategyConfig = { mode: 'speed-first', backupModels: [] };\n }\n}\n\nfunction setStrategy(mode, backupModels) {\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n var cfg = {};\n try { cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8')); } catch(e) {}\n cfg.strategy = mode || cfg.strategy || 'speed-first';\n if (backupModels) cfg.backupModels = backupModels;\n fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));\n // Update in-memory state\n strategyConfig.mode = cfg.strategy;\n strategyConfig.backupModels = cfg.backupModels || [];\n return true;\n } catch(e) { return false; }\n}\nfunction loadConfig() {\n loadStrategy();\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n if (fs.existsSync(cfgPath)) {\n const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n providerConfigs = cfg.providers || {};\n // Merge apiKeys from provider-keys.json into providerConfigs\n try {\n var pkPath = path.join(BASE, 'provider-keys.json');\n if (fs.existsSync(pkPath)) {\n var pk = JSON.parse(fs.readFileSync(pkPath, 'utf-8'));\n Object.keys(pk).forEach(function(k) {\n if (providerConfigs[k] && pk[k]) {\n providerConfigs[k].apiKey = pk[k];\n } else if (pk[k] && !providerConfigs[k]) {\n if (['deepseek','openai','claude','gemini','tongyi','zhipu','siliconflow','ernie','yi','moonshot','doubao','hunyuan','step','minimax','baichuan','openrouter'].indexOf(k) >= 0) {\n providerConfigs[k] = { enabled: true, apiKey: pk[k], models: {} };\n }\n }\n });\n }\n } catch(pkE) {}\n // credential-store 覆盖(最高优先级)\n try {\n var credStore = require('./credential-store');\n Object.keys(providerConfigs).forEach(function(k) {\n var activeKey = credStore.getApiKey(k);\n if (activeKey) {\n providerConfigs[k].apiKey = activeKey;\n }\n });\n } catch(credE) {\n console.error('[model-router] credential-store 读取失败:', credE.message);\n }\n if (cfg.routingOverride) {\n Object.assign(ROUTING_TABLE, cfg.routingOverride);\n }\n if (cfg.defaults) {\n Object.keys(providerConfigs).forEach(p => {\n const def = cfg.defaults;\n if (!providerConfigs[p].models) providerConfigs[p].models = {};\n Object.keys(DEFAULT_PROVIDERS[p]?.models || {}).forEach(m => {\n if (!providerConfigs[p].models[m]) {\n providerConfigs[p].models[m] = { enabled: true };\n }\n });\n });\n }\n } else {\n const defaultCfg = generateDefaultConfig();\n fs.writeFileSync(cfgPath, JSON.stringify(defaultCfg, null, 2), 'utf-8');\n providerConfigs = defaultCfg.providers;\n }\n } catch(e) {\n console.error('[ModelRouter] Config load error:', e.message);\n providerConfigs = {};\n }\n}\n\nfunction generateDefaultConfig() {\n return {\n enabled: true,\n strategy: 'speed-first',\n providers: {\n ollama: {\n enabled: true,\n noApiKey: true,\n apiKey: '',\n models: {\n 'qwen3.5:9b': { enabled: true, priority: 1 },\n 'deepseek-coder-v2:16b': { enabled: true, priority: 2 }\n }\n },\n deepseek: {\n enabled: true,\n apiKey: process.env.DEEPSEEK_API_KEY || '',\n models: {\n 'deepseek-v4-flash': { enabled: true, priority: 1 },\n 'deepseek-v4-pro': { enabled: true, priority: 2 },\n 'deepseek-chat': { enabled: true, priority: 3 },\n 'deepseek-reasoner': { enabled: true, priority: 4 }\n }\n },\n qwen: {\n enabled: true,\n apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '',\n models: {\n 'qwen3.7-max': { enabled: true, priority: 1 },\n 'qwen3.5-plus': { enabled: true, priority: 2 },\n 'qwen3-max-2026-01-23': { enabled: false, priority: 3 },\n 'qwen3-coder-plus': { enabled: false, priority: 4 }\n }\n }\n },\n routingOverride: {},\n createdAt: new Date().toISOString()\n };\n}\n\n// ========== 核心路由函数 ==========\n\nfunction selectModel(taskText, options = {}) {\n // 如果用户明确指定了模型名(如 deepseek-reasoner / deepseek-chat),直接返回精确路由\n if (options.preferredModel) {\n var prefModel = options.preferredModel;\n // 匹配 deepseek 提供者下的所有模型\n var provMap = { 'deepseek': 'deepseek', 'ds': 'deepseek' };\n var provider = 'deepseek';\n var rawModel = prefModel;\n // 支持 'deepseek/deepseek-reasoner' 或 'ds/deepseek-chat' 格式\n if (prefModel.indexOf('/') >= 0) {\n var parts = prefModel.split('/');\n provider = provMap[parts[0]] || parts[0];\n rawModel = parts[1];\n }\n loadConfigIfNeeded();\n var provCfg = providerConfigs[provider];\n var apiKey = (provCfg && provCfg.apiKey) || '';\n // 优先从 provider-keys.json 读取最信配置,再 fallback 到 env\n if (!apiKey) {\n try { var pkf = JSON.parse(require('fs').readFileSync(require('path').join(__dirname, '..', 'provider-keys.json'), 'utf-8')); if (pkf[provider]) apiKey = pkf[provider]; } catch(e) {}\n }\n if (!apiKey) {\n apiKey = process.env[provider.toUpperCase() + '_API_KEY'] || '';\n }\n if (!apiKey && provider !== 'ollama') {\n // 没 API Key 时仍然返回配置但标记空 key,让调用方处理\n }\n return {\n provider: provider,\n model: rawModel,\n apiKey: apiKey,\n apiBase: provCfg ? getApiBase(provider, rawModel, provCfg) : 'https://api.deepseek.com/v1/chat/completions',\n taskType: 'preferred',\n reason: '用户显式选择模型: ' + rawModel,\n cost: DEFAULT_PROVIDERS[provider]?.models[rawModel]?.cost || { input: 0.14, output: 0.28 },\n contextWindow: DEFAULT_PROVIDERS[provider]?.models[rawModel]?.contextWindow || 131072\n };\n }\n\n const taskType = options.taskType || classifyTask(taskText);\n const routes = ROUTING_TABLE[taskType] || ROUTING_TABLE.simple;\n\n loadConfigIfNeeded();\n\n \n // === Strategy-aware routing ===\n var strategy = options.strategy || strategyConfig.mode || 'speed-first';\n var backupModels = options.backupModels || strategyConfig.backupModels || [];\n\n // Get all enabled providers for current routing\n function getEnabledRoutes(specificType) {\n var type = specificType || taskType;\n var rts = ROUTING_TABLE[type] || ROUTING_TABLE.simple;\n return rts.filter(function(rt) {\n var provCfg = providerConfigs[rt.provider];\n if (!provCfg || !provCfg.enabled) return false;\n var modelCfg = provCfg.models && provCfg.models[rt.model];\n if (modelCfg && modelCfg.enabled === false) return false;\n if (!provCfg.noApiKey) {\n var ak = provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '';\n if (!ak) return false;\n }\n return true;\n });\n }\n\n // === Fixed mode: always use first enabled model ===\n if (strategy === 'fixed') {\n var enabledRoutes = getEnabledRoutes();\n if (enabledRoutes.length) {\n var rt = enabledRoutes[0];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: '固定模型: ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n return getLegacyFallback();\n }\n\n // === Fallback mode: try primary, then backup models ===\n if (strategy === 'fallback') {\n var enabledRoutes = getEnabledRoutes();\n for (var i = 0; i < enabledRoutes.length; i++) {\n var rt = enabledRoutes[i];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: 'fallback 主模型: ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n // Try backup models if any\n if (backupModels && backupModels.length) {\n for (var bi = 0; bi < backupModels.length; bi++) {\n var bm = backupModels[bi];\n for (var pi in providerConfigs) {\n var provCfg = providerConfigs[pi];\n if (!provCfg || !provCfg.enabled) continue;\n var mCfg = provCfg.models && provCfg.models[bm];\n if (mCfg && mCfg.enabled !== false) {\n var ak = provCfg.apiKey || process.env[pi.toUpperCase() + '_API_KEY'] || '';\n if (provCfg.noApiKey || ak) {\n return {\n provider: pi,\n model: bm,\n apiKey: ak,\n apiBase: getApiBase(pi, bm, provCfg),\n taskType: taskType,\n reason: 'fallback 备用: ' + bm,\n cost: DEFAULT_PROVIDERS[pi]?.models[bm]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[pi]?.models[bm]?.contextWindow || 131072\n };\n }\n }\n }\n }\n }\n return getLegacyFallback();\n }\n\n // === RoundRobin mode: cycle through enabled models ===\n if (strategy === 'roundrobin') {\n var enabledRoutes = getEnabledRoutes();\n if (!roundRobinIndex[taskType]) roundRobinIndex[taskType] = 0;\n if (enabledRoutes.length) {\n var idx = roundRobinIndex[taskType] % enabledRoutes.length;\n roundRobinIndex[taskType] = (idx + 1) % Math.max(1, enabledRoutes.length);\n var rt = enabledRoutes[idx];\n var provCfg = providerConfigs[rt.provider];\n return {\n provider: rt.provider,\n model: rt.model,\n apiKey: provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: '轮询 (' + (idx+1) + '/' + enabledRoutes.length + '): ' + rt.model,\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n };\n }\n return getLegacyFallback();\n }\n\n // === Smart mode (default): task-type aware + cost-aware ===\nfor (const route of routes) {\n const provCfg = providerConfigs[route.provider];\n if (!provCfg || !provCfg.enabled) continue;\n\n const modelCfg = provCfg.models && provCfg.models[route.model];\n if (modelCfg && modelCfg.enabled === false) continue;\n\n // 本地模型(Ollama)不需要 API Key\n if (!provCfg.noApiKey) {\n const apiKey = provCfg.apiKey || process.env[route.provider.toUpperCase() + '_API_KEY'] || '';\n if (!apiKey) continue;\n }\n\n return {\n provider: route.provider,\n model: route.model,\n apiKey: provCfg.apiKey || process.env[route.provider.toUpperCase() + '_API_KEY'] || '',\n apiBase: getApiBase(route.provider, route.model, provCfg),\n taskType: taskType,\n reason: route.reason,\n cost: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.contextWindow || 131072\n };\n }\n\n return getLegacyFallback();\n}\n\nfunction getApiBase(provider, model, provCfg) {\n const defaults = DEFAULT_PROVIDERS[provider];\n if (provCfg && provCfg.baseUrl) {\n let base = provCfg.baseUrl.replace(/\\/+$/, '');\n if (provCfg.apiFormat) base += provCfg.apiFormat;\n else base += defaults?.apiFormat || '/v1/chat/completions';\n return base;\n }\n var envUrl = process.env.OLLAMA_BASE_URL;\n if (!envUrl && defaults) return (defaults.baseUrl + (defaults.apiFormat || '/v1/chat/completions'));\n return envUrl || 'http://127.0.0.1:11434/v1/chat/completions';\n}\n\nlet configLoaded = false;\nfunction loadConfigIfNeeded() {\n if (!configLoaded) { loadConfig(); configLoaded = true; }\n}\n\nfunction getLegacyFallback() {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'ai-provider.json'), 'utf-8'));\n const p = cfg.provider || 'ollama';\n return {\n provider: p,\n model: cfg.model || 'qwen3.5:9b',\n apiKey: cfg.apiKey || '',\n apiBase: (function(){ if(cfg.apiBase) return cfg.apiBase; var d=DEFAULT_PROVIDERS[p]; return d ? d.baseUrl+(d.apiFormat||'/v1/chat/completions') : 'http://127.0.0.1:11434/v1/chat/completions'; })(),\n taskType: 'fallback',\n reason: 'Legacy config fallback',\n cost: { input: 0, output: 0 }\n };\n } catch(e) {\n return {\n provider: 'ollama',\n model: 'qwen3.5:9b',\n apiKey: '',\n apiBase: process.env.OLLAMA_BASE_URL || 'http://127.0.0.1:11434/v1/chat/completions',\n taskType: 'fallback',\n reason: 'Ultimate fallback',\n cost: { input: 0, output: 0 }\n };\n }\n}\n\n// ========== 用量统计 ==========\n\nfunction recordUsage(route, inputTokens, outputTokens) {\n usageStats.calls++;\n const key = route.provider + '/' + route.model;\n if (!usageStats.byProvider[key]) usageStats.byProvider[key] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };\n usageStats.byProvider[key].calls++;\n usageStats.byProvider[key].inputTokens += inputTokens || 0;\n usageStats.byProvider[key].outputTokens += outputTokens || 0;\n const cost = (route.cost?.input || 0) * (inputTokens || 0) / 1000000 + (route.cost?.output || 0) * (outputTokens || 0) / 1000000;\n usageStats.byProvider[key].cost += cost;\n usageStats.totalCost += cost;\n usageStats.todayCost += cost;\n}\n\nfunction getUsageStats() {\n return usageStats;\n}\n\nfunction resetDailyStats() {\n usageStats.todayCost = 0;\n}\n\n// ========== 路由测试 ==========\n\nfunction testRoute(text) {\n const taskType = classifyTask(text);\n const route = selectModel(text);\n return {\n input: text.substring(0, 100),\n classified: taskType,\n selected: route\n };\n}\n\n// ========== 注册HTTP路由 ==========\n\nfunction registerRouterRoutes(registerRoute, parseBody, json) {\n registerRoute(['POST'], /^\\/api\\/router\\/select$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = body.taskType || classifyTask(text);\n const route = selectModel(text, { taskType, strategy: body.strategy });\n json(res, { ok: true, taskType, route });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/classify$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = classifyTask(text);\n json(res, { ok: true, taskType, input: text.substring(0, 200) });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/stats$/, (req, res) => {\n json(res, { ok: true, ...usageStats });\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/config$/, (req, res) => {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'model-router.json'), 'utf-8'));\n Object.keys(cfg.providers || {}).forEach(p => {\n if (cfg.providers[p].apiKey) cfg.providers[p].apiKey = cfg.providers[p].apiKey.slice(0,8) + '...';\n });\n json(res, { ok: true, config: cfg, routingTable: ROUTING_TABLE });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/config$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const cfgPath = path.join(BASE, 'model-router.json');\n const current = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (body.providers) {\n Object.keys(body.providers).forEach(p => {\n if (!current.providers[p]) current.providers[p] = {};\n Object.assign(current.providers[p], body.providers[p]);\n });\n }\n if (body.strategy) current.strategy = body.strategy;\n fs.writeFileSync(cfgPath, JSON.stringify(current, null, 2), 'utf-8');\n configLoaded = false;\n loadConfig();\n json(res, { ok: true, message: '配置已更新' });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n}\n\nloadConfig();\n\n\n// === Save enabled models from frontend settings ===\n\n// === Get config summary for frontend ===\nfunction getConfigSummary() {\n loadConfigIfNeeded();\n return {\n providers: providerConfigs\n };\n}\n\nfunction getModelList() {\n loadConfigIfNeeded();\n var models = [];\n Object.keys(providerConfigs).forEach(function(p) {\n var pc = providerConfigs[p];\n if (pc.enabled === false || !pc.models) return;\n Object.keys(pc.models).forEach(function(m) {\n var mc = pc.models[m];\n if (mc.enabled === false) return;\n models.push({\n provider: p,\n model: m,\n name: mc.name || (p + ' · ' + m)\n });\n });\n });\n return { models: models };\n}\n\nfunction setEnabledModels(provider, enabledMap) {\n loadConfigIfNeeded();\n if (!providerConfigs[provider]) return false;\n var provCfg = providerConfigs[provider];\n if (!provCfg.models) provCfg.models = {};\n for (var modelId in enabledMap) {\n if (provCfg.models[modelId]) {\n provCfg.models[modelId].enabled = enabledMap[modelId] !== false;\n } else {\n provCfg.models[modelId] = { enabled: enabledMap[modelId] !== false, priority: 99 };\n }\n }\n // Persist to file (merge with default)\n var cfgPath = path.join(BASE, 'model-router.json');\n try {\n var config = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (!config.providers) config.providers = {};\n if (!config.providers[provider]) config.providers[provider] = {};\n if (!config.providers[provider].models) config.providers[provider].models = {};\n for (var modelId in enabledMap) {\n if (!config.providers[provider].models[modelId]) {\n config.providers[provider].models[modelId] = {};\n }\n config.providers[provider].models[modelId].enabled = enabledMap[modelId] !== false;\n }\n fs.writeFileSync(cfgPath, JSON.stringify(config, null, 2));\n return true;\n } catch(e) {\n console.error('[ModelRouter] Failed to save config:', e.message);\n return false;\n }\n}\n\n\n// ========== 模型自动切换(带健康检查 + fallback链) ==========\n\n/**\n * 返回当前可用模型的排序列表(按健康度),供调用者做自动 Fallback。\n * 返回格式:[{ provider, model, apiKey, apiBase, reason }, ...]\n * 会按配置的优先级排序,并跳过无 API Key 的供应商。\n */\nfunction selectModelWithFallback(taskText, options = {}) {\n loadConfigIfNeeded();\n\n const taskType = options.taskType || classifyTask(taskText);\n const routes = ROUTING_TABLE[taskType] || ROUTING_TABLE.simple;\n\n // 收集所有可用路由 + 备用模型\n var candidates = [];\n var seen = {};\n\n // 1. 主路由表的路由\n function addCandidate(rt) {\n var provCfg = providerConfigs[rt.provider];\n if (!provCfg || provCfg.enabled === false) return;\n var modelCfg = provCfg.models && provCfg.models[rt.model];\n if (modelCfg && modelCfg.enabled === false) return;\n var ak = provCfg.apiKey || process.env[rt.provider.toUpperCase() + '_API_KEY'] || '';\n if (!ak && !provCfg.noApiKey) return;\n var key = rt.provider + '/' + rt.model;\n if (seen[key]) return;\n seen[key] = true;\n candidates.push({\n provider: rt.provider,\n model: rt.model,\n apiKey: ak,\n apiBase: getApiBase(rt.provider, rt.model, provCfg),\n taskType: taskType,\n reason: rt.reason || ('候选模型: ' + rt.model),\n cost: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[rt.provider]?.models[rt.model]?.contextWindow || 131072\n });\n }\n\n routes.forEach(addCandidate);\n\n // 2. 备用模型(来自配置文件的 backupModels)\n if (options.backupModels) {\n options.backupModels.forEach(function(bm) {\n // 按 provider 匹配\n if (bm.indexOf('/') > 0) {\n var parts = bm.split('/');\n addCandidate({ provider: parts[0], model: parts[1], reason: '备用: ' + parts[1] });\n } else {\n // 无 provider 前缀,尝试所有有 API Key 的 provider\n Object.keys(providerConfigs).forEach(function(p) {\n var pc = providerConfigs[p];\n var ak = pc.apiKey || process.env[p.toUpperCase() + '_API_KEY'] || '';\n if (!ak && !pc.noApiKey) return;\n var mc = pc.models && pc.models[bm];\n if (mc && mc.enabled !== false) {\n addCandidate({ provider: p, model: bm, reason: '备用: ' + p + '/' + bm });\n }\n });\n }\n });\n }\n\n // 3. 策略排序\n var strategy = options.strategy || 'speed-first';\n if (strategy === 'speed-first') {\n // 速度优先:同一 provider 的先排\n candidates.sort(function(a, b) { return (a.cost?.output || 99) - (b.cost?.output || 99); });\n } else if (strategy === 'cost-first') {\n candidates.sort(function(a, b) { return (a.cost?.output || 99) - (b.cost?.output || 99); });\n } else if (strategy === 'quality-first') {\n candidates.sort(function(a, b) { return ((b.cost?.output || 0) - (a.cost?.output || 0)); });\n }\n\n return candidates;\n}\n\n// 用于 P95 等无感知降级场景\nfunction createFallbackChain(taskText, options = {}) {\n var list = selectModelWithFallback(taskText, options);\n var idx = 0;\n\n /**\n * 自动遍历候选链:当前一个失败,自动尝试下一个。\n * @param {Function} caller - async (model) => reply,模型参数就是候选对象\n * @returns {Object} { reply, model, fallbacks: number }\n */\n async function execute(caller) {\n var lastErr = null;\n for (; idx < list.length; idx++) {\n var m = list[idx];\n try {\n var reply = await caller(m);\n if (reply) {\n return { reply: reply, model: m.provider + '/' + m.model, fallbacks: idx };\n }\n } catch (e) {\n lastErr = e;\n console.log('[Fallback] ' + m.provider + '/' + m.model + ' 失败: ' + e.message + ',尝试下一个... (' + (list.length - idx - 1) + ' 个剩余)');\n }\n }\n throw lastErr || new Error('所有模型都失败');\n }\n\n return { execute: execute, list: list };\n}\n\n// === Strategy API ===\nmodule.exports = {\n classifyTask,\n selectModel,\n recordUsage,\n getUsageStats,\n resetDailyStats,\n testRoute,\n registerRouterRoutes,\n getConfigSummary,\n getModelList,\n setEnabledModels,\n loadStrategy,\n setStrategy,\n selectModelWithFallback,\n createFallbackChain\n};\n"}, "31e81b020b05869ec1d80e262adc3d1f3b7530c6": {"t": 1785766823.4868593, "n": 14905, "h": "180a993ed308096e", "c": "/**\n * eCompany Multi-Model Router v1.0\n * 多模型智能路由 — 根据任务类型自动选择最优模型\n * \n * 路由策略:\n * - 主模型: Ollama 本地 (Qwen 3.5 9B / DeepSeek Coder V2)\n * - 备用: DeepSeek V4 Flash 云端\n * - 兜底: Qwen 免费云端\n */\nconst fs = require('fs');\nconst path = require('path');\nconst BASE = path.join(__dirname, '..');\n\n// ========== 默认模型配置 ==========\nconst DEFAULT_PROVIDERS = {\n ollama: {\n name: 'Ollama (本地)',\n baseUrl: 'http://127.0.0.1:11434',\n apiFormat: '/v1/chat/completions',\n noApiKey: true,\n models: {\n 'qwen3.5:9b': {\n name: 'Qwen 3.5 9B',\n capabilities: ['simple', 'search', 'translate', 'summarize', 'analysis', 'code', 'chat'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-coder-v2:16b': {\n name: 'DeepSeek Coder V2 16B',\n capabilities: ['code', 'debug', 'analysis', 'reasoning'],\n cost: { input: 0, output: 0 },\n contextWindow: 32768,\n maxTokens: 8192,\n reasoning: true,\n speed: 'normal'\n }\n }\n },\n deepseek: {\n name: 'DeepSeek',\n baseUrl: 'https://api.deepseek.com',\n apiFormat: '/v1/chat/completions',\n models: {\n 'deepseek-v4-flash': {\n name: 'DeepSeek V4 Flash',\n capabilities: ['simple', 'search', 'translate', 'summarize'],\n cost: { input: 0.14, output: 0.28 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'fast'\n },\n 'deepseek-v4-pro': {\n name: 'DeepSeek V4 Pro',\n capabilities: ['analysis', 'code', 'reasoning', 'complex'],\n cost: { input: 1.74, output: 3.48 },\n contextWindow: 1000000,\n maxTokens: 384000,\n reasoning: true,\n speed: 'normal'\n },\n 'deepseek-chat': {\n name: 'DeepSeek Chat (V3)',\n capabilities: ['simple', 'chat', 'translate'],\n cost: { input: 0.28, output: 0.42 },\n contextWindow: 131072,\n maxTokens: 8192,\n reasoning: false,\n speed: 'fast'\n },\n 'deepseek-reasoner': {\n name: 'DeepSeek Reasoner',\n capabilities: ['reasoning', 'analysis', 'math', 'code'],\n cost: { input: 0.28, output: 0.42 },\n contextWindow: 131072,\n maxTokens: 65536,\n reasoning: true,\n speed: 'slow'\n }\n }\n },\n qwen: {\n name: 'Qwen (阿里云)',\n baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n apiFormat: '',\n models: {\n 'qwen3.5-plus': {\n name: 'Qwen 3.5 Plus',\n capabilities: ['simple', 'chat', 'translate', 'analysis', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 1000000,\n maxTokens: 65536,\n reasoning: false,\n speed: 'fast'\n },\n 'qwen3-max-2026-01-23': {\n name: 'Qwen3 Max',\n capabilities: ['complex', 'analysis', 'reasoning', 'code'],\n cost: { input: 0, output: 0 },\n contextWindow: 262144,\n maxTokens: 65536,\n reasoning: false,\n speed: 'normal'\n },\n 'qwen3-coder-plus': {\n name: 'Qwen3 Coder Plus',\n capabilities: ['code', 'debug', 'analysis'],\n cost: { input: 0, output: 0 },\n contextWindow: 262144,\n maxTokens: 65536,\n reasoning: false,\n speed: 'normal'\n }\n }\n }\n};\n\n// ========== 任务类型识别 ==========\n\nconst TASK_PATTERNS = {\n simple: [\n /^(你好|hi|hello|在吗|早安|晚安|你是谁|你叫什么)/i,\n /^(天气|搜索|翻译)/i,\n /^(谢谢|好的|明白|知道了|ok|yes|no|是|否)/i,\n ],\n search: [\n /^(?!.*(趋势|报表|数据|统计|排行|系统|分析)).*(搜索|查找|找一下|搜一下|查一下|search)/i,\n ],\n code: [\n /写一个|写一段|代码|编程|实现|函数|class|function|bug|debug|修复.*问题/,\n /api|接口|api|API|路由|端点|endpoint/,\n /docker|dockerfile|deploy|部署|ci|cd/,\n /登录|注册|认证|auth|token|jwt/i,\n ],\n analysis: [\n /分析|统计|对比|比较|评估|review|总结|归纳|汇报|报告|报表/,\n /为什么|如何|原理|机制|架构|设计方案/,\n /数据|图表|趋势|维度|dashboard/,\n /^查(一下|一?下)?(系统|趋势|数据|报表|统计|排行)/i,\n ],\n creative: [\n /写(一|篇|首|段)|创作|创意|文案|策划|脚本|方案/,\n /故事|文章|文章|内容|描述|形容/,\n ],\n complex: [\n /系统设计|架构设计|技术方案|整体方案/,\n /多轮|复杂|大型|综合/,\n /计划|规划|路线图|roadmap/,\n ]\n};\n\nfunction classifyTask(text) {\n if (!text) return 'simple';\n for (const [type, patterns] of Object.entries(TASK_PATTERNS)) {\n for (const p of patterns) {\n if (p.test(text)) return type;\n }\n }\n if (text.length > 500) return 'analysis';\n if (text.length > 200) return 'complex';\n return 'simple';\n}\n\n// ========== 路由表 (Ollama 为主, DeepSeek 云端备用) ==========\n\nconst ROUTING_TABLE = {\n simple: [\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地快速响应' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n ],\n search: [\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地搜索问答' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n ],\n code: [\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地代码模型' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地通用备用' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端兜底' },\n ],\n analysis: [\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地分析推理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n ],\n creative: [\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地创意写作' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n ],\n complex: [\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地处理' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端备用' },\n ],\n reasoning: [\n { provider: 'ollama', model: 'deepseek-coder-v2:16b', reason: '本地深度推理' },\n { provider: 'ollama', model: 'qwen3.5:9b', reason: '本地备用' },\n { provider: 'deepseek', model: 'deepseek-v4-flash', reason: '云端兜底' },\n ]\n};\n\n// ========== 配置管理 ==========\n\nlet providerConfigs = {};\nlet usageStats = { calls: 0, byProvider: {}, totalCost: 0, todayCost: 0 };\n\nfunction loadConfig() {\n try {\n const cfgPath = path.join(BASE, 'model-router.json');\n if (fs.existsSync(cfgPath)) {\n const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n providerConfigs = cfg.providers || {};\n if (cfg.routingOverride) {\n Object.assign(ROUTING_TABLE, cfg.routingOverride);\n }\n if (cfg.defaults) {\n Object.keys(providerConfigs).forEach(p => {\n const def = cfg.defaults;\n if (!providerConfigs[p].models) providerConfigs[p].models = {};\n Object.keys(DEFAULT_PROVIDERS[p]?.models || {}).forEach(m => {\n if (!providerConfigs[p].models[m]) {\n providerConfigs[p].models[m] = { enabled: true };\n }\n });\n });\n }\n // 从 ai-provider.json 同步 Key(确保 Key 最新)\n try {\n var _apPath = path.join(BASE, 'ai-provider.json');\n if (fs.existsSync(_apPath)) {\n var _apCfg = JSON.parse(fs.readFileSync(_apPath, 'utf-8'));\n if (_apCfg.apiKey && _apCfg.apiKey.length > 10) {\n Object.keys(providerConfigs).forEach(function(_p) {\n if (_p === 'deepseek' || _p === _apCfg.provider) {\n providerConfigs[_p].apiKey = _apCfg.apiKey;\n }\n });\n }\n }\n } catch(_ape) {}\n } else {\n const defaultCfg = generateDefaultConfig();\n fs.writeFileSync(cfgPath, JSON.stringify(defaultCfg, null, 2), 'utf-8');\n providerConfigs = defaultCfg.providers;\n }\n } catch(e) {\n console.error('[ModelRouter] Config load error:', e.message);\n providerConfigs = {};\n }\n}\n\nfunction generateDefaultConfig() {\n return {\n enabled: true,\n strategy: 'speed-first',\n providers: {\n ollama: {\n enabled: true,\n noApiKey: true,\n apiKey: '',\n models: {\n 'qwen3.5:9b': { enabled: true, priority: 1 },\n 'deepseek-coder-v2:16b': { enabled: true, priority: 2 }\n }\n },\n deepseek: {\n enabled: true,\n apiKey: (function(){try{var c=JSON.parse(fs.readFileSync(path.join(BASE,'ai-provider.json'),'utf-8'));return c.apiKey||process.env.DEEPSEEK_API_KEY||''}catch(e){return process.env.DEEPSEEK_API_KEY||''}})(),\n models: {\n 'deepseek-v4-flash': { enabled: true, priority: 1 },\n 'deepseek-v4-pro': { enabled: false, priority: 2 },\n 'deepseek-chat': { enabled: false, priority: 3 },\n 'deepseek-reasoner': { enabled: false, priority: 4 }\n }\n },\n qwen: {\n enabled: true,\n apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '',\n models: {\n 'qwen3.5-plus': { enabled: true, priority: 1 },\n 'qwen3-max-2026-01-23': { enabled: false, priority: 2 },\n 'qwen3-coder-plus': { enabled: false, priority: 3 }\n }\n }\n },\n routingOverride: {},\n createdAt: new Date().toISOString()\n };\n}\n\n// ========== 核心路由函数 ==========\n\nfunction selectModel(taskText, options = {}) {\n const taskType = options.taskType || classifyTask(taskText);\n const routes = ROUTING_TABLE[taskType] || ROUTING_TABLE.simple;\n const strategy = options.strategy || 'speed-first';\n\n loadConfigIfNeeded();\n\n for (const route of routes) {\n const provCfg = providerConfigs[route.provider];\n if (!provCfg || !provCfg.enabled) continue;\n\n const modelCfg = provCfg.models && provCfg.models[route.model];\n if (modelCfg && modelCfg.enabled === false) continue;\n\n // 本地模型(Ollama)不需要 API Key\n if (!provCfg.noApiKey) {\n const apiKey = provCfg.apiKey || process.env[route.provider.toUpperCase() + '_API_KEY'] || '';\n if (!apiKey) continue;\n }\n\n return {\n provider: route.provider,\n model: route.model,\n apiKey: provCfg.apiKey || '',\n apiBase: getApiBase(route.provider, route.model, provCfg),\n taskType: taskType,\n reason: route.reason,\n cost: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.cost || { input: 0, output: 0 },\n contextWindow: DEFAULT_PROVIDERS[route.provider]?.models[route.model]?.contextWindow || 131072\n };\n }\n\n return getLegacyFallback();\n}\n\nfunction getApiBase(provider, model, provCfg) {\n const defaults = DEFAULT_PROVIDERS[provider];\n if (provCfg && provCfg.baseUrl) {\n let base = provCfg.baseUrl.replace(/\\/+$/, '');\n if (provCfg.apiFormat) base += provCfg.apiFormat;\n else base += defaults?.apiFormat || '/v1/chat/completions';\n return base;\n }\n if (defaults) {\n return (defaults.baseUrl + (defaults.apiFormat || '/v1/chat/completions'));\n }\n return 'http://127.0.0.1:11434/v1/chat/completions';\n}\n\nlet configLoaded = false;\nfunction loadConfigIfNeeded() {\n if (!configLoaded) { loadConfig(); configLoaded = true; }\n}\n\nfunction getLegacyFallback() {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'ai-provider.json'), 'utf-8'));\n const p = cfg.provider || 'ollama';\n return {\n provider: p,\n model: cfg.model || 'qwen3.5:9b',\n apiKey: cfg.apiKey || '',\n apiBase: cfg.apiBase || 'http://127.0.0.1:11434/v1/chat/completions',\n taskType: 'fallback',\n reason: 'Legacy config fallback',\n cost: { input: 0, output: 0 }\n };\n } catch(e) {\n return {\n provider: 'ollama',\n model: 'qwen3.5:9b',\n apiKey: '',\n apiBase: 'http://127.0.0.1:11434/v1/chat/completions',\n taskType: 'fallback',\n reason: 'Ultimate fallback',\n cost: { input: 0, output: 0 }\n };\n }\n}\n\n// ========== 用量统计 ==========\n\nfunction recordUsage(route, inputTokens, outputTokens) {\n usageStats.calls++;\n const key = route.provider + '/' + route.model;\n if (!usageStats.byProvider[key]) usageStats.byProvider[key] = { calls: 0, inputTokens: 0, outputTokens: 0, cost: 0 };\n usageStats.byProvider[key].calls++;\n usageStats.byProvider[key].inputTokens += inputTokens || 0;\n usageStats.byProvider[key].outputTokens += outputTokens || 0;\n const cost = (route.cost?.input || 0) * (inputTokens || 0) / 1000000 + (route.cost?.output || 0) * (outputTokens || 0) / 1000000;\n usageStats.byProvider[key].cost += cost;\n usageStats.totalCost += cost;\n usageStats.todayCost += cost;\n}\n\nfunction getUsageStats() {\n return usageStats;\n}\n\nfunction resetDailyStats() {\n usageStats.todayCost = 0;\n}\n\n// ========== 路由测试 ==========\n\nfunction testRoute(text) {\n const taskType = classifyTask(text);\n const route = selectModel(text);\n return {\n input: text.substring(0, 100),\n classified: taskType,\n selected: route\n };\n}\n\n// ========== 注册HTTP路由 ==========\n\nfunction registerRouterRoutes(registerRoute, parseBody, json) {\n registerRoute(['POST'], /^\\/api\\/router\\/select$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = body.taskType || classifyTask(text);\n const route = selectModel(text, { taskType, strategy: body.strategy });\n json(res, { ok: true, taskType, route });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/classify$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const text = body.text || body.query || '';\n const taskType = classifyTask(text);\n json(res, { ok: true, taskType, input: text.substring(0, 200) });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/stats$/, (req, res) => {\n json(res, { ok: true, ...usageStats });\n });\n\n registerRoute(['GET'], /^\\/api\\/router\\/config$/, (req, res) => {\n try {\n const cfg = JSON.parse(fs.readFileSync(path.join(BASE, 'model-router.json'), 'utf-8'));\n Object.keys(cfg.providers || {}).forEach(p => {\n if (cfg.providers[p].apiKey) cfg.providers[p].apiKey = cfg.providers[p].apiKey.slice(0,8) + '...';\n });\n json(res, { ok: true, config: cfg, routingTable: ROUTING_TABLE });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n\n registerRoute(['POST'], /^\\/api\\/router\\/config$/, async (req, res) => {\n try {\n const body = await parseBody(req);\n const cfgPath = path.join(BASE, 'model-router.json');\n const current = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));\n if (body.providers) {\n Object.keys(body.providers).forEach(p => {\n if (!current.providers[p]) current.providers[p] = {};\n Object.assign(current.providers[p], body.providers[p]);\n });\n }\n if (body.strategy) current.strategy = body.strategy;\n fs.writeFileSync(cfgPath, JSON.stringify(current, null, 2), 'utf-8');\n configLoaded = false;\n loadConfig();\n json(res, { ok: true, message: '配置已更新' });\n } catch(e) { json(res, { ok: false, error: e.message }, 500); }\n });\n}\n\nloadConfig();\n\nmodule.exports = {\n classifyTask,\n selectModel,\n recordUsage,\n getUsageStats,\n resetDailyStats,\n testRoute,\n registerRouterRoutes\n};\n"}}}} |