Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/18388853038_guoshaowen-blog.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
184 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}, "backend/modules/quality-system.js": {"a0e92cca8533660972d0a16d2874b571292e53fa": {"t": 1786068265.6203735, "n": 18076, "h": "07de80eb03db7ca4", "c": "/**\n * eCompany P3: 质量体系模块\n * 功能:\n * 1. 任务质量自动评分(A/B/C/D)\n * 2. 审批流程(老板审批节点)\n * 3. 决策追溯(Audit Log)\n * 4. 质量报告\n * 5. CEO 工具增强\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst BASE = path.join(__dirname, '..');\nconst TASKS_FILE = path.join(BASE, 'tasks.json');\nconst AUDIT_LOG_FILE = path.join(BASE, 'audit-log.json');\nconst QUALITY_REPORT_FILE = path.join(BASE, 'quality-reports.json');\n\n// 最大审计日志条目数\nconst MAX_AUDIT_ENTRIES = 500;\n\n// ========== 辅助函数 ==========\n\nfunction readJSON(file, fallback) {\n try {\n if (fs.existsSync(file)) {\n var raw = fs.readFileSync(file, 'utf-8');\n if (raw.charCodeAt(0) === 0xFEFF || raw.charCodeAt(0) === 239)\n raw = raw.replace(/^[\\uFEFF\\xEF\\xBB\\xBF]+/, '');\n if (raw.trim().length === 0) return fallback;\n return JSON.parse(raw);\n }\n } catch(e) { /* ignore */ }\n return fallback;\n}\n\nfunction writeJSON(file, data) {\n try {\n var dir = path.dirname(file);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8');\n return true;\n } catch(e) { return false; }\n}\n\nfunction loadTasks() {\n return readJSON(TASKS_FILE, []);\n}\n\nfunction saveTasks(tasks) {\n return writeJSON(TASKS_FILE, tasks);\n}\n\nfunction uuid() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n// ========== AI 评分调用 ==========\n\n/**\n * 调用 AI 对任务结果进行质量评分\n * @param {string} taskTitle - 任务标题\n * @param {string} taskResult - 任务执行结果\n * @returns {Promise<Object>} 评分结果\n */\nasync function callAIScoring(taskTitle, taskResult) {\n // 尝试调用 AI 进行评分\n try {\n // 读取 AI 配置\n var prov = readJSON(path.join(BASE, 'ai-provider.json'), { provider: 'deepseek', apiKey: '', apiBase: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-chat' });\n var apiKey = prov.apiKey || process.env.DEEPSEEK_API_KEY || '';\n\n // 如果未配置 API Key,使用规则评分\n if (!apiKey) {\n return ruleBasedScoring(taskTitle, taskResult);\n }\n\n var apiBase = prov.apiBase || 'https://api.deepseek.com/v1/chat/completions';\n var model = prov.model || 'deepseek-chat';\n var provider = (prov.provider || 'deepseek').toLowerCase();\n\n var response = await fetch(apiBase, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey\n },\n body: JSON.stringify({\n model: model,\n messages: [\n {\n role: 'system',\n content: '你是一个专业的任务质量评估专家。请对以下任务结果进行评分,返回严格的 JSON 格式(不要任何 markdown 标记)。\\n\\n评分维度(每项 0-100):\\n- completeness(完成度):任务目标的达成程度\\n- quality(质量):输出内容的专业性和准确性\\n- timeliness(时效性):对截止时间的遵守程度\\n- innovation(创新性):解决方案的创新程度\\n\\n总体等级:\\n- A(优秀 85-100):全面超出预期\\n- B(良好 70-84):达到预期\\n- C(及格 55-69):基本完成但存在不足\\n- D(不及格 0-54):未达标\\n\\n返回格式:\\n{\"overall\":\"A\",\"dimensions\":{\"completeness\":85,\"quality\":80,\"timeliness\":90,\"innovation\":70},\"feedback\":\"简要评价\"}'\n },\n {\n role: 'user',\n content: '任务标题:' + taskTitle + '\\n\\n任务结果:' + taskResult\n }\n ],\n max_tokens: 512,\n temperature: 0.3\n }),\n signal: AbortSignal.timeout(15000)\n });\n\n if (!response.ok) {\n return ruleBasedScoring(taskTitle, taskResult);\n }\n\n var data = await response.json();\n var content = (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) || '';\n\n // 尝试解析 AI 返回的 JSON\n try {\n // 清理可能的 markdown 标记\n var cleanContent = content.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n var parsed = JSON.parse(cleanContent);\n if (parsed.overall && parsed.dimensions) {\n return {\n overall: parsed.overall,\n dimensions: {\n completeness: Math.min(100, Math.max(0, parsed.dimensions.completeness || 0)),\n quality: Math.min(100, Math.max(0, parsed.dimensions.quality || 0)),\n timeliness: Math.min(100, Math.max(0, parsed.dimensions.timeliness || 0)),\n innovation: Math.min(100, Math.max(0, parsed.dimensions.innovation || 0))\n },\n feedback: parsed.feedback || 'AI 评分完成',\n reviewMethod: 'ai'\n };\n }\n } catch(e) { /* parse failed, fallback to rule-based */ }\n\n return ruleBasedScoring(taskTitle, taskResult);\n } catch(e) {\n return ruleBasedScoring(taskTitle, taskResult);\n }\n}\n\n/**\n * 基于规则的评分(AI 不可用时回退)\n */\nfunction ruleBasedScoring(taskTitle, taskResult) {\n var resultText = (taskResult || '') + ' ' + (taskTitle || '');\n var score = 70; // 基础分\n\n // 完成度评价\n var completeness = 70;\n if (resultText.length > 200) completeness += 15;\n if (resultText.length > 500) completeness += 5;\n if (resultText.includes('完成') || resultText.includes('success') || resultText.includes('成功')) completeness += 5;\n if (resultText.includes('实现') || resultText.includes('implement')) completeness += 5;\n\n // 质量评价\n var quality = 70;\n if (resultText.includes('测试') || resultText.includes('test') || resultText.includes('验证')) quality += 10;\n if (resultText.includes('优化') || resultText.includes('optimize') || resultText.includes('重构')) quality += 10;\n if (resultText.includes('文档') || resultText.includes('report') || resultText.includes('报告')) quality += 5;\n\n // 时效性:无截止时间则给中等分\n var timeliness = 75;\n\n // 创新性\n var innovation = 60;\n if (resultText.includes('设计') || resultText.includes('方案') || resultText.includes('架构')) innovation += 10;\n if (resultText.includes('改进') || resultText.includes('创新') || resultText.includes('新方案')) innovation += 15;\n\n // 限制范围\n completeness = Math.min(100, Math.max(0, completeness));\n quality = Math.min(100, Math.max(0, quality));\n timeliness = Math.min(100, Math.max(0, timeliness));\n innovation = Math.min(100, Math.max(0, innovation));\n\n // 计算总分(非简单平均,用于定等级)\n var weightedScore = completeness * 0.35 + quality * 0.30 + timeliness * 0.20 + innovation * 0.15;\n\n var overall;\n if (weightedScore >= 85) overall = 'A';\n else if (weightedScore >= 70) overall = 'B';\n else if (weightedScore >= 55) overall = 'C';\n else overall = 'D';\n\n return {\n overall: overall,\n dimensions: { completeness: completeness, quality: quality, timeliness: timeliness, innovation: innovation },\n feedback: overall === 'A' ? '出色完成,表现优异' : overall === 'B' ? '良好完成任务' : overall === 'C' ? '基本完成,存在改进空间' : '未达到预期,需改进',\n reviewMethod: 'rule-based'\n };\n}\n\n// ========== Audit Log ==========\n\nfunction loadAuditLog() {\n return readJSON(AUDIT_LOG_FILE, { entries: [] });\n}\n\nfunction saveAuditLog(log) {\n if (log.entries.length > MAX_AUDIT_ENTRIES) {\n log.entries = log.entries.slice(-MAX_AUDIT_ENTRIES);\n }\n writeJSON(AUDIT_LOG_FILE, log);\n}\n\n/**\n * 记录审计条目\n * @param {string} actor - 执行者 ID\n * @param {string} action - 操作类型\n * @param {string} target - 目标/接收者\n * @param {Object} detail - 详情\n * @param {string} result - 结果(success/failure)\n */\nfunction logAudit(actor, action, target, detail, result) {\n var log = loadAuditLog();\n var entry = {\n id: uuid(),\n timestamp: new Date().toISOString(),\n actor: actor,\n action: action,\n target: target || '',\n detail: detail || {},\n result: result || 'success'\n };\n log.entries.push(entry);\n saveAuditLog(log);\n return entry;\n}\n\n/**\n * 查询审计日志\n * @param {Object} options\n * @param {string} options.since - ISO 起始时间\n * @param {string} options.actor - 按执行者筛选\n * @param {string} options.action - 按操作类型筛选\n * @param {number} options.limit - 返回条数限制\n * @param {number} options.offset - 偏移量\n * @returns {Array}\n */\nfunction queryAuditLog(options) {\n var log = loadAuditLog();\n var entries = log.entries;\n\n if (options) {\n if (options.since) {\n var sinceTs = new Date(options.since).getTime();\n entries = entries.filter(function(e) {\n return new Date(e.timestamp).getTime() >= sinceTs;\n });\n }\n if (options.actor) {\n entries = entries.filter(function(e) { return e.actor === options.actor; });\n }\n if (options.action) {\n entries = entries.filter(function(e) { return e.action === options.action; });\n }\n }\n\n // 按时间降序\n entries = entries.sort(function(a, b) {\n return b.timestamp.localeCompare(a.timestamp);\n });\n\n var offset = (options && options.offset) || 0;\n var limit = (options && options.limit) || 50;\n\n return entries.slice(offset, offset + limit);\n}\n\n// ========== 质量评分 ==========\n\n/**\n * 对任务进行 AI 质量评分并更新任务记录\n * @param {string} taskId - 任务 ID\n * @param {string} result - 任务执行结果\n * @param {Object} options - 可选参数\n * @param {string} options.reviewedBy - 评分执行者\n * @returns {Promise<Object>} 评分结果\n */\nasync function scoreTask(taskId, result, options) {\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (!task) throw new Error('任务不存在: ' + taskId);\n\n var scoreResult = await callAIScoring(task.title || '', result || task.result || '');\n\n task.score = {\n overall: scoreResult.overall,\n dimensions: scoreResult.dimensions,\n feedback: scoreResult.feedback,\n reviewedBy: (options && options.reviewedBy) || 'ai_reviewer',\n reviewedAt: new Date().toISOString()\n };\n\n // 更新 task\n task.updatedAt = new Date().toISOString();\n saveTasks(tasks);\n\n // 记录审计\n logAudit('system', 'quality_score', task.assigneeId || '', {\n taskId: taskId,\n title: task.title,\n score: scoreResult.overall,\n dimensions: scoreResult.dimensions\n }, 'success');\n\n return scoreResult;\n}\n\n/**\n * 获取任务评分\n * @param {string} taskId\n * @returns {Object|null}\n */\nfunction getTaskScore(taskId) {\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (!task || !task.score) return null;\n return task.score;\n}\n\n/**\n * 获取指定 Agent 的质量报告\n * @param {string} agentId\n * @returns {Object}\n */\nfunction getAgentQualityReport(agentId) {\n var tasks = loadTasks();\n var agentTasks = tasks.filter(function(t) {\n return t.assigneeId === agentId && t.score;\n });\n\n if (agentTasks.length === 0) {\n return {\n agentId: agentId,\n totalTasks: 0,\n scoredTasks: 0,\n averageScore: 'N/A',\n scoreDistribution: { A: 0, B: 0, C: 0, D: 0 },\n recentScores: []\n };\n }\n\n var distribution = { A: 0, B: 0, C: 0, D: 0 };\n var totalWeighted = 0;\n\n agentTasks.forEach(function(t) {\n if (t.score && t.score.overall) {\n distribution[t.score.overall] = (distribution[t.score.overall] || 0) + 1;\n }\n });\n\n // 计算平均分(映射 A=90, B=75, C=60, D=40)\n var scoreMap = { A: 90, B: 75, C: 60, D: 40 };\n agentTasks.forEach(function(t) {\n if (t.score && t.score.overall && scoreMap[t.score.overall]) {\n totalWeighted += scoreMap[t.score.overall];\n }\n });\n\n var averageScore = (totalWeighted / agentTasks.length).toFixed(1);\n\n // 最近评分(按时间倒序)\n var recentScores = agentTasks\n .sort(function(a, b) {\n return (b.score && b.score.reviewedAt || '').localeCompare(a.score && a.score.reviewedAt || '');\n })\n .slice(0, 10)\n .map(function(t) {\n return {\n taskId: t.id,\n title: t.title,\n overall: t.score.overall,\n reviewedAt: t.score.reviewedAt\n };\n });\n\n return {\n agentId: agentId,\n totalTasks: tasks.filter(function(t) { return t.assigneeId === agentId; }).length,\n scoredTasks: agentTasks.length,\n averageScore: parseFloat(averageScore),\n scoreDistribution: distribution,\n recentScores: recentScores\n };\n}\n\n/**\n * 获取全局质量报告\n * @returns {Object}\n */\nfunction getOverallReport() {\n var tasks = loadTasks();\n\n // 各 Agent 统计\n var agentMap = {};\n tasks.forEach(function(t) {\n if (!t.assigneeId) return;\n if (!agentMap[t.assigneeId]) {\n agentMap[t.assigneeId] = { total: 0, scored: 0, scoreSum: 0, distribution: { A: 0, B: 0, C: 0, D: 0 } };\n }\n agentMap[t.assigneeId].total++;\n if (t.score && t.score.overall) {\n agentMap[t.assigneeId].scored++;\n agentMap[t.assigneeId].distribution[t.score.overall] = (agentMap[t.assigneeId].distribution[t.score.overall] || 0) + 1;\n }\n });\n\n var scoreMap = { A: 90, B: 75, C: 60, D: 40 };\n var agentReports = {};\n Object.keys(agentMap).forEach(function(aid) {\n var d = agentMap[aid];\n var avg = 'N/A';\n if (d.scored > 0) {\n var sum = 0;\n Object.keys(d.distribution).forEach(function(grade) {\n sum += scoreMap[grade] * d.distribution[grade];\n });\n avg = (sum / d.scored).toFixed(1);\n }\n agentReports[aid] = { totalTasks: d.total, scoredTasks: d.scored, averageScore: avg, scoreDistribution: d.distribution };\n });\n\n // 全局评分分布\n var globalDist = { A: 0, B: 0, C: 0, D: 0 };\n tasks.forEach(function(t) {\n if (t.score && t.score.overall) {\n globalDist[t.score.overall] = (globalDist[t.score.overall] || 0) + 1;\n }\n });\n\n // 待审批数量\n var pendingApproval = tasks.filter(function(t) { return t.approval && t.approval.status === 'pending'; }).length;\n\n // 近期趋势(最近 20 个评分)\n var recentTasks = tasks\n .filter(function(t) { return t.score && t.score.reviewedAt; })\n .sort(function(a, b) {\n return (b.score.reviewedAt || '').localeCompare(a.score.reviewedAt || '');\n })\n .slice(0, 20)\n .map(function(t) {\n return {\n taskId: t.id,\n title: t.title,\n overall: t.score.overall,\n reviewedAt: t.score.reviewedAt\n };\n });\n\n return {\n agents: agentReports,\n globalDistribution: globalDist,\n totalScored: tasks.filter(function(t) { return t.score; }).length,\n pendingApproval: pendingApproval,\n trend: recentTasks,\n generatedAt: new Date().toISOString()\n };\n}\n\n// ========== 审批流程 ==========\n\n/**\n * 标记任务需要审批\n * @param {string} taskId\n * @param {string} reason - 审批原因\n */\nfunction requireApproval(taskId, reason) {\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (!task) throw new Error('任务不存在: ' + taskId);\n\n task.requires_approval = true;\n task.approval = {\n status: 'pending',\n approvedBy: null,\n approvedAt: null,\n comment: '',\n reason: reason || '需要老板确认'\n };\n\n // 如果任务完成了,改为 pending_approval 状态\n if (task.status === 'done' || task.status === 'completed') {\n task.status = 'pending_approval';\n }\n\n task.updatedAt = new Date().toISOString();\n saveTasks(tasks);\n\n logAudit('system', 'require_approval', task.assigneeId || '', {\n taskId: taskId,\n title: task.title,\n reason: reason\n }, 'success');\n\n return task;\n}\n\n/**\n * 老板审批通过\n * @param {string} taskId\n * @param {string} by - 审批人\n * @param {string} comment - 审批意见\n */\nfunction approveTask(taskId, by, comment) {\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (!task) throw new Error('任务不存在: ' + taskId);\n if (!task.approval || task.approval.status !== 'pending') throw new Error('任务不在待审批状态');\n\n task.approval.status = 'approved';\n task.approval.approvedBy = by || 'boss';\n task.approval.approvedAt = new Date().toISOString();\n task.approval.comment = comment || '';\n task.status = 'approved';\n task.updatedAt = new Date().toISOString();\n saveTasks(tasks);\n\n logAudit(by || 'boss', 'approve_task', task.assigneeId || '', {\n taskId: taskId,\n title: task.title,\n comment: comment\n }, 'success');\n\n return task;\n}\n\n/**\n * 老板驳回任务\n * @param {string} taskId\n * @param {string} by - 审批人\n * @param {string} comment - 驳回原因\n */\nfunction rejectTask(taskId, by, comment) {\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (!task) throw new Error('任务不存在: ' + taskId);\n if (!task.approval || task.approval.status !== 'pending') throw new Error('任务不在待审批状态');\n\n task.approval.status = 'rejected';\n task.approval.approvedBy = by || 'boss';\n task.approval.approvedAt = new Date().toISOString();\n task.approval.comment = comment || '';\n task.status = 'rejected';\n task.updatedAt = new Date().toISOString();\n saveTasks(tasks);\n\n logAudit(by || 'boss', 'reject_task', task.assigneeId || '', {\n taskId: taskId,\n title: task.title,\n reason: comment\n }, 'success');\n\n return task;\n}\n\n/**\n * 获取待审批任务列表\n * @returns {Array}\n */\nfunction getPendingApprovals() {\n var tasks = loadTasks();\n return tasks.filter(function(t) {\n return t.approval && t.approval.status === 'pending';\n }).map(function(t) {\n return {\n id: t.id,\n title: t.title,\n description: t.description || '',\n status: t.status,\n assigneeId: t.assigneeId,\n priority: t.priority || 'medium',\n score: t.score || null,\n approval: t.approval,\n createdAt: t.createdAt,\n updatedAt: t.updatedAt\n };\n });\n}\n\n/**\n * 任务完成后自动评分并检查是否需要审批\n * @param {string} taskId\n * @param {string} result\n * @param {Object} options\n * @returns {Promise<Object>}\n */\nasync function autoScoreAndApproval(taskId, result, options) {\n var resultData = {};\n\n // 1. 自动评分\n try {\n var scoreResult = await scoreTask(taskId, result, options);\n resultData.scored = true;\n resultData.score = scoreResult;\n } catch(e) {\n resultData.scored = false;\n resultData.scoreError = e.message;\n }\n\n // 2. 检查是否需要审批\n var tasks = loadTasks();\n var task = tasks.find(function(t) { return t.id === taskId; });\n if (task && task.requires_approval) {\n try {\n requireApproval(taskId, '任务完成,需老板审批');\n resultData.requireApproval = true;\n } catch(e) {\n resultData.requireApproval = false;\n resultData.approvalError = e.message;\n }\n } else {\n resultData.requireApproval = false;\n }\n\n return resultData;\n}\n\n// ========== 导出 ==========\n\nmodule.exports = {\n // 质量评分\n scoreTask: scoreTask,\n getTaskScore: getTaskScore,\n getAgentQualityReport: getAgentQualityReport,\n getOverallReport: getOverallReport,\n\n // 审批流程\n requireApproval: requireApproval,\n approveTask: approveTask,\n rejectTask: rejectTask,\n getPendingApprovals: getPendingApprovals,\n\n // 审计日志\n logAudit: logAudit,\n queryAuditLog: queryAuditLog,\n\n // 综合流程\n autoScoreAndApproval: autoScoreAndApproval,\n\n // 内部工具(供测试用)\n callAIScoring: callAIScoring,\n ruleBasedScoring: ruleBasedScoring\n};\n"}}}}