Files
dsh_shenxian/dsh-server-docs/04-调整方案/91-模型设置页复刻官方交互-卡片行与两步新增.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

12 KiB
Raw Blame History

91-模型设置页复刻官方交互——卡片行 + 两步新增(2026-09-14 落地)

  • 日期:2026-09-14 | 状态:✅ 已上线(business-plugins 0.3.15,两实例已铺发)
  • 触发:用户原话 ——「admin模型设置和状态 一个名称换行了,另一个两个按钮换行了,交互体验需要优化。还有新增模型的操作交互和官方原始新增模型的交互差距很大,建议仔细参考 复刻官方的交互」(附截图:内置 DeepSeek 折行 + 已启用 折行 + 停用/删除 折行)

TL;DR 换行不是"窄了",是版式选错了 —— 原「我已添加的厂家」是 4 列表格(pa-tbl,列宽 auto): 内置 DeepSeek、已启用、停用|删除 全是按列宽被挤断的。 改为官方 ui-settings-models 的卡片行(rowHead 身份左对齐 + rowActions margin-left:auto, 两侧 white-space:nowrap)⇒ 结构上不可能换行,同时把整页拉齐到官方观感。 「新增」从「常开表单 + 下拉里混一项『自定义厂家…』」改成官方两步式:默认只露两个虚线按钮 (+ 添加提供方 / + 添加自定义提供方),点击才展开卡片;卡片主字段是单独一个「API 密钥」, 显示名挪进收起的「自定义设置」<details>。 顺带修两处真缺陷:① admin 自己看共享卡片时写「由 admin 配置」;② 「内置 DeepSeek 密钥留空则回落平台共享密钥」是错的(留空必 400)。 另补删除二次确认(官方 deleteDialog 等价物)—— 原来「删除」紧挨「停用」且一点即删,连已存密钥一起丢。 CSS 数值与文案逐值照抄官方(本档 §四 给出单一来源),并新增两个构建期校验脚本防回归。


一、问题定位(先把"为什么"钉住,别停在"看着窄")

现象(用户截图) 根因 判据
内置 DeepSeek 折成两行 表格 4 列 auto 列宽,第 1 列拿不到 110px .pa-tbl th 有 white-space:nowrap,td 没有(lib/client.js 的 .pa-tbl 规则)
已启用 折成「已启/用」 同上(状态 列同样 auto) .pa-badge 是 inline-block,列宽不足时内部换行
停用/删除 各占一行 同上;两按钮在同一个 td 里塞着 原代码 jsxRuntime.jsxs("td", { children: [button, button] })

⚠️ 注意:这三个现象不是同一个巧合,而是同一个结构性原因(auto 列宽)的三个实例。 所以修复只能换版式,加 nowrap 补丁不算修好(第 2 列的长 endpoint 会把整表顶宽、又重新挤第 1 列)。

二、官方交互取证([email protected])

取证位置(单一来源,勿凭记忆):

内容 路径
页面语义/交互说明(中文) /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-client-ui-settings-models/README.zh.md
CSS 模块(版式数值的唯一来源) 同包 lib/client.js 头部 \0dsh-css: 区块(css$3,类名带哈希前缀 zGbnIq_)
中文文案词典 同包 lib/client.js 的 zh 对象(add / customAdd / customRouteHint / fetchModels …)
组件结构 同包 lib/client.js 的 ModelsSection / ProviderEditor / CustomProviderCard

官方骨架(本轮照此复刻):

