{"repo": "123-qw-as/beacon", "files": {"frontend/providers.json": {"4fc3ab1285422d3b317708a8542a91a8882eddab": {"t": 1785674191.0372386, "n": 1674, "h": "3ce60c2ac93e515f", "c": "[\n {\n \"id\": \"deepseek\",\n \"name\": \"DeepSeek\",\n \"apiBase\": \"https://api.deepseek.com/v1\",\n \"defaultModel\": \"openai/deepseek-chat\",\n \"strongModel\": \"openai/deepseek-reasoner\",\n \"figureModel\": \"openai/deepseek-chat\",\n \"website\": \"https://platform.deepseek.com\",\n \"description\": \"性价比高,适合数学建模\"\n },\n {\n \"id\": \"openai\",\n \"name\": \"OpenAI\",\n \"apiBase\": \"https://api.openai.com/v1\",\n \"defaultModel\": \"openai/gpt-4o-mini\",\n \"strongModel\": \"openai/gpt-4o\",\n \"figureModel\": \"openai/gpt-4o\",\n \"website\": \"https://platform.openai.com\",\n \"description\": \"GPT 系列,能力全面\"\n },\n {\n \"id\": \"zhipu\",\n \"name\": \"智谱 AI\",\n \"apiBase\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"defaultModel\": \"openai/glm-4-flash\",\n \"strongModel\": \"openai/glm-4\",\n \"figureModel\": \"openai/glm-4v\",\n \"website\": \"https://open.bigmodel.cn\",\n \"description\": \"GLM 系列,国产大模型\"\n },\n {\n \"id\": \"moonshot\",\n \"name\": \"月之暗面\",\n \"apiBase\": \"https://api.moonshot.cn/v1\",\n \"defaultModel\": \"openai/moonshot-v1-8k\",\n \"strongModel\": \"openai/moonshot-v1-32k\",\n \"figureModel\": \"openai/moonshot-v1-32k\",\n \"website\": \"https://platform.moonshot.cn\",\n \"description\": \"Kimi,长上下文优势\"\n },\n {\n \"id\": \"ollama\",\n \"name\": \"本地 Ollama\",\n \"apiBase\": \"http://localhost:11434/v1\",\n \"defaultModel\": \"ollama/llama3\",\n \"strongModel\": \"ollama/llama3\",\n \"figureModel\": \"ollama/llama3\",\n \"website\": \"https://ollama.com\",\n \"description\": \"本地部署,离线可用\"\n },\n {\n \"id\": \"custom\",\n \"name\": \"自定义\",\n \"apiBase\": \"\",\n \"defaultModel\": \"\",\n \"strongModel\": \"\",\n \"figureModel\": \"\",\n \"website\": \"\",\n \"description\": \"手动填写端点和模型\"\n }\n]\n"}, "05990eb9bf14055753eab2c1e27447eb85357573": {"t": 1785761235.378646, "n": 1674, "h": "3ce60c2ac93e515f", "c": "[\n {\n \"id\": \"deepseek\",\n \"name\": \"DeepSeek\",\n \"apiBase\": \"https://api.deepseek.com/v1\",\n \"defaultModel\": \"openai/deepseek-chat\",\n \"strongModel\": \"openai/deepseek-reasoner\",\n \"figureModel\": \"openai/deepseek-chat\",\n \"website\": \"https://platform.deepseek.com\",\n \"description\": \"性价比高,适合数学建模\"\n },\n {\n \"id\": \"openai\",\n \"name\": \"OpenAI\",\n \"apiBase\": \"https://api.openai.com/v1\",\n \"defaultModel\": \"openai/gpt-4o-mini\",\n \"strongModel\": \"openai/gpt-4o\",\n \"figureModel\": \"openai/gpt-4o\",\n \"website\": \"https://platform.openai.com\",\n \"description\": \"GPT 系列,能力全面\"\n },\n {\n \"id\": \"zhipu\",\n \"name\": \"智谱 AI\",\n \"apiBase\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"defaultModel\": \"openai/glm-4-flash\",\n \"strongModel\": \"openai/glm-4\",\n \"figureModel\": \"openai/glm-4v\",\n \"website\": \"https://open.bigmodel.cn\",\n \"description\": \"GLM 系列,国产大模型\"\n },\n {\n \"id\": \"moonshot\",\n \"name\": \"月之暗面\",\n \"apiBase\": \"https://api.moonshot.cn/v1\",\n \"defaultModel\": \"openai/moonshot-v1-8k\",\n \"strongModel\": \"openai/moonshot-v1-32k\",\n \"figureModel\": \"openai/moonshot-v1-32k\",\n \"website\": \"https://platform.moonshot.cn\",\n \"description\": \"Kimi,长上下文优势\"\n },\n {\n \"id\": \"ollama\",\n \"name\": \"本地 Ollama\",\n \"apiBase\": \"http://localhost:11434/v1\",\n \"defaultModel\": \"ollama/llama3\",\n \"strongModel\": \"ollama/llama3\",\n \"figureModel\": \"ollama/llama3\",\n \"website\": \"https://ollama.com\",\n \"description\": \"本地部署,离线可用\"\n },\n {\n \"id\": \"custom\",\n \"name\": \"自定义\",\n \"apiBase\": \"\",\n \"defaultModel\": \"\",\n \"strongModel\": \"\",\n \"figureModel\": \"\",\n \"website\": \"\",\n \"description\": \"手动填写端点和模型\"\n }\n]\n"}, "fec125addfcafe9c7c60a70464650278192cb8e5": {"t": 1785761237.7883666, "n": 1590, "h": "5fc1b12b62615b6e", "c": "[\n {\n \"id\": \"deepseek\",\n \"name\": \"DeepSeek\",\n \"apiBase\": \"https://api.deepseek.com/v1\",\n \"defaultModel\": \"deepseek-chat\",\n \"strongModel\": \"deepseek-reasoner\",\n \"figureModel\": \"deepseek-chat\",\n \"website\": \"https://platform.deepseek.com\",\n \"description\": \"性价比高,适合数学建模\"\n },\n {\n \"id\": \"openai\",\n \"name\": \"OpenAI\",\n \"apiBase\": \"https://api.openai.com/v1\",\n \"defaultModel\": \"openai/gpt-4o-mini\",\n \"strongModel\": \"openai/gpt-4o\",\n \"figureModel\": \"openai/gpt-4o\",\n \"website\": \"https://platform.openai.com\",\n \"description\": \"GPT 系列,能力全面\"\n },\n {\n \"id\": \"zhipu\",\n \"name\": \"智谱 AI\",\n \"apiBase\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"defaultModel\": \"glm-4-flash\",\n \"strongModel\": \"glm-4\",\n \"figureModel\": \"glm-4v\",\n \"website\": \"https://open.bigmodel.cn\",\n \"description\": \"GLM 系列,国产大模型\"\n },\n {\n \"id\": \"moonshot\",\n \"name\": \"月之暗面\",\n \"apiBase\": \"https://api.moonshot.cn/v1\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"strongModel\": \"moonshot-v1-32k\",\n \"figureModel\": \"moonshot-v1-32k\",\n \"website\": \"https://platform.moonshot.cn\",\n \"description\": \"Kimi,长上下文优势\"\n },\n {\n \"id\": \"ollama\",\n \"name\": \"本地 Ollama\",\n \"apiBase\": \"http://localhost:11434/v1\",\n \"defaultModel\": \"llama3\",\n \"strongModel\": \"llama3\",\n \"figureModel\": \"llama3\",\n \"website\": \"https://ollama.com\",\n \"description\": \"本地部署,离线可用\"\n },\n {\n \"id\": \"custom\",\n \"name\": \"自定义\",\n \"apiBase\": \"\",\n \"defaultModel\": \"\",\n \"strongModel\": \"\",\n \"figureModel\": \"\",\n \"website\": \"\",\n \"description\": \"手动填写端点和模型\"\n }\n]\n"}, "14cbc5c32e7b0d7e75343cf7d45dc90c934eb9f1": {"t": 1785761239.9334903, "n": 1399, "h": "d34b58591c3ed7c2", "c": "[\n {\n \"id\": \"deepseek\",\n \"name\": \"DeepSeek\",\n \"apiBase\": \"https://api.deepseek.com/v1\",\n \"defaultModel\": \"deepseek-chat\",\n \"strongModel\": \"deepseek-reasoner\",\n \"website\": \"https://platform.deepseek.com\",\n \"description\": \"性价比高,适合数学建模\"\n },\n {\n \"id\": \"openai\",\n \"name\": \"OpenAI\",\n \"apiBase\": \"https://api.openai.com/v1\",\n \"defaultModel\": \"openai/gpt-4o-mini\",\n \"strongModel\": \"openai/gpt-4o\",\n \"website\": \"https://platform.openai.com\",\n \"description\": \"GPT 系列,能力全面\"\n },\n {\n \"id\": \"zhipu\",\n \"name\": \"智谱 AI\",\n \"apiBase\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"defaultModel\": \"glm-4-flash\",\n \"strongModel\": \"glm-4\",\n \"website\": \"https://open.bigmodel.cn\",\n \"description\": \"GLM 系列,国产大模型\"\n },\n {\n \"id\": \"moonshot\",\n \"name\": \"月之暗面\",\n \"apiBase\": \"https://api.moonshot.cn/v1\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"strongModel\": \"moonshot-v1-32k\",\n \"website\": \"https://platform.moonshot.cn\",\n \"description\": \"Kimi,长上下文优势\"\n },\n {\n \"id\": \"ollama\",\n \"name\": \"本地 Ollama\",\n \"apiBase\": \"http://localhost:11434/v1\",\n \"defaultModel\": \"llama3\",\n \"strongModel\": \"llama3\",\n \"website\": \"https://ollama.com\",\n \"description\": \"本地部署,离线可用\"\n },\n {\n \"id\": \"custom\",\n \"name\": \"自定义\",\n \"apiBase\": \"\",\n \"defaultModel\": \"\",\n \"strongModel\": \"\",\n \"website\": \"\",\n \"description\": \"手动填写端点和模型\"\n }\n]\n"}}, "docs/superpowers/plans/2026-07-11-packaging-and-onboarding.md": {"4fc3ab1285422d3b317708a8542a91a8882eddab": {"t": 1785746579.271744, "n": 55718, "h": "2a2861eb9233b37a", "c": "# Beacon 打包与使用门槛降低 - 实现计划\n\n> **历史方案记录(非现行事实源)**:路径、状态、命令和待办保留当时上下文;当前实现与限制见 [`../../README.md`](../../README.md)。\n\n> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.\n\n**Goal:** 在现有 Beacon 架构上新增引导向导(5步)、设置页面、启动脚本增强,让用户无需手动编辑 .env 或安装命令行工具即可使用。\n\n**Architecture:** Node 服务器全权负责环境检测/winget 安装/.env 读写/LLM 连接测试。将 server.mjs 的共享工具提取到 `frontend/lib/shared.mjs`,新增路由模块挂载到 `/api/env/*`、`/api/config/*`、`/api/onboarding/*`。前端新增引导向导覆盖层和设置页面。Python 后端完全不改。\n\n**Tech Stack:** Node.js (原生 http)、vanilla HTML/CSS/JS、winget (Windows 包管理)、dotenv\n\n---\n\n## 文件结构\n\n| 文件 | 操作 | 职责 |\n|------|------|------|\n| `frontend/lib/shared.mjs` | 新建 | 共享工具:sendJson、readJsonBody、safeProjectPath、HttpError、projectRoot、env 解析 |\n| `frontend/providers.json` | 新建 | 6 个 API 提供商预设数据 |\n| `frontend/routes/env.mjs` | 新建 | 环境检测 + winget 安装 |\n| `frontend/routes/config.mjs` | 新建 | .env 读写 + LLM 连接测试 |\n| `frontend/routes/onboarding.mjs` | 新建 | 引导状态判断 |\n| `frontend/server.mjs` | 修改 | 路由分发 + health 增强 |\n| `frontend/server.test.mjs` | 修改 | 新增 API 测试 |\n| `frontend/index.html` | 修改 | 引导向导覆盖层 + 设置页入口 |\n| `frontend/styles.css` | 修改 | 引导/设置样式 |\n| `frontend/app.js` | 修改 | 引导/设置交互逻辑 |\n| `scripts/start.bat` | 修改 | 不阻断启动 + 友好提示 |\n\n---\n\n### Task 1: 创建共享工具模块\n\n**Files:**\n- Create: `frontend/lib/shared.mjs`\n\n- [ ] **Step 1: 创建 `frontend/lib/shared.mjs`**\n\n从 `server.mjs` 提取共享工具,供路由模块复用。\n\n```javascript\n// frontend/lib/shared.mjs\nimport { resolve } from \"node:path\";\nimport { isAbsolute, relative, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const root = resolve(fileURLToPath(new URL(\".\", import.meta.url)));\nexport const projectRoot = resolve(root, \"..\", \"..\");\nexport const env = globalThis.process?.env || {};\n\nexport class HttpError extends Error {\n constructor(status, message) {\n super(message);\n this.status = status;\n }\n}\n\nexport function sendJson(response, status, payload) {\n response.writeHead(status, { \"Content-Type\": \"application/json; charset=utf-8\" });\n response.end(JSON.stringify(payload));\n}\n\nexport function safeProjectPath(inputPath) {\n const target = resolve(projectRoot, inputPath || \".\");\n const rel = relative(projectRoot, target);\n if (isAbsolute(rel) || rel === \"..\" || rel.startsWith(`..${sep}`)) {\n throw new HttpError(403, \"Path is outside the project workspace.\");\n }\n return target;\n}\n\nexport async function readJsonBody(request) {\n const chunks = [];\n let size = 0;\n for await (const chunk of request) {\n size += chunk.length;\n if (size > 1024 * 1024) throw new HttpError(413, \"Request body exceeds 1 MiB.\");\n chunks.push(chunk);\n }\n const text = Buffer.concat(chunks).toString(\"utf8\");\n if (!text) return {};\n try {\n return JSON.parse(text);\n } catch (error) {\n throw new HttpError(400, `Invalid JSON body: ${error.message}`);\n }\n}\n```\n\n- [ ] **Step 2: 验证模块可被导入**\n\nRun: `node -e \"import('./frontend/lib/shared.mjs').then(m => console.log('OK', m.projectRoot))\"`\nExpected: 输出 `OK` + 项目根路径\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add frontend/lib/shared.mjs\ngit commit -m \"feat: extract shared utilities to lib/shared.mjs\"\n```\n\n---\n\n### Task 2: 创建 API 提供商预设数据\n\n**Files:**\n- Create: `frontend/providers.json`\n\n- [ ] **Step 1: 创建 `frontend/providers.json`**\n\n```json\n[\n {\n \"id\": \"deepseek\",\n \"name\": \"DeepSeek\",\n \"apiBase\": \"https://api.deepseek.com/v1\",\n \"defaultModel\": \"deepseek-chat\",\n \"strongModel\": \"deepseek-reasoner\",\n \"website\": \"https://platform.deepseek.com\",\n \"description\": \"性价比高,适合数学建模\"\n },\n {\n \"id\": \"openai\",\n \"name\": \"OpenAI\",\n \"apiBase\": \"https://api.openai.com/v1\",\n \"defaultModel\": \"openai/gpt-4o-mini\",\n \"strongModel\": \"openai/gpt-4o\",\n \"website\": \"https://platform.openai.com\",\n \"description\": \"GPT 系列,能力全面\"\n },\n {\n \"id\": \"zhipu\",\n \"name\": \"智谱 AI\",\n \"apiBase\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"defaultModel\": \"glm-4-flash\",\n \"strongModel\": \"glm-4\",\n \"website\": \"https://open.bigmodel.cn\",\n \"description\": \"GLM 系列,国产大模型\"\n },\n {\n \"id\": \"moonshot\",\n \"name\": \"月之暗面\",\n \"apiBase\": \"https://api.moonshot.cn/v1\",\n \"defaultModel\": \"moonshot-v1-8k\",\n \"strongModel\": \"moonshot-v1-32k\",\n \"website\": \"https://platform.moonshot.cn\",\n \"description\": \"Kimi,长上下文优势\"\n },\n {\n \"id\": \"ollama\",\n \"name\": \"本地 Ollama\",\n \"apiBase\": \"http://localhost:11434/v1\",\n \"defaultModel\": \"llama3\",\n \"strongModel\": \"llama3\",\n \"website\": \"https://ollama.com\",\n \"description\": \"本地部署,离线可用\"\n },\n {\n \"id\": \"custom\",\n \"name\": \"自定义\",\n \"apiBase\": \"\",\n \"defaultModel\": \"\",\n \"strongModel\": \"\",\n \"website\": \"\",\n \"description\": \"手动填写端点和模型\"\n }\n]\n```\n\n- [ ] **Step 2: Commit**\n\n```bash\ngit add frontend/providers.json\ngit commit -m \"feat: add API provider presets (providers.json)\"\n```\n\n---\n\n### Task 3: 创建环境检测路由模块\n\n**Files:**\n- Create: `frontend/routes/env.mjs`\n\n- [ ] **Step 1: 创建 `frontend/routes/env.mjs`**\n\n```javascript\n// frontend/routes/env.mjs\nimport { execSync } from \"node:child_process\";\nimport { sendJson, readJsonBody, HttpError } from \"../lib/shared.mjs\";\n\n/**\n * 检测单个工具的版本信息。\n * @param {string} command - 命令名 (如 \"python\", \"node\", \"uv\")\n * @returns {{ installed: boolean, version: string }}\n */\nfunction checkTool(command) {\n try {\n const output = execSync(`${command} --version`, {\n encoding: \"utf8\",\n timeout: 10_000,\n windowsHide: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n // 提取版本号:取输出中第一个 x.y.z 格式的字符串\n const match = output.match(/(\\d+\\.\\d+(?:\\.\\d+)?)/);\n return { installed: true, version: match ? match[1] : output };\n } catch {\n return { installed: false, version: \"\" };\n }\n}\n\n/**\n * 比较版本号是否达标。\n * @param {string} version - 实际版本 \"3.12.3\"\n * @param {string} minVersion - 最低要求 \"3.11\"\n * @returns {boolean}\n */\nfunction versionOk(version, minVersion) {\n if (!version) return false;\n const actual = version.split(\".\").map(Number);\n const required = minVersion.split(\".\").map(Number);\n for (let i = 0; i < required.length; i++) {\n const a = actual[i] || 0;\n const r = required[i] || 0;\n if (a > r) return true;\n if (a < r) return false;\n }\n return true;\n}\n\nconst WINGET_PACKAGES = {\n python: \"Python.Python.3.12\",\n node: \"OpenJS.NodeJS.LTS\",\n uv: \"astral-sh.uv\",\n};\n\n/**\n * 处理 /api/env/* 路由。\n */\nexport async function handleEnvRoutes(request, response, url) {\n // GET /api/env/check - 检测环境\n if (request.method === \"GET\" && url.pathname === \"/api/env/check\") {\n const python = checkTool(\"python\");\n const python3 = !python.installed ? checkTool(\"python3\") : null;\n const node = checkTool(\"node\");\n const uv = checkTool(\"uv\");\n\n const pythonResult = python.installed\n ? python\n : python3 && python3.installed\n ? python3\n : { installed: false, version: \"\" };\n\n sendJson(response, 200, {\n python: {\n installed: pythonResult.installed,\n version: pythonResult.version,\n ok: pythonResult.installed && versionOk(pythonResult.version, \"3.11\"),\n },\n node: {\n installed: node.installed,\n version: node.version,\n ok: node.installed && versionOk(node.version, \"18\"),\n },\n uv: {\n installed: uv.installed,\n version: uv.version,\n ok: uv.installed,\n },\n allOk:\n pythonResult.installed &&\n versionOk(pythonResult.version, \"3.11\") &&\n node.installed &&\n versionOk(node.version, \"18\") &&\n uv.installed,\n });\n return;\n }\n\n // POST /api/env/install - winget 安装\n if (request.method === \"POST\" && url.pathname === \"/api/env/install\") {\n const body = await readJsonBody(request);\n const tool = body.tool;\n if (!tool || !WINGET_PACKAGES[tool]) {\n throw new HttpError(400, `Invalid tool. Must be one of: ${Object.keys(WINGET_PACKAGES).join(\", \")}`);\n }\n\n // 检查 winget 是否可用\n try {\n execSync(\"winget --version\", { encoding: \"utf8\", timeout: 10_000, windowsHide: true, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n } catch {\n sendJson(response, 200, {\n tool,\n status: \"failed\",\n message: \"winget 不可用,请手动安装\",\n downloadUrl: getDownloadUrl(tool),\n });\n return;\n }\n\n // 阻塞式安装\n const pkg = WINGET_PACKAGES[tool];\n try {\n const output = execSync(\n `winget install ${pkg} --silent --accept-package-agreements --accept-source-agreements`,\n {\n encoding: \"utf8\",\n timeout: 300_000, // 5 分钟\n windowsHide: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n },\n );\n sendJson(response, 200, {\n tool,\n status: \"done\",\n message: \"安装完成\",\n output: output.slice(-500),\n });\n } catch (error) {\n sendJson(response, 200, {\n tool,\n status: \"failed\",\n message: error.killed ? \"安装超时(超过5分钟)\" : \"安装失败\",\n output: String(error.stderr || error.stdout || error.message).slice(-500),\n downloadUrl: getDownloadUrl(tool),\n });\n }\n return;\n }\n\n // GET /api/providers - 返回提供商预设\n if (request.method === \"GET\" && url.pathname === \"/api/providers\") {\n const { readFile } = await import(\"node:fs/promises\");\n const { resolve } = await import(\"node:path\");\n const { projectRoot } = await import(\"../lib/shared.mjs\");\n try {\n const raw = await readFile(resolve(projectRoot, \"frontend/providers.json\"), \"utf8\");\n sendJson(response, 200, JSON.parse(raw));\n } catch {\n sendJson(response, 200, []);\n }\n return;\n }\n\n sendJson(response, 404, { error: \"Unknown env route.\" });\n}\n\nfunction getDownloadUrl(tool) {\n const urls = {\n python: \"https://www.python.org/downloads/\",\n node: \"https://nodejs.org/\",\n uv: \"https://docs.astral.sh/uv/getting-started/installation/\",\n };\n return urls[tool] || \"\";\n}\n```\n\n- [ ] **Step 2: Commit**\n\n```bash\ngit add frontend/routes/env.mjs\ngit commit -m \"feat: add env check + winget install route module\"\n```\n\n---\n\n### Task 4: 创建配置读写路由模块\n\n**Files:**\n- Create: `frontend/routes/config.mjs`\n\n- [ ] **Step 1: 创建 `frontend/routes/config.mjs`**\n\n```javascript\n// frontend/routes/config.mjs\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { sendJson, readJsonBody, HttpError, projectRoot } from \"../lib/shared.mjs\";\n\nconst ENV_PATH = resolve(projectRoot, \".env\");\nconst ENV_EXAMPLE_PATH = resolve(projectRoot, \".env.example\");\n\n/** 前端字段 -> .env 变量名映射 */\nconst FIELD_MAP = {\n apiBase: \"OPENAI_API_BASE\",\n apiKey: \"OPENAI_API_KEY\",\n defaultModel: \"MATH_AGENT_DEFAULT_MODEL\",\n strongModel: \"MATH_AGENT_STRONG_MODEL\",\n llmTimeout: \"MATH_AGENT_LLM_TIMEOUT\",\n maxModelIterations: \"MATH_AGENT_MAX_MODEL_ITERATIONS\",\n ragEnabled: \"MATH_AGENT_RAG_ENABLED\",\n ragEmbeddingModel: \"MATH_AGENT_RAG_EMBED\",\n ragTopK: \"MATH_AGENT_RAG_TOPK\",\n port: \"PORT\",\n};\n\n/** .env 变量名 -> 前端字段 反向映射 */\nconst ENV_TO_FIELD = Object.fromEntries(\n Object.entries(FIELD_MAP).map(([field, envKey]) => [envKey, field]),\n);\n\n/**\n * 解析 .env 文件为 key-value 对象。\n */\nfunction parseEnvFile(filePath) {\n if (!existsSync(filePath)) return {};\n const content = readFileSync(filePath, \"utf8\");\n const result = {};\n for (const line of content.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n const key = trimmed.slice(0, eqIndex).trim();\n const value = trimmed.slice(eqIndex + 1).trim();\n result[key] = value;\n }\n return result;\n}\n\n/**\n * 掩码 API 密钥:显示前3位 + *** + 后4位。\n */\nfunction maskApiKey(key) {\n if (!key || key.length < 8) return key ? \"***\" : \"\";\n return `${key.slice(0, 3)}***${key.slice(-4)}`;\n}\n\n/**\n * 更新 .env 文件,保留注释和格式,仅替换对应行的值。\n */\nfunction updateEnvFile(updates) {\n const templatePath = existsSync(ENV_PATH) ? ENV_PATH : ENV_EXAMPLE_PATH;\n const content = readFileSync(templatePath, \"utf8\");\n const lines = content.split(\"\\n\");\n const remaining = new Set(Object.keys(updates));\n\n const result = lines.map((line) => {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) return line;\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) return line;\n const key = trimmed.slice(0, eqIndex).trim();\n if (key in updates) {\n remaining.delete(key);\n return `${key}=${updates[key]}`;\n }\n return line;\n });\n\n // 追加 .env 中不存在的新 key\n for (const key of remaining) {\n result.push(`${key}=${updates[key]}`);\n }\n\n writeFileSync(ENV_PATH, result.join(\"\\n\"), \"utf8\");\n}\n\n/**\n * 处理 /api/config* 路由。\n */\nexport async function handleConfigRoutes(request, response, url) {\n // GET /api/config - 读取当前配置\n if (request.method === \"GET\" && url.pathname === \"/api/config\") {\n const envVars = parseEnvFile(ENV_PATH);\n const config = {\n apiBase: envVars.OPENAI_API_BASE || \"\",\n apiKey: maskApiKey(envVars.OPENAI_API_KEY || \"\"),\n hasApiKey: !!(envVars.OPENAI_API_KEY && envVars.OPENAI_API_KEY !== \"123456\"),\n defaultModel: envVars.MATH_AGENT_DEFAULT_MODEL || \"\",\n strongModel: envVars.MATH_AGENT_STRONG_MODEL || \"\",\n llmTimeout: Number(envVars.MATH_AGENT_LLM_TIMEOUT || 300),\n maxModelIterations: Number(envVars.MATH_AGENT_MAX_MODEL_ITERATIONS || 3),\n ragEnabled: envVars.MATH_AGENT_RAG_ENABLED === \"1\",\n ragEmbeddingModel: envVars.MATH_AGENT_RAG_EMBED || \"text-embedding-3-small\",\n ragTopK: Number(envVars.MATH_AGENT_RAG_TOPK || 4),\n port: Number(envVars.PORT || 5173),\n };\n sendJson(response, 200, config);\n return;\n }\n\n // POST /api/config - 保存配置到 .env\n if (request.method === \"POST\" && url.pathname === \"/api/config\") {\n const body = await readJsonBody(request);\n\n const updates = {};\n // 只更新提供的字段\n if (body.apiBase !== undefined) updates[FIELD_MAP.apiBase] = body.apiBase;\n // apiKey: 如果值包含 *** 表示是掩码值,跳过不更新\n if (body.apiKey !== undefined && !body.apiKey.includes(\"***\")) {\n updates[FIELD_MAP.apiKey] = body.apiKey;\n }\n if (body.defaultModel !== undefined) updates[FIELD_MAP.defaultModel] = body.defaultModel;\n if (body.strongModel !== undefined) updates[FIELD_MAP.strongModel] = body.strongModel;\n if (body.llmTimeout !== undefined) updates[FIELD_MAP.llmTimeout] = String(body.llmTimeout);\n if (body.maxModelIterations !== undefined) updates[FIELD_MAP.maxModelIterations] = String(body.maxModelIterations);\n if (body.ragEnabled !== undefined) updates[FIELD_MAP.ragEnabled] = body.ragEnabled ? \"1\" : \"0\";\n if (body.ragEmbeddingModel !== undefined) updates[FIELD_MAP.ragEmbeddingModel] = body.ragEmbeddingModel;\n if (body.ragTopK !== undefined) updates[FIELD_MAP.ragTopK] = String(body.ragTopK);\n if (body.port !== undefined) updates[FIELD_MAP.port] = String(body.port);\n\n if (Object.keys(updates).length === 0) {\n throw new HttpError(400, \"No config fields to update.\");\n }\n\n updateEnvFile(updates);\n sendJson(response, 200, { ok: true });\n return;\n }\n\n // POST /api/config/test-llm - 测试 LLM 连接\n if (request.method === \"POST\" && url.pathname === \"/api/config/test-llm\") {\n const body = await readJsonBody(request);\n const { apiBase, apiKey, model } = body;\n\n if (!apiBase || !apiKey || !model) {\n throw new HttpError(400, \"apiBase, apiKey, and model are required.\");\n }\n\n const startTime = Date.now();\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 30_000);\n\n const res = await fetch(`${apiBase.replace(/\\/+$/, \"\")}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"user\", content: \"hi\" }],\n max_tokens: 1,\n }),\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n const latency = Date.now() - startTime;\n\n if (res.ok) {\n sendJson(response, 200, { success: true, latency_ms: latency });\n } else {\n const errorText = await res.text().catch(() => \"\");\n let errorMsg = `HTTP ${res.status}`;\n if (res.status === 401) errorMsg = \"API 密钥无效\";\n else if (res.status === 404) errorMsg = \"模型不存在或端点错误\";\n else if (errorText) errorMsg = `${errorMsg}: ${errorText.slice(0, 200)}`;\n sendJson(response, 200, { success: false, latency_ms: latency, error: errorMsg });\n }\n } catch (error) {\n const latency = Date.now() - startTime;\n const errorMsg = error.name === \"AbortError\"\n ? \"连接超时(30秒)\"\n : `连接失败: ${error.message}`;\n sendJson(response, 200, { success: false, latency_ms: latency, error: errorMsg });\n }\n return;\n }\n\n sendJson(response, 404, { error: \"Unknown config route.\" });\n}\n```\n\n- [ ] **Step 2: Commit**\n\n```bash\ngit add frontend/routes/config.mjs\ngit commit -m \"feat: add config read/write + LLM test route module\"\n```\n\n---\n\n### Task 5: 创建引导状态路由模块\n\n**Files:**\n- Create: `frontend/routes/onboarding.mjs`\n\n- [ ] **Step 1: 创建 `frontend/routes/onboarding.mjs`**\n\n```javascript\n// frontend/routes/onboarding.mjs\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { sendJson, projectRoot } from \"../lib/shared.mjs\";\n\nconst ENV_PATH = resolve(projectRoot, \".env\");\n\n/**\n * 判断是否需要引导。\n * @returns {{ needed: boolean, reason: \"no_env\" | \"no_api_key\" | \"done\" }}\n */\nexport function getOnboardingStatus() {\n if (!existsSync(ENV_PATH)) {\n return { needed: true, reason: \"no_env\" };\n }\n\n const content = readFileSync(ENV_PATH, \"utf8\");\n const apiKeyMatch = content.match(/^OPENAI_API_KEY\\s*=\\s*(.+)$/m);\n const apiKey = apiKeyMatch ? apiKeyMatch[1].trim() : \"\";\n\n if (!apiKey || apiKey === \"123456\") {\n return { needed: true, reason: \"no_api_key\" };\n }\n\n return { needed: false, reason: \"done\" };\n}\n\n/**\n * 处理 /api/onboarding/* 路由。\n */\nexport async function handleOnboardingRoutes(request, response, url) {\n // GET /api/onboarding/status\n if (request.method === \"GET\" && url.pathname === \"/api/onboarding/status\") {\n sendJson(response, 200, getOnboardingStatus());\n return;\n }\n\n sendJson(response, 404, { error: \"Unknown onboarding route.\" });\n}\n```\n\n- [ ] **Step 2: Commit**\n\n```bash\ngit add frontend/routes/onboarding.mjs\ngit commit -m \"feat: add onboarding status route module\"\n```\n\n---\n\n### Task 6: 重构 server.mjs 接入新路由\n\n**Files:**\n- Modify: `frontend/server.mjs`\n\n- [ ] **Step 1: 在 server.mjs 顶部添加路由导入**\n\n在 `frontend/server.mjs` 第 9 行(`import { fileURLToPath }` 之后)添加:\n\n```javascript\nimport { handleEnvRoutes } from \"./routes/env.mjs\";\nimport { handleConfigRoutes } from \"./routes/config.mjs\";\nimport { handleOnboardingRoutes, getOnboardingStatus } from \"./routes/onboarding.mjs\";\n```\n\n- [ ] **Step 2: 在 handleApi 函数中添加新路由分发**\n\n在 `frontend/server.mjs` 的 `handleApi` 函数开头(第 249 行 `if (request.method === \"POST\" && url.pathname === \"/api/upload\")` 之前)插入:\n\n```javascript\n // --- 新增路由:环境检测、配置、引导 ---\n if (url.pathname.startsWith(\"/api/env/\") || url.pathname === \"/api/providers\") {\n return handleEnvRoutes(request, response, url);\n }\n if (url.pathname.startsWith(\"/api/config\")) {\n return handleConfigRoutes(request, response, url);\n }\n if (url.pathname.startsWith(\"/api/onboarding/\")) {\n return handleOnboardingRoutes(request, response, url);\n }\n```\n\n- [ ] **Step 3: 增强 health 端点,合并 onboarding 状态**\n\n在 `frontend/server.mjs` 中找到 health 端点(约第 313 行),替换为:\n\n```javascript\n if (request.method === \"GET\" && url.pathname === \"/api/health\") {\n const fixtures = await listFixtures().catch(() => []);\n const onboarding = getOnboardingStatus();\n sendJson(response, 200, {\n ok: true,\n projectRoot,\n mathAgentCommand: env.MATH_AGENT_COMMAND || \"uv run math-agent\",\n fixtures: fixtures.length,\n onboarding,\n });\n return;\n }\n```\n\n- [ ] **Step 4: 验证服务器能启动**\n\nRun: `node -e \"import('./frontend/lib/shared.mjs').then(() => console.log('shared OK'))\" && node -e \"import('./frontend/routes/env.mjs').then(() => console.log('env OK'))\" && node -e \"import('./frontend/routes/config.mjs').then(() => console.log('config OK'))\" && node -e \"import('./frontend/routes/onboarding.mjs').then(() => console.log('onboarding OK'))\"`\nExpected: 四行 `OK`\n\n- [ ] **Step 5: Commit**\n\n```bash\ngit add frontend/server.mjs\ngit commit -m \"feat: wire env/config/onboarding routes into server.mjs + enhance health\"\n```\n\n---\n\n### Task 7: 编写后端 API 集成测试\n\n**Files:**\n- Modify: `frontend/server.test.mjs`\n\n- [ ] **Step 1: 在 `frontend/server.test.mjs` 末尾追加新测试**\n\n在文件末尾(第 172 行之后)追加:\n\n```javascript\ntest(\"GET /api/env/check 返回环境检测结果\", async () => {\n const response = await fetch(`${base}/api/env/check`);\n assert.equal(response.status, 200);\n const data = await response.json();\n assert.ok(data.python);\n assert.ok(data.node);\n assert.ok(data.uv);\n assert.equal(typeof data.allOk, \"boolean\");\n});\n\ntest(\"GET /api/onboarding/status 返回引导状态\", async () => {\n const response = await fetch(`${base}/api/onboarding/status`);\n assert.equal(response.status, 200);\n const data = await response.json();\n assert.ok([\"no_env\", \"no_api_key\", \"done\"].includes(data.reason));\n assert.equal(typeof data.needed, \"boolean\");\n});\n\ntest(\"GET /api/config 返回配置(密钥掩码)\", async () => {\n const response = await fetch(`${base}/api/config`);\n assert.equal(response.status, 200);\n const data = await response.json();\n assert.ok(data.apiBase !== undefined);\n assert.ok(data.defaultModel !== undefined);\n assert.ok(data.strongModel !== undefined);\n // apiKey 不应包含明文(如果存在的话应被掩码)\n if (data.apiKey) {\n assert.ok(data.apiKey.includes(\"***\") || data.apiKey.length === 0);\n }\n});\n\ntest(\"GET /api/health 包含 onboarding 字段\", async () => {\n const response = await fetch(`${base}/api/health`);\n const data = await response.json();\n assert.ok(data.onboarding);\n assert.ok([\"no_env\", \"no_api_key\", \"done\"].includes(data.onboarding.reason));\n});\n\ntest(\"POST /api/config/test-llm 拒绝缺少参数的请求\", async () => {\n const response = await fetch(`${base}/api/config/test-llm`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ apiBase: \"https://example.com/v1\" }),\n });\n assert.equal(response.status, 400);\n});\n\ntest(\"GET /api/providers 返回提供商列表\", async () => {\n const response = await fetch(`${base}/api/providers`);\n assert.equal(response.status, 200);\n const data = await response.json();\n assert.ok(Array.isArray(data));\n assert.ok(data.length >= 5);\n assert.ok(data.some((p) => p.id === \"deepseek\"));\n});\n```\n\n- [ ] **Step 2: 运行测试**\n\nRun: `node --test frontend/server.test.mjs`\nExpected: 所有测试 PASS(包括原有测试)\n\n- [ ] **Step 3: Commit**\n\n```bash\ngit add frontend/server.test.mjs\ngit commit -m \"test: add integration tests for env/config/onboarding APIs\"\n```\n\n---\n\n### Task 8: 添加引导向导和设置页面的 HTML\n\n**Files:**\n- Modify: `frontend/index.html`\n\n- [ ] **Step 1: 在侧边栏导航添加\"设置\"入口**\n\n在 `frontend/index.html` 第 23 行(`知识库` 之后)添加:\n\n```html\n 设置\n```\n\n- [ ] **Step 2: 在 `` 之前添加设置页面容器**\n\n在 `frontend/index.html` 找到 ``(第 230 行),在其之前添加:\n\n```html\n Configuration设置
\n
让我们花几分钟完成初始配置
\n正在检测运行所需依赖...
\n \n `;\n try {\n const data = await api(\"/api/env/check\");\n const list = document.querySelector(\"#envCheckList\");\n list.innerHTML = renderEnvItem(\"Python\", \"≥ 3.11\", data.python) +\n renderEnvItem(\"Node.js\", \"≥ 18\", data.node) +\n renderEnvItem(\"uv\", \"Python 包管理器\", data.uv);\n onboardingNext.disabled = !data.allOk;\n if (!data.allOk) {\n list.querySelectorAll(\".env-install-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => installTool(btn.dataset.tool, btn));\n });\n }\n } catch (error) {\n onboardingBody.innerHTML = `检测失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderEnvItem(name, requirement, info) {\n const ok = info.ok;\n const status = ok\n ? `✓ ${info.version}`\n : `✗ ${info.installed ? info.version + \" 版本过低\" : \"未检测到\"}`;\n const action = ok ? \"\" : ``;\n // 修正 tool 名称映射\n const toolName = name === \"Python\" ? \"python\" : name === \"Node.js\" ? \"node\" : name === \"uv\" ? \"uv\" : \"\";\n const actionBtn = ok ? \"\" : ``;\n return `\n手动安装: ${result.downloadUrl}
`);\n }\n }\n } catch (error) {\n btn.disabled = false;\n btn.classList.remove(\"installing\");\n btn.textContent = \"重试\";\n showToast(`安装失败: ${error.message}`);\n }\n}\n\nasync function renderProviderStep() {\n onboardingNext.disabled = true;\n onboardingBody.innerHTML = `\n选择你的 LLM 服务提供商,选择后会自动填入端点和推荐模型。
\n \n `;\n try {\n const providers = await api(\"/api/providers\");\n const grid = document.querySelector(\"#providerGrid\");\n grid.innerHTML = providers.map((p) => `\n加载提供商列表失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderApiKeyStep() {\n onboardingBody.innerHTML = `\n填写你的 API 密钥。端点已根据所选提供商自动填入,可修改。
\n选择主力模型和强力模型,然后测试连接。
\n测试中...
';\n try {\n const [r1, r2] = await Promise.all([\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.defaultModel),\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.strongModel),\n ]);\n const html = renderTestResult(\"主力模型\", r1) + renderTestResult(\"强力模型\", r2);\n result.innerHTML = html;\n testOk = r1.success && r2.success;\n onboardingNext.disabled = !testOk;\n } catch (error) {\n result.innerHTML = `配置已自动保存到 .env 文件
\n端点: ${escapeHtml(onboardingData.apiBase)}
\n主力模型: ${escapeHtml(onboardingData.defaultModel)}
\n强力模型: ${escapeHtml(onboardingData.strongModel)}
\n加载配置失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderSettings() {\n if (!settingsConfig) return;\n if (settingsTab === \"api\") renderSettingsApi();\n else if (settingsTab === \"models\") renderSettingsModels();\n else if (settingsTab === \"rag\") renderSettingsRag();\n else if (settingsTab === \"advanced\") renderSettingsAdvanced();\n}\n\nfunction renderSettingsApi() {\n settingsContent.innerHTML = `\n测试中...
';\n try {\n const r = await testLlm(apiBase, apiKey.includes(\"***\") ? \"\" : apiKey, model);\n result.innerHTML = r.success\n ? `启用后注入经典模型模式到提示词
\nConfiguration
\n让我们花几分钟完成初始配置
\n正在检测运行所需依赖...
\n \n `;\n try {\n const data = await api(\"/api/env/check\");\n const list = document.querySelector(\"#envCheckList\");\n list.innerHTML = renderEnvItem(\"Python\", \"≥ 3.11\", data.python) +\n renderEnvItem(\"Node.js\", \"≥ 18\", data.node) +\n renderEnvItem(\"uv\", \"Python 包管理器\", data.uv);\n onboardingNext.disabled = !data.allOk;\n if (!data.allOk) {\n list.querySelectorAll(\".env-install-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => installTool(btn.dataset.tool, btn));\n });\n }\n } catch (error) {\n onboardingBody.innerHTML = `检测失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderEnvItem(name, requirement, info) {\n const ok = info.ok;\n const status = ok\n ? `✓ ${info.version}`\n : `✗ ${info.installed ? info.version + \" 版本过低\" : \"未检测到\"}`;\n const action = ok ? \"\" : ``;\n // 修正 tool 名称映射\n const toolName = name === \"Python\" ? \"python\" : name === \"Node.js\" ? \"node\" : name === \"uv\" ? \"uv\" : \"\";\n const actionBtn = ok ? \"\" : ``;\n return `\n手动安装: ${result.downloadUrl}
`);\n }\n }\n } catch (error) {\n btn.disabled = false;\n btn.classList.remove(\"installing\");\n btn.textContent = \"重试\";\n showToast(`安装失败: ${error.message}`);\n }\n}\n\nasync function renderProviderStep() {\n onboardingNext.disabled = true;\n onboardingBody.innerHTML = `\n选择你的 LLM 服务提供商,选择后会自动填入端点和推荐模型。
\n \n `;\n try {\n const providers = await api(\"/api/providers\");\n const grid = document.querySelector(\"#providerGrid\");\n grid.innerHTML = providers.map((p) => `\n加载提供商列表失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderApiKeyStep() {\n onboardingBody.innerHTML = `\n填写你的 API 密钥。端点已根据所选提供商自动填入,可修改。
\n选择主力模型和强力模型,然后测试连接。
\n测试中...
';\n try {\n const [r1, r2] = await Promise.all([\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.defaultModel),\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.strongModel),\n ]);\n const html = renderTestResult(\"主力模型\", r1) + renderTestResult(\"强力模型\", r2);\n result.innerHTML = html;\n testOk = r1.success && r2.success;\n onboardingNext.disabled = !testOk;\n } catch (error) {\n result.innerHTML = `配置已自动保存到 .env 文件
\n端点: ${escapeHtml(onboardingData.apiBase)}
\n主力模型: ${escapeHtml(onboardingData.defaultModel)}
\n强力模型: ${escapeHtml(onboardingData.strongModel)}
\n加载配置失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderSettings() {\n if (!settingsConfig) return;\n if (settingsTab === \"api\") renderSettingsApi();\n else if (settingsTab === \"models\") renderSettingsModels();\n else if (settingsTab === \"rag\") renderSettingsRag();\n else if (settingsTab === \"advanced\") renderSettingsAdvanced();\n}\n\nfunction renderSettingsApi() {\n settingsContent.innerHTML = `\n测试中...
';\n try {\n const r = await testLlm(apiBase, apiKey.includes(\"***\") ? \"\" : apiKey, model);\n result.innerHTML = r.success\n ? `启用后注入经典模型模式到提示词
\nConfiguration
\n让我们花几分钟完成初始配置
\n正在检测运行所需依赖...
\n \n `;\n try {\n const data = await api(\"/api/env/check\");\n const list = document.querySelector(\"#envCheckList\");\n list.innerHTML = renderEnvItem(\"Python\", \"≥ 3.11\", data.python) +\n renderEnvItem(\"Node.js\", \"≥ 18\", data.node) +\n renderEnvItem(\"uv\", \"Python 包管理器\", data.uv);\n onboardingNext.disabled = !data.allOk;\n if (!data.allOk) {\n list.querySelectorAll(\".env-install-btn\").forEach((btn) => {\n btn.addEventListener(\"click\", () => installTool(btn.dataset.tool, btn));\n });\n }\n } catch (error) {\n onboardingBody.innerHTML = `检测失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderEnvItem(name, requirement, info) {\n const ok = info.ok;\n const status = ok\n ? `✓ ${info.version}`\n : `✗ ${info.installed ? info.version + \" 版本过低\" : \"未检测到\"}`;\n const action = ok ? \"\" : ``;\n // 修正 tool 名称映射\n const toolName = name === \"Python\" ? \"python\" : name === \"Node.js\" ? \"node\" : name === \"uv\" ? \"uv\" : \"\";\n const actionBtn = ok ? \"\" : ``;\n return `\n手动安装: ${result.downloadUrl}
`);\n }\n }\n } catch (error) {\n btn.disabled = false;\n btn.classList.remove(\"installing\");\n btn.textContent = \"重试\";\n showToast(`安装失败: ${error.message}`);\n }\n}\n\nasync function renderProviderStep() {\n onboardingNext.disabled = true;\n onboardingBody.innerHTML = `\n选择你的 LLM 服务提供商,选择后会自动填入端点和推荐模型。
\n \n `;\n try {\n const providers = await api(\"/api/providers\");\n const grid = document.querySelector(\"#providerGrid\");\n grid.innerHTML = providers.map((p) => `\n加载提供商列表失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderApiKeyStep() {\n onboardingBody.innerHTML = `\n填写你的 API 密钥。端点已根据所选提供商自动填入,可修改。
\n选择主力模型和强力模型,然后测试连接。
\n测试中...
';\n try {\n const [r1, r2] = await Promise.all([\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.defaultModel),\n testLlm(onboardingData.apiBase, onboardingData.apiKey, onboardingData.strongModel),\n ]);\n const html = renderTestResult(\"主力模型\", r1) + renderTestResult(\"强力模型\", r2);\n result.innerHTML = html;\n testOk = r1.success && r2.success;\n onboardingNext.disabled = !testOk;\n } catch (error) {\n result.innerHTML = `配置已自动保存到 .env 文件
\n端点: ${escapeHtml(onboardingData.apiBase)}
\n主力模型: ${escapeHtml(onboardingData.defaultModel)}
\n强力模型: ${escapeHtml(onboardingData.strongModel)}
\n加载配置失败: ${escapeHtml(error.message)}
`;\n }\n}\n\nfunction renderSettings() {\n if (!settingsConfig) return;\n if (settingsTab === \"api\") renderSettingsApi();\n else if (settingsTab === \"models\") renderSettingsModels();\n else if (settingsTab === \"rag\") renderSettingsRag();\n else if (settingsTab === \"advanced\") renderSettingsAdvanced();\n}\n\nfunction renderSettingsApi() {\n settingsContent.innerHTML = `\n测试中...
';\n try {\n const r = await testLlm(apiBase, apiKey.includes(\"***\") ? \"\" : apiKey, model);\n result.innerHTML = r.success\n ? `启用后注入经典模型模式到提示词
\n