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 一律写「远程服务器」。
12 KiB
91-模型设置页复刻官方交互——卡片行 + 两步新增(2026-09-14 落地)
- 日期:2026-09-14 | 状态:✅ 已上线(
business-plugins0.3.15,两实例已铺发) - 触发:用户原话 ——「admin模型设置和状态 一个名称换行了,另一个两个按钮换行了,交互体验需要优化。还有新增模型的操作交互和官方原始新增模型的交互差距很大,建议仔细参考 复刻官方的交互」(附截图:
内置 DeepSeek折行 +已启用折行 +停用/删除折行)
TL;DR 换行不是"窄了",是版式选错了 —— 原「我已添加的厂家」是 4 列表格(
pa-tbl,列宽 auto):内置 DeepSeek、已启用、停用|删除全是按列宽被挤断的。 改为官方ui-settings-models的卡片行(rowHead身份左对齐 +rowActionsmargin-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}?」
官方三条关键口径(原文照引,不要再自己发明):
- 「编辑卡片上的主字段是单独一个 API 密钥输入框——页面从不询问环境变量名」;
- 「收起的『自定义设置』折叠区承载精选的额外字段」;
- 「添加自定义提供方声明一条 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系列样式仍被其它页(系统管理各功能页)使用,未删。