section(max-width:720px, flex column, gap 12)
├─ title / intro
├─ ul.rows(gap 8)
│   └─ li.rowCard(border .5px, radius 16, padding 12px 14px, flex column gap 12)
│       ├─ div.rowHead(flex, align-center, gap 10)
│       │   ├─ span.rowIdentity(inline-flex, gap 6, min-width 0)
│       │   │   ├─ span.rowName(14px/500/22)
│       │   │   ├─ span.rowTag(border .5px, radius 4, 11px/16)
│       │   │   └─ span.credentialDot(8px 圆点,绿=已配置 / 红=缺失)
│       │   └─ span.rowActions(margin-left:auto, gap 4)
│       │       ├─ button 编辑(28px/radius14/pad 0 10/12px)
│       │       └─ button 删除(danger,同尺寸、无边框、红字)
│       └─ [展开的编辑卡片]
├─ div.addBlock(flex column gap 12)
│   ├─ 若 adding  → div.addCard(bg-module-platform, radius 12, pad 14px 16px)
│   │      ├─ field「提供方」+ select(休眠目录清单)
│   │      ├─ ProviderEditor:**主字段 = 单个「API 密钥」** + `<details>`「自定义设置」
│   │      └─ editorActions(右对齐:取消 / 保存)
│   ├─ 若 declaring→ div.addCard → CustomProviderCard(Provider ID→显示名→API 地址→协议→密钥→模型目录)
│   └─ 否则      → div.addActions(flex-wrap, gap 10) → **两个虚线按钮**
│                    button.addButton(border 1px dashed, radius 16, flex 1 1 0, min-width 180px, h 44)
└─ 删除 = Modal,标题「删除 {provider}?」

官方三条关键口径(原文照引,不要再自己发明):

  1. 「编辑卡片上的主字段是单独一个 API 密钥输入框——页面从不询问环境变量名」;
  2. 「收起的『自定义设置』折叠区承载精选的额外字段」;
  3. 「添加自定义提供方声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 Provider ID、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底」。

三、改了什么(poc/business-plugins/lib/client.js)

# 项 之前 之后
1 「我已添加的提供方」 div.pa-wrap > table.pa-tbl(4 列,thead:厂家/说明/状态/操作) ul.ms-rows > li.ms-rowCard(名称+标签+状态点 / 操作右对齐);状态文字下沉到独占一行的副标题(整行宽度 ⇒ 不可能折行,信息量不丢)
2 新增入口 一个常开的 pa-box,里面 3 行 input + 下拉(下拉里混一项「自定义厂家(OpenAI 兼容网关)…」) 两个虚线按钮(ms-addActions)→ 点击展开对应卡片;一次只开一张
3 「添加提供方」卡片 名称 + API Key 两个并排 input 「提供方」select + 单独一个「API 密钥」(主字段);显示名与模型目录收进 <details>「自定义设置」
4 自定义提供方 端点/标识并排、协议下拉、模型用 textarea 一行一个 官方字段序;模型改用行式编辑器(ms-modelEntry 一行一个 + + 添加模型 + 行尾 ✕)
5 按钮 pa-sm / pa-btn(门户家族) ms-btn(官方度量:36px/radius18/pad 0 14;行内 28px/radius14/pad 0 10)
6 行标签 无 ms-rowTag:内置 / 目录 / 自定义(官方 rowTag 位置)
7 状态点 徽章文字 已启用/已停用 8px 圆点(ms-dot,绿=已启用 / 灰=已停用,带 title+aria-label)+ 副标题里的状态文字
8 删除 一点即删 二次确认弹窗(点名该提供方,遮罩/取消可关)

样式落点:新增 .ms-* 规则块,插在既有 .pa-note 之后;注释里写明「类名去哈希、前缀改 ms-,数值逐值照抄官方」。 词典:ms.* 增补到 305 键(zh/en 同键集);沿用官方中文文案(自定义设置 / Provider ID / API 协议 / 其余字段在 settings.yaml 中,请直接编辑对应段。 等)。

四、顺带修掉的两处真缺陷(非样式)

# 缺陷 判据 修法
1 admin 自己打开该页时,共享卡片副标题写「由 admin 配置」 后端 sharedKeyInfo() 早已返回 ownerIsMe,前端却只用 shared.owner ownerIsMe === true ⇒ 显示「由你配置」
2 提示「内置 DeepSeek:密钥留空则回落到平台共享密钥」 src/web/routes/auth.ts 的 keyAddSchema:apiKey 是 required + minLength:1,且入口处有 /^[A-Za-z0-9\-_.]{1,256}$/ ⇒ 留空必然 400 invalid_api_key 文案改为「输入你的 DeepSeek API 密钥」,并前端禁用保存按钮直到非空

