133 lines
12 KiB
Markdown
133 lines
12 KiB
Markdown
# 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` 系列样式仍被其它页(系统管理各功能页)使用,**未删**。
|