# 07 · 修复清单(Fix Log) > 本文档记录 `dsh-free-models-hub` 已知问题、根因分析与修复结果,按「严重度 / 修复顺序」排列。 > 适用于维护者复现、验证与回归。最后更新日期见文末。 --- ## 问题定位总览 | # | 问题 | 严重度 | 状态 | 涉及模块 | |---|------|--------|------|----------| | F1 | 「一键配置到 DSH」/「配置本页全部」写入后 DSH 提供方被**清空** / 不生效 | 致命 | ✅ 已修复 | `src/client/index.js`(settingsScope 用法) | | F2 | 手写 YAML 解析/序列化导致 `settings.yaml` 损坏(`BAD_INDENT`/`UNEXPECTED_TOKEN`),DSH 启动崩溃 | 致命 | ✅ 已修复(弃用) | `src/index.js`(已删除全部 YAML 读写) | | F3 | 代理转发请求时 `content-length`/`transfer-encoding` 处理不当,上游可能挂起或 `502` | 高 | ✅ 已修复 | `src/index.js`(改为读满 body 后带正确长度转发) | | F4 | `writePoolFile` 可能在异常/空池时**误清空**已有的 keypools 文件 | 高 | ✅ 已修复 | `src/index.js`(写入前校验非空保护) | | F5 | 多 Key 轮换提供方在 DSH 凭据检查处报「no credential」 | 中 | ✅ 已修复 | `src/index.js`(启动时为池内提供方设置 dummy env 变量) | | F6 | 面板用户缺少**首次使用引导**,不知道要「设置 → 模型」填 Key | 中 | ✅ 已新增 | `src/client/index.js`(教程 UI) | | F7 | 单 Key 提供方「未写入 keypools 文件」易被误解为 bug(实为设计) | 低 | 📖 已说明 | 见 F7「说明」一节 | --- ## F1 · 提供方写入被清空 / 不生效(致命) ### 现象 - 用户在面板点「⚡ 一键配置到 DSH」或「⚡ 配置本页全部」后, 到 `设置 → 模型` 看不到新增提供方;或原有的多个提供方被**全部清空**,只留下刚写的一个。 ### 根因 - 客户端读取提供方列表时,**没有等 `settingsScope` 的异步 `load()` 完成**就读取 `bound.value`。 初始状态永远是 `{}`,于是: - `readProviders()` 返回空对象; - 后续 `bound.set('providers', {})` 把原有提供方整体覆盖成空 → 灾难性清空。 - `getBoundNamespace('llm-pi-ai')` 内部还存在 `bound.load()` 引用在 `bound` 声明之前的 bug。 ### 修复方案 1. `readProviders()` 改为 `async`: ```js const bound = await getBoundNamespace('llm-pi-ai') await bound.load() // 必须等待异步加载完成 return bound.getSnapshot().value.providers || {} // value 是整个 llm-pi-ai 命名空间对象 ``` 2. 修正 `getBoundNamespace` 中 `bound` 未声明即引用的顺序问题。 3. 所有读取方统一走新的 `async readProviders(bound)`,完成后 `bound.set('providers', providers)` 整体替换 `providers` 字段(这是期望的「替换全量提供方」语义,前提是拿到的是正确的合并副本)。 4. 受影响函数全部重写为异步:`applyProvider`、`writeProviderEnsure`、`batchApplyAll`、 `saveKeyPool`、`disableKeyPool`、启动时 baseURL 修正。 ### 验证 - `npm run build` 通过;`npm test` 33/33 通过。 - 部署到真机后一键配置新增提供方成功,DSH `settings.yaml` 中已有 5 个提供方并存, 未再出现清空(见 F7 的 `settings.yaml` 现状)。 --- ## F2 · 手写 YAML 破坏 settings.yaml(致命,已弃用) ### 现象 - 编辑 `D:\dsharness\dsh-home\settings.yaml` 后,DSH 解析报 `BAD_INDENT` / `UNEXPECTED_TOKEN`, DSH 启动失败。 ### 根因 - 主机进程用自研字符串 YAML 解析/序列化拼参数,未严格遵循 DSH 的 `providers:\n {`(换行 + 花括号展开 + 正确缩进)写法,写成非法 YAML。 ### 修复方案 - **完全移除** `src/index.js` 中所有 YAML 读写代码(约 202 行)。 - 删除旧接口 `/p/read-providers`、`/p/save-provider`、`/p/save-providers-batch`。 - 提供方读写**全部走 DSH 官方 `settingsScope`**(`llm-pi-ai` 命名空间),由 DSH 自行持久化, 插件不再直接碰 `settings.yaml`。 - `settings.yaml` 已手工恢复为 DSH 合法格式(`js-yaml` 校验通过,5 个提供方并存)。 ### 铁律 > **绝不直接编辑 `settings.yaml`**。所有提供方变更一律走 `settingsScope`。 --- ## F3 · 代理转发请求体 / 长度头处理(高) ### 现象 - 通过本地代理(多 Key 轮换)请求时,上游偶发挂起、或返回 `502 upstream error`。 ### 根因 - 原实现 `req.pipe(proxyReq)` 把 `content-length` / `transfer-encoding` 原样透传, 但读取完整个流后才校准长度,可能造成长度不一致或分块编码混乱,上游无法正确解析 body。 ### 修复方案 - 改为:先**完整收集请求体**(`data`/`end`),在 `end` 时用 `Buffer.concat(chunks)`, 显式设置 `opts.headers['content-length'] = body.length` 并向 `transfer-encoding` 删除, 再 `proxyReq.write(body)` + `proxyReq.end()`。 - 移除上游响应的 `connection` 头,避免 keep-alive 冲突。 - 增加请求/转发日志便于排查。 ### 验证 - 代理 `/p/ping` 正常;`/p/save-pools` 返回 `{"ok":true}`。 --- ## F4 · 空池误清空 keypools 文件(高) ### 现象 - 某些重启/回退场景下 `dsh-free-models-hub-keypools.json` 被写成空对象,丢失已有 Key 池。 ### 根因 - `writePoolFile(pools, targets)` 直接以调用方传入内容覆盖全文件, 而调用方在「异常写入路径」可能传入空池。 ### 修复方案 - `writePoolFile` 增加保护:若本次要写入的 `pools` 为空,而磁盘上已有非空 `keyPools`, 则拒绝覆盖并日志告警,保留既有池。 --- ## F5 · 多 Key 提供方报「no credential」(中) ### 现象 - 把提供方 baseURL 切到本地代理后,DSH 校验该提供方凭据时报「no credential」,无法使用。 ### 根因 - DSH 要求提供方有对应 `apiKeyEnv` 环境变量(或已存 Key)才会放行,而池内 Key 只存在于代理进程内部,没有对应环境变量。 ### 修复方案 - 主机进程 `readPoolFile()` 启动时为**每个池内提供方**设置 dummy 环境变量 `_API_KEY=dsh-proxy-pool`,让 DSH 凭据检查通过; - 同时以 **User 级**设置了 `FREEHUB_MODEL_4_API_KEY=dsh-proxy-pool`。 - 代理拿到请求后,用池内真实 Key 做轮询替换 `Authorization`。 --- ## F6 · 新增:首次使用引导(教程 UI) ### 内容 - 在面板顶部(数据源配置下方)新增可折叠「📖 使用教程」区块,三步引导: 1. **一键配置到 DSH** — 点模型行「⚡ 一键配置到 DSH」或顶部「⚡ 配置本页全部」批量添加; 2. **到「设置 → 模型」填 Key** — 找到 `freehub-*` 提供方,把本页「申请免费密钥key」拿到的 Key 粘贴到 API Key 框并保存;附「no credential / invalid key」提示; 3. **开始使用** — 聊天页切换模型即可对话;多 Key 可点「🔄 多 Key 轮换」一次填入多条。 - 默认收起,点标题展开/收起;展开状态用 `localStorage('fmh:guideOpen')` 记忆。 - 新增 i18n 字符串与 CSS 类(`.fmh-guide*`),不改动既有 `.fmh-*` 样式。 --- ## F7 · 单 Key 提供方「未写入 keypools 文件」说明(设计,非 bug) ### 现象 - 站长免费模型(`https://chat.ywc.cc.cd/v1`)一键配置后能进 DSH 设置,但没出现在 `dsh-free-models-hub-keypools.json`。 ### 说明 - `dsh-free-models-hub-keypools.json` **只用于「多 Key 轮换代理」**,保存 `pid → 真实API` 与 `pid → 多个Key`。 - **单 Key 直连**提供方(如 `freehub-model-5`,baseURL 是真实 API URL) **不需要代理,也不写 keypools**;Key 直接填在 DSH `设置 → 模型` 即可。 - 只有点了「🔄 多 Key 轮换」并保存 ≥1 个 Key 时,才经 `/p/save-pools` 写入 keypools, 并把 baseURL 切成 `http://127.0.0.1:8787/p/`。 - 现状 `settings.yaml` 中 `freehub-model-5` baseURL = `https://chat.ywc.cc.cd/v1`(直连)即为此设计。 > 可选增强:若希望「一键配置单 Key」时也把 `targets[pid]=apiBase` 提前写入 keypools 文件 > 以便日后无缝切换多 Key,可在 `applyProvider` 成功后同步一次 `targets`(当前未实现,未必要)。 --- ## 回归验证清单 1. `npm run tree`(若有)/ `npm run build` 通过,`lib/` 与 `src/` 同步。 2. `npm test` 33/33 通过。 3. 一键配置新增提供方 → DSH `设置 → 模型` 出现且**不覆盖已有**提供方。 4. 多 Key 轮换:点「🔄 多 Key 轮换」保存 Key 后,keypools 文件出现该 pid 且 baseURL 切到代理。 5. 直连单 Key:基础 URL 保持真实地址,不写 keypools。 6. 重启 DSH 与代理后,代理 `/p/ping` 与 DSH `:3080` 均正常。 --- *最后更新:2026-08-27*