第 2 条是旧文案在骗用户:照着填必然失败,而错误码会被翻成「API 密钥不合法」,用户只会以为自己抄错了密钥。

五、为什么没做官方的「获取可用模型」

官方那支是服务端经 Remote llm/discoverModels 去问端点(fetchModels: "获取可用模型" → 可搜索选择器 → 「添加所选」)。 平台侧要等价实现得新开一个"服务端代为请求用户给的 URL"的接口 —— 那是出网面扩大(用户可控 URL ⇒ SSRF 面), 按 **R5(权限只准收窄)**必须先出权限影响评估并取得确认,不是本轮该顺手做的事。

采取的最小替代:目录提供方在「自定义设置」里只读展示该目录自带的模型清单(数据已在 /api/me/model-providers 里,无需新接口); 自定义提供方保留手工填写模型(官方的兜底路径,原文:「其余协议会报告自己无法被询问,其模型需手工填写」)。

若后续要做真发现,正确做法是后端加白名单化探测(限制协议/私网/超时 + 只回模型 id),并单独立档走 R5 评估。

六、验证(四层,逐层给证据)

层 手段 结果
L1 语法 node --check lib/client.js ✅
L2 词典 + 版式锚点 新脚本 scripts/verify-models-dict.mjs ✅ 20 项全绿(含 nowrap/margin-left:auto 断言)。本轮正是它抓到 ms.needUrl / ms.needModels 被引用未声明(语法与构建都不报错的静默缺陷)
L3 渲染冒烟 新脚本 scripts/verify-models-render.cjs ✅ 5 个分支(默认双按钮 / 添加提供方卡片 / 自定义卡片 / 删除弹窗 / 卡片行)真渲染,无运行期异常、无缺键
L4 上线实证 产物 sha1 比对 + 实例进程启动时刻 vs 包落地时刻 ✅ 本机 838b4f11… == 服务器同值;两 profile 均已 0.3.15,运行中实例启动时刻晚于包落地 ⇒ 加载的是新 bundle

铺发(本平台唯一方式,不走候选池):npm pack → scp /opt/dsh/artifacts/business-plugins-0.3.15.tgz → cd /opt/dshs && node scripts/ensure-biz-plugins.cjs --all --restart(admin bundles=7、guest bundles=6,均含该包)。

⚠️ 该脚本按产物文件名取最大版本 ⇒ 升版本号前先确认该号没用过(同号不同内容会被静默跳过)。

七、回滚

  • 单点回退:/opt/dsh/artifacts/ 里删掉 business-plugins-0.3.15.tgz(保留 0.3.14 及以前)⇒ 再跑一次 ensure-biz-plugins.cjs --all --restart,脚本会选最大版本 0.3.14。
  • 本机源码回退:git -C D:\github\dsh_shenxian checkout -- poc/business-plugins/lib/client.js poc/business-plugins/package.json package.json。
  • 无数据迁移、无 SQLite 变更 ⇒ 回滚不需要动库。

八、遗留

  • ⚠️ 代码侧与文档库本轮仍未提交(工作树留痕):poc/business-plugins/{lib/client.js,package.json}、scripts/verify-models-dict.mjs、scripts/verify-models-render.cjs、package.json。用户明确说提交才提交(根 CODEBUDDY.md §4)。
  • 官方的两支能力有意不做:获取可用模型(见 §五)、全选/取消全选 候选选择器(依附前者)。
  • 官方的「首次运行弹窗(内测声明 + DeepSeek 引导)」与「settings.models.provider-card 扩展席位」与本平台模型不符(我们是平台侧落地、spawn 时写实例配置),不采纳。
  • pa-tbl 系列样式仍被其它页(系统管理各功能页)使用,未删。