Files
dsh_shenxian/dsh-server-docs/archive/交接单-已完成/T06-模型设置页-多厂家条目可开关.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

80 lines
8.4 KiB
Markdown
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.
# T06 · 平台自建「模型设置」页 —— 多厂家条目自助配置(各自开关、可同时启用)
- 日期:2026-09-13
- 状态:✅ **已完成并归档**(后端已部署 + 插件 **0.3.11** 已铺发两实例 + 角色补丁已刷新;验收四件事见 §6)
- 执行会话:`craft-session-T06`(原 lane 会话 `rebuild-my-keys` 失联,经**用户明确授权**解锁后接手)
- 触发(用户 09-13 22:5x 三轮口径):官方「设置 → 模型」页在平台环境**必然报错** ⇒ 改走平台自建页;条目要能**各自开关**、平台共享模型也要**列入且可开关**;具体用哪个模型**在 dsh 对话框里选**。
- 档案:**`04-调整方案/87-模型设置页-用户自配厂家与共享开关.md`**(本轮全部技术结论的单一来源)
---
## 1. 目标(已达成)
> 让**每个用户**在实例内「设置 → **模型设置**」自助管理**多条**模型条目(内置 DeepSeek + 自定义 OpenAI 兼容厂家),**每条可独立开关、可同时启用**;平台按「已启用」把配置写进实例(`$DSH_HOME/.credentials.yaml` 的 `refs:` + `$DSH_HOME/settings.yaml` 的 `llm-pi-ai.providers.<route>`),用户再到 **dsh 对话框的模型选择器**里挑具体模型。
## 2. 只读前置(执行时的核实结果)
| # | 核实项 | 结果 |
|---|---|---|
| 1 | 工作树里哪些是本轮改动 | `src/db/*`、`src/web/{server.ts,routes/auth.ts,model-landing.ts}`、`ensure-role-profile-patch.cjs`、`poc/business-plugins/**` = **本轮**;`admin-user-ops.ts` / `dsh.ts` / `portal.html` / client.js 的改名段 = **档案 86** |
| 2 | `npx tsc --noEmit` 应报「类未实现接口」 | ✅ 如预期(`sqlite.ts`/`pg.ts` 各缺 5 项)→ 已补齐归零 |
| 3 | 「官方页为何不可用」的结论在哪 | ⚠️ **不在档案 86**(86 = admin 跨用户实例管理 + 两处改名,全文仅 §一–§七);真结论原只在**代码注释**里。本轮已挖到**权威出处**(官方 `dsh-client-ui-settings/README.md:97`)并落进档案 87 §一 —— 同时辨清了与 `03-路线图` 决策 3 的「两层」关系(见档案 87 §1.1) |
| 4 | 线上状态 | 后端未构建未部署、插件 0.3.10 ✅(但**角色补丁其实已被上一会话刷过**——见 §7 教训) |
| 5 | `0.3.11` 这个版本号没被用过 | ✅(本机 / `/opt/dsh/artifacts` / 两实例 profile 三方核对) |
## 3. 范围
**改了**(详见档案 87 §四):`src/db/{schema,types,repo,adapter,sqlite,pg}.ts` · **新文件** `src/web/model-landing.ts` · `src/web/server.ts` · `src/web/routes/auth.ts` · `poc/business-plugins/lib/client.js` + `package.json`(→0.3.11) · `ensure-role-profile-patch.cjs` · `test/db.test.mjs` · `scripts/verify-{model-landing,platform-admin-section,mem-model}.mjs` · `package.json`(注册新验证脚本)
**没动**(守住了边界):官方 dsh 主程序与缓存 · `web/portal.html` 的「模型管理」页 · `04-调整方案/86` · `/var/lib/**` 之外的系统面 · systemd 单元 · nginx/nft · 内存配额档位。
## 4. 决策点
- **用户已定三条口径**(各自开关可同时启用 / 共享模型也列入且可开关 / 模型在 dsh 对话框选)—— 全程未再上抛。
- **技术自决**(可推翻):共享开关走单列查询不动 `USER_COLS`;`getEnabledCredentialKeyRef` 重定义为「已启用的内置 DeepSeek 条目」;`selectCredentialKey` 降级为非互斥同义实现;落地用**成对标记**夹住自己写的 route 块。
- **开跑前唯一的未取证项**(自定义厂家的 settings 字段名)= 已取证,且**救回一个必须纠正的字段名**:协议字段是 **`api`**,不是 `protocol`(§5)。
## 5. 步骤与验证(全部执行完毕)
1. 抢锁 + 占号 → ✅(原 lane 会话失联;**用户明确授权解锁**,随后自持 `craft-session-T06`)
2. 补 `repo.ts` 两个函数 + 重定义 `getEnabledCredentialKeyRef` → ✅
3. **取证官方 schema** → ✅ 读 `[email protected]`:分区名 `llm-pi-ai`、凭据字段 `apiKeyEnv`、endpoint `baseURL`、协议 **`api`**(三枚举)、ref 名须匹配 `^[A-Za-z_][A-Za-z0-9_]*$`、`provider`/`maxRetries` 会直接抛错
4. 补 `sqlite.ts` / `pg.ts` 各 5 项 → ✅ `tsc` 全绿
5. 写落地层 `model-landing.ts` + **13 组回归** → ✅(回归当场抓出**一个真 bug**:`providers:` 被写重复 —— 第一版按"只看顶层行"找链,而 `providers:` 本身就是缩进 2 的子键)
6. 改 `server.ts`(逐条落地 + 托管清单 + **一次性交接**)→ ✅ 见 §6③
7. 改 `auth.ts` 4 条路由 → ✅ 含 4 类非法入参被拒(400/409)
8. 前端「模型设置」分区(插件 **0.3.11**)→ ✅ 无浏览器 harness 全绿(新增 8 条断言)
9. 角色补丁 `--force --restart` → ✅ **先留回滚点**再跑,跑完 diff 验证
10. 收尾四件套 + 台账 → ✅
## 6. 验收(用户点名的四件事 —— 全部有证据)
| # | 验收项 | 证据 |
|---|---|---|
| ① | 官方「模型」栏两账号都不再出现 | 两账号 `cordis.patch.yml` 各 1 处 `- id: ui-settings-models` + `disabled: true`;`--restart` 已生效(客户端 bundle 于实例启动时重打)。**页面实看留用户确认**(本机 `agent-browser` daemon 起不来,沿用档案 86 §七-3 的做法) |
| ② | 新页能配 / 能开关 / 能删 | HTTP 实测(临时 session 直插,R4):新增内置条目 → `effective: shared→own`;新增自定义厂家 → `route/base_url/api/models` 全部落库;`toggle` 关/开 → `enabled` 0/1;非法入参:非 http baseUrl→**400**、缺 models→**400**、route 撞车→**409**、非法协议→**400**;前端 harness 全绿 |
| ③ | 共享模型开关**真的影响实例落地** | 关共享 + 重启 → 实例 `.credentials.yaml` 里 `DEEPSEEK_API_KEY` **消失**(老实现写下的那行被"一次性交接"正确认领并撤掉),**实例进程 env 里该变量 = 0 条**;再打开 → 重启 → 该行**回来**且与自定义厂家的 ref **并存** |
| ④ | 自定义厂家被 dsh 模型选择器认到 | 实例 `settings.yaml` 落地为官方 schema 原文(`llm-pi-ai.providers.acctest-gw: {apiKeyEnv, baseURL, api: openai-completions, models: [...]}`);实例**启动成功**、`journalctl` 无 `llm-pi-ai` / `settings-rejected` 报错;删除该条目后重启 → **整块消失** |
本地全套(Node 22):单测 **49 项 0 失败** + `verify-inject` / `verify-static` / `verify-platform-admin-section` / `verify-mem-model` / `verify-model-landing` **全绿**。
## 7. 回滚
见档案 87 §七。要点:后端 `npm run build && systemctl restart dshs`;插件回落 0.3.10;角色补丁回滚件 = `/opt/dsh/backups/patch-20260913-234511/`(⚠️ 回滚它 = 把必然报错的官方页放回给用户,除非接受该副作用)。
## 8. 回报格式(已回填)
- 每步命令与输出:见档案 87 §五/§六;
- **取证结论**:档案 87 §三(`api` 不是 `protocol`,是本轮最重要的一条外部事实);
- 产物:插件 **0.3.11** / 48,590 B / md5 `15c8083f5db85b3fce6a8a15db133319`,两实例 `bundles=6` 且含 `@dsh-local/business-plugins`;
- 台账:本表 + `交接单/README.md §一` + `INDEX.md`;
- commit:**未提交**(用户未要求;且本机代码仓含**别人在途**的档案 86 改动,不能一起提)。
---
## 附:本轮暴露的三个「顺便修 / 已上报」项(都属本 lane)
1. 🔴 **`ensure-role-profile-patch.cjs` 的整文件覆盖是颗雷**(已修):它的 `--force` 升级分支原本 `writeFileSync(patchPath, block)`,而 admin 的 `cordis.patch.yml` 里**同时住着三个平台块**(`disable-hmr` / `workspace-scoped-picker` / role patch)⇒ 一旦触发升级就会**静默抹掉另两个**(HMR 重新连、目录选择器退回无限制版)。修法 = 新增 `stripManagedBlock()` 只替换自己那段;实机 diff 验证另两块完整保留。
2. ⚠️ **`verify-mem-model.mjs` 陈旧断言**(已修):档案 86 重构后 `request.user!.id` → `user.id`,该断言长期红着。
3. ⚠️ **上一会话自述与线上不符**:它称「未构建未部署」,但**角色补丁实际已被它刷到线上**(两账号的 patch 里已有 `ADMIN_MODELS_BLOCK` 的内容)。⚠️ 教训:**别拿会话自述当线上事实**,一律看服务器实际文件 —— 这正是本次「先看服务器再动手」救回两颗雷的原因。