Files
dsh_shenxian/dsh-server-docs/archive/交接单-已完成/T06-模型设置页-多厂家条目可开关.md
T

79 lines
8.4 KiB
Markdown
Raw Normal View History

# 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` 的内容)。⚠️ 教训:**别拿会话自述当线上事实**,一律看服务器实际文件 —— 这正是本次「先看服务器再动手」救回两颗雷的原因。