# 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.`),用户再到 **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** → ✅ 读 `dsh-llm-pi-ai@0.1.2-rc.1`:分区名 `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` 的内容)。⚠️ 教训:**别拿会话自述当线上事实**,一律看服务器实际文件 —— 这正是本次「先看服务器再动手」救回两颗雷的原因。