@nimoxie/dsh-model-organizer 0.5.11 → 0.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DEVELOPMENT.md CHANGED
@@ -133,10 +133,28 @@ pi-ai 的 profile 与 `modelFields` **都没有 enabled/disabled 字段**,所
133
133
 
134
134
  ---
135
135
 
136
- ## 设置页的两个 UI 陷阱(都是实机截图打回来的)
136
+ ## 设置页的 UI 约定(都是实机截图打回来的)
137
137
 
138
- 1. **控件必须 `boxSizing: "border-box"`**。`width: "100%"` 配默认的 content-box 时,宽度是 100% 再加左右 padding(16px) 与边框(2px),输入框直接顶出设置列 —— 实机截图里 API 地址那一栏就是这样。凡是会被拉伸的输入框/选择器/按钮都显式声明了 border-box。
139
- 2. **不要用原生 `<select>`**。下拉列表由平台绘制,本主题下渲染成白底浅字(截图里「完全看不清」)。协议选择改成了三个自绘的 radio 样式分段按钮(`pageSegments`/`pageSegment`,选中态用 accent 边框),完整标签(含 endpoint)放在 `title` 与下方提示行里。
138
+ ### 基础两条(0.4.0)
139
+
140
+ 1. **控件必须 `boxSizing: "border-box"`**:`width: "100%"` 配默认 content-box 时,宽度是 100% 再加 padding 与边框,输入框会顶出设置列(实机截图里 API 地址那栏)。凡会被拉伸的输入框/选择器/按钮都显式声明 border-box。
141
+ 2. **不要用原生 `<select>`**:下拉列表由平台绘制,本主题下是白底浅字(截图里「完全看不清」)。协议选择改用三个自绘的 radio 分段按钮(`pageSegments`/`pageSegment`),完整标签放 `title` 与下方提示行。
142
+
143
+ ### 0.5.12:按钮的尺寸、对齐、颜色与文案
144
+
145
+ - **尺寸**:三颗行内图标键(供应商删除、模型测试、模型删除)共用 `ACTION_BOX`(19×17、圆角 6、`border-l1`)。高度**刻意等于启用开关的 17**——早期 26×22 被截图指出「比开关高一头」;宽度按截图在 20 → 22 → 21 → 17 → 19 之间收敛定稿,图标 13px。
146
+ - **对齐**:详情头部是 `align-items: baseline`。图标键与开关都没有文字基线(前者首个 flex 项是 SVG,后者是带 2px padding + 1px 边框的圆点盒),各自按自己的基线对,盒子同高却差 1–2px。两者都加 `alignSelf: "center"`:文字仍按基线排,控件按中线互相对齐。
147
+ - **颜色**:删除键 = `DELETE_ACTION`(共享盒子 + 红框、透明底、红图标);测试连接键 = `TEST_ACTION`(只覆盖 `background` 为普通按钮的浅底)。结论色只留在图标上(`.dsh-mo-act-ok/-miss/-danger`),不染边框。
148
+ - **文案**:悬停只回答「这个按钮做什么 / 现在什么状态」,一行说完;实现细节留在本文档。冒烟测试钉「提示 ≤16 字、结论 ≤18 字」,防止长段落回流。
149
+
150
+ ### 按钮悬停:规则为什么只能写在注入样式表里
151
+
152
+ 页面与四个弹窗的按钮**静止态是内联样式**(`S.pageButton` 等),而内联样式赢过普通 `:hover`——直接写 `.foo:hover{background:…}` 会静默失效(与 0.4.0「内联背景盖掉官方 hover」是同一个坑)。做法:
153
+
154
+ - 悬停/按下/禁用三态写在 `ensureOwnStyles()` 注入的样式表里,且用 `!important`(只用于这三个状态,静止态仍由内联样式负责);
155
+ - 作用域 `:where(page、panel、四个弹窗的 button)`:`:where()` 优先级为 0,变体规则(主要操作保持强调色、危险操作泛红、旁白按钮提亮文字)才能稳定压过通用规则;
156
+ - **开关(`role="switch"`)显式排除**:它的底色就是开/关状态,悬停改底色会被误读成状态切换,只加 `box-shadow` 光环;
157
+ - 关闭键(`data-mo$="-close"`)悬停时把图标提到 `label-primary`,否则只变底色看不出来。
140
158
 
141
159
  ## 已知的非插件告警:permission preset "auto"(会连带「权限选择消失」和「工具通道断」)
142
160
 
@@ -172,7 +190,7 @@ Error: permission: cannot restore preset "auto" without its active integration
172
190
 
173
191
  **症状**:在「添加模型提供商」弹窗里按 Tab,焦点跳到弹窗背后的设置页。
174
192
 
175
- **原因**:三个弹窗都用 `ReactDOM.createPortal` 挂到 `document.body`,在文档顺序里排在整页**之后**。两张脸:① Tab 从卡片最后一个控件继续往前走,浏览器绕回文档开头(也就是设置页的导航);② 打开瞬间焦点没落进弹窗(宿主壳自己的 focus 流程会把刚插入的 portal 的 `focus()` 抢走),于是第一次 Tab 直接从页面上的按钮继续走。
193
+ **原因**:每个弹窗都用 `ReactDOM.createPortal` 挂到 `document.body`,在文档顺序里排在整页**之后**。两张脸:① Tab 从卡片最后一个控件继续往前走,浏览器绕回文档开头(也就是设置页的导航);② 打开瞬间焦点没落进弹窗(宿主壳自己的 focus 流程会把刚插入的 portal 的 `focus()` 抢走),于是第一次 Tab 直接从页面上的按钮继续走。
176
194
 
177
195
  **0.5.1 为什么没修好**:第一版只在卡片上挂 `onKeyDown`,且**只在边界**(第一个/最后一个)才 `preventDefault`。中间步骤仍交给浏览器自己走;只要焦点不在卡片里(情形②),或者边界判定没命中,浏览器就照旧走到页面上去。**教训:不能让浏览器"帮我们走一步",必须整段接管。**
178
196
 
@@ -238,81 +256,74 @@ Error: permission: cannot restore preset "auto" without its active integration
238
256
 
239
257
  **教训**:凡是「问宿主网关」的调用,参数要和已经跑通的那条路径逐项对一遍;冒烟测试里固定断言 `request.baseURL`,防止再退化。
240
258
 
241
- ### 0.5.6–0.5.7:按钮尺寸的两次返工(**已被 0.5.8 取代**,留作教训)
242
-
243
- 那两版在"按钮多大"上来回改:`56×20 → 32×20 → 36×22 → 20×17`,期间还把 emoji `🗑` 换成手绘 SVG 的 `TrashIcon`,越修越不像界面里的东西。
259
+ ### 0.5.6–0.5.11:行内图标键的四次返工(教训)
244
260
 
245
- 两个教训:
261
+ - **别自己画图标**:emoji `🗑` 的墨迹远小于字号,手绘 SVG 又和界面里的东西不像;改用 DSH 自带 primitives(0.5.8,98 个图标),`kitIcon()` 取不到时退回同尺寸文字 glyph,无头测试仍能跑。
262
+ - **尺寸别靠眼估**:盒子在 `56×20 → 32×20 → 36×22 → 20×17` 之间反复;真正的判据是「与相邻控件同高」(开关 17),最终统一 19×17(见 0.5.12)。
263
+ - **「转圈看不见」不是动画没写**:本地网关 50ms 就答完,React 会把 busy 与结论合并进同一帧,屏幕上从未出现转圈帧。修法:点击回调里直接给 DOM 加 `dsh-mo-act-busy` 并起 CSS 动画(`startSpinNow`),结论至少 450ms 后才落状态;动画作用在 `svg` 上,按钮尺寸不变。
264
+ - **测试连接失败先查参数**:`discoverModels()` 对目录外路由要求 `baseURL`,只发 `{ provider }` 会被宿主拒绝(0.5.5 的真因);凡是问宿主的调用,参数与已经跑通的那条路径逐项对齐。
246
265
 
247
- 1. **盒子高度对了不代表看起来对**:`🗑` 在 `font-size: 10` 下实际墨迹只有几像素,四周全是空白,所以"按钮很小"其实是"图标很小"。emoji 的字形尺寸和 `font-size` 不同比例,描边路径没有这个问题。
248
- 2. **别自己画图标**:DSH 有现成的图标库(见 0.5.8),手绘 SVG 只会和界面其它按钮不是一套。
266
+ ### 0.5.13–0.5.14:新建弹窗里的模型改成「清单 + 勾选」
249
267
 
250
- (当前尺寸以 0.5.10 / 0.5.11 为准:模型行 `26×22` 带边框,供应商详情 `22×17`,测试按钮与删除按钮**同一个盒子**。)
268
+ **问题**:0.5.3 为了让「必须列出模型」这条宿主规则可满足,把「从网关获取」做成了**一次性把网关返回的所有 id 灌进输入框**。这和官方交互相反(官方是弹窗勾选、点添加才落盘),用户也没得选。
251
269
 
252
- ### 0.5.8:行内操作改用 DSH 自带图标库(自绘 SVG 作废)
253
-
254
- **背景**:0.5.7 手绘了一个垃圾桶 `<svg>`,虽然尺寸对了,但和界面其它按钮不是一套东西。DSH 其实自带图标库:
270
+ **做法**:
255
271
 
256
- ```js
257
- const P = require("@deepseek-ai/dsh-client-ui-primitives"); // 官方页面就是这么拿的
258
- P.IconTrashOutlineRegular, P.IconPlayOutlineRegular, P.IconCheckOutlineRegular,
259
- P.IconWarningTriangleOutlineRegular, P.IconCloseOutlineRegular, P.IconLoadingOutlineRegular
260
- ```
272
+ 1. `addDialog.models` 从字符串改成**草稿数组**(每项 `{id, context, maxTokens, vision}`),另加 `modelInput` 只承载手工输入;
273
+ 2. 手工添加:`parseModelIds(modelInput)` → `addModelsToDraft()` 去空去重后追加,输入框清空;
274
+ 3. 「获取可用模型」:用对话框里的草稿(`baseURL` + `api` + 一次性 `apiKey`)调 `discoverModels`,候选塞进**同一个 picker**,只是多带一个 `target: "draft"`;
275
+ 4. `fetch-adopt` 分叉:`target === "draft"` → 只回填弹窗清单(**不写盘**);否则维持 `writeModels(draftRows.concat(chosen))`;
276
+ 5. 清单在弹窗里逐行展示(id + 容量/视觉 chips + 🗑 移除),空清单显示提示;
277
+ 6. `createProvider()` 改用 `modelsFromDraft()` 统一校验(空 ID / 重复 / 容量非法都在写盘前拒),profile 写成 `{ api, models: built.models }`。
261
278
 
262
- 这个模块由前端在模块表里注册(`dsh-web-frontend` 的 loader 表里是 `@deepseek-ai/dsh-client-ui-primitives`),共 98 个图标;插件里原有的 `icon(name, props)` 就在用它(`MenuSurface` / `rankByName` 同门),所以只差没用在按钮上。
279
+ **为什么复用 picker**:同一个弹窗、同一套搜索/全选/勾选,draft 与 saved 的唯一差别是「采纳之后去哪」,用 `target` 区分,避免第二套弹窗各写一份逻辑。
263
280
 
264
- **改法**:
281
+ **0.5.14(同一处的排版)**:手工输入框原来用 `pageInputWide`(`width: 100%`),在 `pageKeyRow`(`flex-wrap: wrap`)里会把「添加」挤到下一行。改成新的 `pageInlineRow`(`nowrap`)+ `pageInputGrow`(`flex: 1 1 auto; width: auto`):输入框自适应,按钮稳定在右侧。冒烟断言 `flexWrap === "nowrap"` 且输入框 `width === "auto"`。
265
282
 
266
- 1. 删掉自绘的 `TrashIcon`,新增 `kitIcon(name, glyph, size)`:优先 `icon(name, {size})`,取不到时退回一个同尺寸的文字 glyph(无头冒烟测试里拿不到真实模块,靠它保证布局断言仍成立);
267
- 2. 三个按钮都换成**无边框 ghost 图标按钮**(`S.pageIconAction` 26×22、`S.pageIconActionHead` 22×17),hover 底色与配色全部交给注入的样式表:`.dsh-mo-act` / `.dsh-mo-act-danger` / `.dsh-mo-act-ok` / `.dsh-mo-act-miss`,图标靠 `currentColor` 继承颜色;
268
- 3. 测试按钮的**图标就是结论**:`▶` → `⟳` → `✓` / `⚠` / `✗`(同一 14px,按钮盒子不变)。删除按钮是 `IconTrashOutlineRegular`,常红;
269
- 4. 供应商详情那颗高度仍是 17(等于启用开关),模型行两颗同为 26×22。
283
+ **顺带**:删掉 0.5.8 之后就不再使用的 `page.testOkShort / testMissingShort / testFailShort`(zh/en 同步,157/157)。
270
284
 
271
- **回归**:冒烟的 require 桩现在提供 `@deepseek-ai/dsh-client-ui-primitives`(图标组件带 `data-icon`),断言:供应商删除按钮必须是 `trash` 图标 + `dsh-mo-act-danger` + `height 17`;两行操作按钮盒子相同(26×22,`border: none`);测试结论必须把图标换成 `check` / `warn`。
285
+ **回归**:冒烟断言 —— 「获取可用模型」必须弹出 picker(`fetch-dialog`);「添加所选」后清单出现该 id 且**仍未写盘**;逐个移除后创建被拒且零写盘;手工输入 `glm-4.6, qwen-max` 后清单两项、创建时同一次 mutate 带 `models`。
272
286
 
273
- ### 0.5.9:测试按钮的图标语义 + 转圈动画
274
287
 
275
- **图标**:空闲态从 `IconPlayOutlineRegular` 换成 `IconLinkOutlineRegular`。这个动作是「去问网关它提供哪些模型」,不是「跑一次模型」,播放键会误导。结论图标不变(`IconCheckOutlineRegular` / `IconWarningTriangleOutlineRegular` / `IconCloseOutlineRegular`),进行中是 `IconLoadingOutlineRegular`。
288
+ ### 0.5.15:内置目录回退 + 第三方预设
276
289
 
277
- **转圈为什么不动**:kit 里的 `IconLoadingOutlineRegular` 只是一个**静态字形**,图标库不负责动画。补法写在注入的样式表里:
290
+ **A. 目录回退(为什么必须先做)**:本页的模型一直只来自配置里的 `providers.<route>.models`。而用预设(或官方编辑器)建出来的内置供应商,profile 是**空对象**,模型在适配器的内置目录里 → 本页会显示「没有模型」。
278
291
 
279
- ```css
280
- @keyframes dsh-mo-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
281
- .dsh-mo-act-busy svg{animation:dsh-mo-spin .9s linear infinite}
282
- ```
292
+ 修法:选中一条 `models` 为空的 route 时,用 `discoverModels(namespace, { provider })` 读目录(目录供应商**零网络**),把结果作为**只读行**渲染(`page-model-inherited` + 一行提示),保留绿点与「测试连接」,不参与拖拽、不能打开详情弹窗;`catalogRows` 按 route 缓存,切回来不重复请求。
283
293
 
284
- (最初还有一行 `@media (prefers-reduced-motion: reduce){...animation:none}` 做无障碍降级,0.5.11 按需求删掉了。)
294
+ **B. 第三方预设**:新建弹窗加了「添加方式」两个选项(仅在存在可添加的预设时出现):
285
295
 
286
- 对应地,测试按钮在 `busy` 时带上 `dsh-mo-act-busy` 类;转圈作用在 `svg` 上(不是按钮盒子),所以按钮尺寸不变。
296
+ - 可添加预设 = `listConfigurableProviders()` 里 `declared === true` 且 `settingsPath` 非空、`settingsNs` 就是本命名空间、且 `providers[id]` 还没配置的条目(和官方那个下拉同源);
297
+ - 选预设 → 只需「显示名(可选)+ 密钥(可选)」;写盘是 **`providers.<id> = {}`**(有密钥时再加 `apiKeyEnv`),**不写 models** —— 地址/协议/模型由目录提供,这也是它不会撞上 `resolves no models` 的原因;
298
+ - 预设弹窗是**独立的第二个分支**(`data-mo-mode="catalog"`),共用一个 `addModeTabs()`;自定义那条分支原样不动,避免在同一段 JSX 里塞两套条件。
287
299
 
288
- **回归**:冒烟桩补了 `IconLinkOutlineRegular`;断言未测试的行图标是 `link`、点击后**同步**变成 `loading` 且带 `dsh-mo-act-busy` 类(然后才 await 结论)、结论是 `check`/`warn`。注意断言前必须 `tree = render()`:busy 是同步 setState,但树要重渲染才看得到。
300
+ **顺带**:`page.baseUrlRequired` 与 `page.modelsFieldHint` 里残留的「从网关获取」都改成「获取可用模型」(按钮改名时漏了这两处,实机截图发现的)。
289
301
 
290
- ### 0.5.10:按钮边框 + 「转圈看不见」的真因
302
+ **0.5.16**:「添加方式」那行改成**始终渲染**。之前在没有可添加预设(内置目录里的供应商都配置过了)时整行 `return null`,实机上看起来就是「这个功能不存在」。现在第三方那格变灰、`title` 说明原因,自定义始终可选。冒烟补了这条:预设耗尽时仍有两个模式选项、catalog 侧 `disabled` 且带说明、custom 侧可用。
291
303
 
292
- **边框**:两个 icon 按钮从 ghost 改回带边框的圆角矩形 —— `26×22`(r7,模型行)/ `22×17`(r6,供应商详情,高度等于开关),`1px solid var(--dsw-alias-border-l1)`,颜色仍由 `.dsh-mo-act-*` 控制。
304
+ **0.5.17:`declared` 的语义我弄反了**(实机反馈「第三方那格一直是灰的」)。目录条目里的 `declared` 是**「用户手工声明」**,不是「由目录声明」:
293
305
 
294
- **「还是不转圈」不是动画没写,而是它只存在几十毫秒**:本地网关 50ms 就答完,而 React 会把「置为 busy」和「置为结论」两次更新合并进同一帧 —— 于是屏幕上从来没有出现过转圈那一帧。
306
+ ```js
307
+ // dsh-llm-pi-ai/lib/index.js directoryEntries()
308
+ declared: installed === void 0 // 目录里没有这个 provider 才是 true
309
+ ```
295
310
 
296
- 修法:`TEST_SPIN_MS = 450`,`settle()` 把结果**至少推迟到 450ms 之后**再落到状态里(`mark` 立即设 busy,结果走 `setTimeout`)。代价是慢网关不受影响(本来就 >450ms),快网关多等不到半秒。
311
+ 所以「可添加预设」必须取 `declared === false` 的那批(目录自带、尚未配置),我写成 `!== true` → 恒为空。改成 `if (entry.declared === true) continue;` 后正常。**教训:字段名读起来像什么不重要,去读生成它的那行代码。**(本页原有的「官方目录 / 自定义供应商」分组其实一直是对的:`entry.declared === true ? "custom" : "catalog"`。)
297
312
 
298
- (0.5.11 起删掉了 `prefers-reduced-motion` 的降级:按需求**无论系统开没开「动画效果」都转**。)
313
+ **0.5.18:一个弹窗,两套内容**(实机反馈「又出来个弹窗」)。0.5.15 为了少改 JSX,把预设做成了**第二个 portal**(切换模式 = 换一个弹窗),视觉上就是「弹窗关掉又冒出一个」。现在合并成一个 `DialogShell`:`addDialog.mode` 只决定内容 —— 预设块用三元插在 `addModeTabs()` 之后,自定义那四个字段用 `addDialog.mode === "catalog" ? null : h(...)` 前缀挡住(前缀不影响数组元素结构),显示名/密钥两栏共用;提交按钮在 catalog 模式且未选预设时禁用。
299
314
 
300
- **回归**:断言按钮必须有 `borderWidth 1` / `borderStyle solid`;点击后同步进入 `busy`(`loading` 图标 + `dsh-mo-act-busy` 类),等 520ms 后必须落到非 busy 的结论。
315
+ **下拉「全白」**:原生 `<select>` 的弹出层跟系统主题走,深色界面里就是白底。补了 `.dsh-mo-select` 规则(深色底 + 前景色 + 边框),并在控件上写 `colorScheme: "dark"`,让 Chromium 用深色渲染那个 popup 与 `option` 列表。
316
+ **回归**:冒烟断言 —— 新建弹窗有两个模式选项;预设下拉列出未配置的 `kimi`;选它创建 = 一次 mutate、只设 `providers/kimi`、**不带 models**、弹窗关闭;`providers.bare = {}` 的路由渲染出目录里的只读模型行 + 提示,且没有可编辑行。
301
317
 
302
- ### 0.5.11:立刻开转、开没开都转
303
318
 
304
- **开没开都转**:删掉 `@media (prefers-reduced-motion: reduce)` 那条覆盖。注:这等于放弃无障碍降级——系统关掉「动画效果」时也会转,是产品取舍,不是疏漏。
319
+ ### 0.5.19:弹窗可拖动、点外面不关
305
320
 
306
- **立刻开转**:只靠 React 的 state 是不够的。`setState` 之后的那次 commit 不保证在**点击的同一帧**画出来,而请求可能在下一帧前就返回了;于是「转圈」可能一帧都没出现。做法是在点击回调里直接动 DOM(`startSpinNow`):
321
+ **可拖动**:`DialogShell` 自己持有一个 `offset {x,y}` 状态,卡片渲染成 `transform: translate(...)`;`onMouseDown` 挂在卡片上,但只认**带 `data-mo-drag` 的头部条**(`S.pickHead`),并且 `closest("button, a, input, select, textarea, [data-mo-no-drag]")` 命中时直接放弃 —— 所以点 ✕、点输入框都不会挪窗。拖动过程用 document 上的 `mousemove`/`mouseup` 捕获,鼠标移出卡片也不会断。
307
322
 
308
- ```js
309
- node.classList.add("dsh-mo-act-busy"); // 让 CSS 规则接手
310
- node.querySelector("svg").style.animation = "dsh-mo-spin .9s linear infinite"; // 当场起转
311
- ```
323
+ **点外面不关**:四个弹窗的遮罩层原来的 `onMouseDown: if (target === currentTarget) close()` 全部删掉。关窗只剩三条路:✕、取消、`Esc`(`Esc` 已经在 DialogShell 的 document 捕获里处理)。理由:这些弹窗里有填了一半的表单,误点空白直接丢弃代价太大;需要看被挡住的内容时,现在可以直接把窗拖开。
312
324
 
313
- 顺序是 `stopRowEvent → startSpinNow → testModel`:先起转,再发请求。之后 React 把图标换成 `IconLoadingOutlineRegular` 时,class 还在按钮上,CSS 继续驱动新的 svg;结论到达(≥450ms)后 class 被移除,动画停。
325
+ **回归**:冒烟断言 —— 遮罩层不再有 `onMouseDown`;每个弹窗的头部条唯一且 `cursor: "move"`;卡片带 `onMouseDown` 且初始 `transform: translate(0px, 0px)`。
314
326
 
315
- **回归**:点击时传一个假的 `currentTarget`(`classList.add` + `querySelector`),断言**同步**加了 `dsh-mo-act-busy` 且拿到了 `dsh-mo-spin` 的动画;再加上「520ms 后必须落到非 busy 结论」与「busy 图标是 loading」两条。
316
327
 
317
328
  ## 测试
318
329
 
@@ -324,14 +335,13 @@ node test/smoke.mjs # 或 npm test
324
335
 
325
336
  为什么值得留着:座位渲染时抛错会被**静默弃权**(官方 UI 悄悄回填),在浏览器里只看到「面板不见了」。这是提交前唯一能撞出这类错误的手段;改座位注册或页面结构后先跑它。
326
337
 
327
- 0.5.1–0.5.11 新增的覆盖面:
338
+ 0.5.1–0.5.12 新增的覆盖面:
328
339
 
329
340
  - **弹窗焦点**:卡片契约(`role=dialog` + `aria-modal` + `tabIndex -1`)、`data-mo-focus` 唯一、Tab/Shift+Tab **手动步进**与两端循环、焦点在卡片外时回拉、隐藏控件被跳过、Escape 关窗;
330
341
  - **新建供应商**:缺模型时**零写盘**并给文案、`从网关获取` 会填 id、一次 mutate 同时带 `models` 与 `apiKeyEnv`;
331
342
  - **删除**:供应商删除必须**先确认**(取消不写盘)、确认后 `unset providers.<route>` + 清派生凭据、模型删除写回剩余列表;
332
343
  - **测试连接**:请求必须带 `baseURL`、点击**同帧**起转(`dsh-mo-act-busy` + `dsh-mo-spin`)、busy 图标是 `loading`、≥450ms 后落到结论、结论把图标换成 `check`/`warn`;
333
- - **图标与布局**:图标必须来自 primitives 桩(断言 `data-icon`)、行内两键同一个盒子(26×22、`borderWidth 1`)。
334
-
344
+ - **图标与布局**:图标来自 primitives 桩(断言 `data-icon`);三颗图标键同一个盒子(19×17);删除键红框、测试连接键浅底;悬停规则、开关光环与基线对齐(`alignSelf: center`);悬停文案 ≤16 字、结论 ≤18 字。
335
345
 
336
346
  ### 0.4.0 验证范围
337
347
 
@@ -374,10 +384,16 @@ node test/smoke.mjs # 或 npm test
374
384
  ```sh
375
385
  # npm 不允许覆盖已发布的版本号:先升版本,再提交,最后发布
376
386
  npm version patch # 或手改 package.json 的 version
377
- git add -A && git commit -m "..." && git push
387
+ git add -A
388
+ git commit -m "..."
389
+ git push origin main
390
+ git tag v0.5.12 # 与 package.json 的 version 对齐
391
+ git push origin v0.5.12
378
392
  npm publish
379
393
  ```
380
394
 
395
+ 发布前先 `npm whoami`:`401 Unauthorized` 说明 token 已失效(例如重置过 2FA),重新登录或按下一段换 token 再发。
396
+
381
397
  发布需要带 **Bypass 2FA** 的 Granular Access Token(npm 2025-11 起只支持 granular token):npmjs.com → 头像 → Access Tokens → Generate New Token → Packages 选 **All Packages + Read and write (publish and stage)**、**Organizations 选 No access**、勾 **Bypass two-factor authentication**;然后 `npm config set //registry.npmjs.org/:_authToken=npm_xxx`(写进用户级 `.npmrc`,不要放进仓库)。
382
398
 
383
399
  包是 scoped 名(`@nimoxie/dsh-model-organizer`,用户作用域),`publishConfig.access: "public"` 已写进 package.json,`npm publish` 不用再带参数。老的无作用域名 `dsh-model-organizer` 已停更,记得发一次 `npm deprecate dsh-model-organizer "已迁移到 @nimoxie/dsh-model-organizer"` 留路标。
package/README.md CHANGED
@@ -1,94 +1,98 @@
1
- # dsh-model-organizer
2
-
3
- 给 DSH(DeepSeek Harness)换个更好用的模型选择器:**按供应商分组、可折叠、可拖拽排序**。
4
-
5
- ## 它解决什么问题
6
-
7
- 官方那个模型下拉是一个扁平长列表——供应商只是粘性小标题,模型多的时候要滚很久,也没法按自己的习惯排序。这个插件把它改成两级:**根层先选「模型」或「推理等级」,模型层里按供应商分组**,供应商和模型的顺序都能直接拖。
8
-
9
- ## 功能
10
-
11
- **输入框的模型菜单**
12
- - 根层与官方一致:「模型」「推理等级」两个入口——「推理等级」**常驻**,未配置等级的模型上置灰、不可点
13
- - 「模型」里保留本插件的改进:按供应商分组、可折叠,顺序与设置页浮窗一致,当前模型字体加粗高亮
14
- - 模型多于 4 个时,「模型」层顶部有官方同款的「搜索模型」框(0.2.0-rc.2 起官方才有此样式,旧版自动隐藏)
15
- - 外观完全沿用官方样式(同一个菜单表面、同一套图标,含官方的毛玻璃背景)
16
-
17
- **排序**
18
- - 在「设置 → 模型」右下角的浮窗里,**直接拖动**供应商行或模型行
19
- - 模型顺序通过官方设置接口持久化(跟着 DSH 的配置走,不存浏览器);供应商顺序只记在当前浏览器里
20
-
21
- **设置页:本插件自己的页面(接管「模型」这一行)**
22
- - 「设置 → 模型」这一行被本插件接管,点开就是本插件自己的页面(导航里仍然只有「模型」一行,本插件不会多占一格)。左栏最上是**只读的「官方(内置)」分组**——DeepSeek 官方那几条路由:悬停提示「官方路由 · 请到官方模型页编辑(点击前往)」,点一下会切回官方模型页(导航仍然是「模型」一行,配置不动)。下面是自己添加的供应商,按「官方目录 / 自定义供应商」分组,**都能拖动排序**(顺序只记在本机),行的悬停与抓握光标与设置页浮窗完全一致
23
- - 右栏编辑选中供应商:API 地址、API 格式(三种协议)即时保存;密钥粘贴后写入凭据库(页面读不回明文,可清除)
24
- - 模型列表的每一行是和供应商行同构的**一整行**(绿点 + 名称 + 参数摘要):**按住整行就能拖动排序**(手感和官方模型页右下角浮窗里的供应商行一致),**单击打开「模型详情」弹窗**,在里面改这个模型的全部配置——模型 ID、显示名、上下文窗口、最大输出 token、输入模态(文本 / 图片)、推理等级(继承 / 不支持 / 自定义)、是否启用,也可以删除该模型;**保存后立即写回 DSH 配置**(保存按钮只在弹窗里;页面底部那个旧按钮和行内输入框都没了)
25
- - 模型列表右上角有「**获取可用模型**」:调用官方发现接口(DSH 目录里已有的供应商不发网络请求;自己加的网关才会去问 `GET {baseURL}/models`),弹出「选择要添加的模型」——可搜索、可全选,**勾选后点「添加所选」才写回配置**,取消或关闭不影响现有配置
26
- - 左栏列表末尾有虚线框「**+ 添加模型提供商**」(与官方模型页同一形态):填路由 ID、显示名称、API 地址、API 格式、密钥、**至少一个模型 ID** 即可新建一条供应商路由;路由 ID 会做非空 / 字符集 / 重名校验,配置与密钥引用在**同一次写入**里落地
27
- - 模型 ID 是**必填**的:自己添加的路由不在 DSH 的目录里,配置里没有模型它就解析不出来(DSH 会直接报 "resolves no models")。填好 API 地址与密钥后可以点「**从网关获取**」,它会按你填的地址去问一次网关,把模型 ID 自动填进来;也可以手动输入,多个用逗号或空格分隔
28
- - 供应商详情右上角(启用开关左边)有一个**垃圾桶图标按钮**(和界面其它图标同一套素材,悬停/读屏为「删除供应商」):先弹确认,确认后移除这条自建路由的配置,并清理本页为它派生的密钥引用(别处配置的密钥不动);官方路由不提供这个按钮
29
- - 每条模型行右侧有两个**带圆角边框的图标按钮**:**🔗 测试连接** = 让宿主去问该供应商的模型列表,并检查这个模型 ID 在不在其中(自建路由会真的访问网关、用已保存的密钥;目录已描述的路由直接读目录)。点击后图标变成**转圈**,结果换成 **✓**(可达且在列表)/ **⚠**(可达但不在列表)/ **✗**(失败),颜色区分、悬停看原因,**图标与按钮尺寸始终不变**;**🗑** = 从配置里删掉这个模型(每条路由至少保留一个)
30
- - 每行左侧的**绿点就是启用开关**:亮=启用,暗=停用。**启用 = 这一项出现在本插件的模型菜单里**;停用只是把它从**本插件的模型选择菜单**里移除(聊天框的模型菜单不再列出),**DSH 配置原样保留、随时可以点回来**。供应商行和模型行是同一个语义;状态与拖拽顺序一样存在本机
31
- - **默认接管官方「模型」页**:设置里**永远只有「模型」一行**(我们那格在两种状态下都隐藏),点它打开的就是被接管的那一页。**没有开关,按钮即状态**:本页标题右侧的「官方模型页」= 把这一行交还官方页;官方页浮窗里的「模型管理页」= 重新接替。两个方向都不改变菜单,只切换这一行背后的页
32
- - **每次打开「模型」都是我们的页**:接管状态不写盘(装了插件就以本插件的页为准);「交还官方页」**只对这一次打开设置有效** —— 关掉设置再进来,又是本插件的页(重载/重启同理)。判定照官方条目的优先级**相对计算**(官方 −1 赢 / 官方 +1 交还),不写死数值
33
- - 两页互相能到:本页头部有「官方模型页」,官方模型页右下角的浮窗里有「模型管理页」(实现见 DEVELOPMENT.md 的 B3 一节)。浮窗的头部**始终渲染**:本插件能排序的供应商少于 2 个时只收起列表,返回本页的按钮不会跟着消失
34
- - 本插件不删改官方页面,只接管「模型」这一行的落点;导航里始终只有「模型」一行(做法见 [DEVELOPMENT.md](./DEVELOPMENT.md) 的 B2 / B3 两节)
35
-
36
- ## 安装
37
-
38
- `<profile>` 是你的目标环境:浏览器版用 `web`,桌面客户端用 `desktop`。
39
-
40
- ```sh
41
- dsh plugin --profile <profile> add @nimoxie/dsh-model-organizer
42
- ```
43
-
44
- - 浏览器版(`web`):装完重启服务(关掉再 `dsh web`)。
45
- - 桌面客户端(`desktop`):桌面 profile 由客户端独占管理,请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-model-organizer`)。
46
-
47
- 卸载(CLI 可管理的环境):
48
-
49
- ```sh
50
- dsh plugin --profile <profile> remove @nimoxie/dsh-model-organizer
51
- ```
52
-
53
- 也可以从 GitHub 或本地目录装(开发用):
54
-
55
- ```sh
56
- dsh plugin --profile <profile> add github:NimoXie15/dsh-model-organizer
57
- dsh plugin --profile <profile> add link:/absolute/path/to/dsh-model-organizer
58
- ```
59
-
60
- > **兼容性**:已在 DSH `0.1.7`、`0.2.0-rc.1`(web)、`0.2.0-rc.2`、`0.2.1-rc.2`(桌面端)上核验,`0.1.5` 起都可用。
61
- >
62
- > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
63
-
64
- ## 想让「推理等级」可用,需要模型自己声明
65
-
66
- 「推理等级」入口常驻菜单根层;只有模型声明了等级,它才可点、才会在按钮上显示当前等级。
67
-
68
- 0.5.0 起在「模型详情」弹窗里直接设置:推理等级选「继承」(沿用该模型已有的声明)、「不支持」,或「自定义」——自定义时逐行添加等级,**键 = 菜单里显示的等级,值 = 选中时发给模型的 API 参数**(只有 `off` 可以留空)。
69
-
70
- 也可以直接在 profile 配置里给这个模型声明(0.2.x 在 `cordis.patch.yml`,0.1.x 在 `settings.yaml`):
71
-
72
- ```yaml
73
- # 该供应商的 models[] 条目(0.2.x 在 profile 的 cordis.patch.yml;0.1.x 在 settings.yaml)
74
- - id: <你的模型 ID>
75
- reasoningEfforts: # 键=菜单里显示的等级,值=选中时发给模型的 API 参数;只有 off 可以留空
76
- off:
77
- high: high
78
- ```
79
-
80
- ## 已知限制
81
-
1
+ # dsh-model-organizer
2
+
3
+ 给 DSH(DeepSeek Harness)换个更好用的模型选择器:**按供应商分组、可折叠、可拖拽排序**。
4
+
5
+ ## 它解决什么问题
6
+
7
+ 官方那个模型下拉是一个扁平长列表——供应商只是粘性小标题,模型多的时候要滚很久,也没法按自己的习惯排序。这个插件把它改成两级:**根层先选「模型」或「推理等级」,模型层里按供应商分组**,供应商和模型的顺序都能直接拖。
8
+
9
+ ## 功能
10
+
11
+ ### 聊天框的模型菜单
12
+ - 根层与官方一致:「模型」「推理等级」两个入口(「推理等级」常驻,模型未声明等级时置灰)
13
+ - 「模型」按供应商分组、可折叠,顺序与设置页浮窗一致;当前模型加粗
14
+ - 模型多于 4 个时出现官方同款的「搜索模型」框
15
+ - 外观完全沿用官方菜单(同一表面、同一套图标、同一毛玻璃背景)
16
+
17
+ ### 设置页「模型」:本插件接管
18
+ - 设置里**始终只有「模型」一行**,点开就是本插件的页;每次打开设置都默认回到本页
19
+ - 交还官方页:本页标题右侧的「官方模型页」= 这一次打开期间交还;官方页右下角浮窗里的「模型管理页」= 再接管回来
20
+ - 左栏:置顶只读的「官方(内置)」路由(点击切回官方页)+ 自建供应商(分「官方目录 / 自定义供应商」,可拖动排序,顺序只记本机)。内置目录里的供应商**没有在配置里列模型**,它们的模型由目录提供,所以在这一页是**只读展示**(可测试连接,不能编辑)
21
+ - 右栏:API 地址、API 格式(三种协议)改动即时保存;密钥写入凭据库(读不回明文,可清除)
22
+ - 模型列表一行一个:**整行可拖动排序**,**单击打开「模型详情」**——ID、显示名、上下文窗口、最大输出、输入模态、推理等级、启用/删除;保存立即写回 DSH 配置
23
+ - 「获取可用模型」:调官方发现接口(目录里已有的供应商不联网,自己加的网关才问 `GET {baseURL}/models`),弹窗可搜索、全选,**勾选后点「添加所选」才写回**
24
+ - 左栏末尾虚线框「**+ 添加模型提供商**」(同一个弹窗里切换内容):顶部「**添加方式**」两种 —— **第三方模型提供商**(从 DSH 内置目录里挑一个,只填密钥就够,地址/协议/模型都由目录提供;如果目录里的供应商都已经添加过,这格会置灰并说明)或 **自定义模型 API**(路由 ID、显示名、地址、格式、密钥,再选出一份模型清单);路由 ID 校验非空/字符集/重名,配置与密钥引用一次写入
25
+ - 删除供应商:详情右上角垃圾桶,先确认;删配置并清理本页派生的密钥引用(别处配置的密钥不动),官方路由没有这个按钮
26
+ - 每行的**绿点就是启用开关**:亮=出现在本插件菜单,暗=从本插件菜单移除;**DSH 配置原样保留**,状态存本机
27
+
28
+ ### 排序、按钮与细节
29
+ - 设置页右下角浮窗:**直接拖动**供应商行/模型行排序;模型顺序写进 DSH 配置(同一份配置的所有设备共用),供应商顺序与浮窗位置只记在当前浏览器
30
+ - 页面上所有按钮(含所有弹窗)都有**悬停反馈**:普通按钮提亮、主要操作保持强调色、危险操作泛红、开关用光环(不改开/关底色,避免被误读为状态变化)
31
+ - 所有弹窗(选模型、添加提供商、模型详情、删除确认)都能**按住标题栏拖动**;**点弹窗外的空白不会关闭**,关闭方式是右上角 ✕、底部「取消」或 `Esc`(避免误点把填了一半的表单扔掉)
32
+ - 模型行两颗**带边框的图标键**:链接图标 = **测试连接**(问网关该模型在不在它的列表里,结论把图标换成 ✓/⚠/✗ 并变色,悬停看原因;点击同帧开始转圈)、垃圾桶 = **删除**(图标常红);两者同一尺寸,转圈与尺寸都不随结论变化
33
+ - 悬停文案只说明「这个按钮做什么 / 现在什么状态」,不写实现细节
34
+
35
+ ## 安装
36
+
37
+ 装到哪个环境,就用对应的方法:
38
+
39
+ **浏览器版(`web`,CLI 管理)**
40
+
41
+ ```sh
42
+ dsh plugin --profile web add @nimoxie/dsh-model-organizer
43
+ ```
44
+
45
+ 装完重启服务(关掉再 `dsh web`)。`--profile` 后面是目标 profile 名:浏览器版是 `web`,你自己另外创建的 profile 填对应的名字。
46
+
47
+ **桌面客户端**
48
+
49
+ 桌面 profile 由客户端独占管理,**不要用上面的 CLI 命令**。请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-model-organizer`)。
50
+
51
+ 卸载(CLI 环境):
52
+
53
+ ```sh
54
+ dsh plugin --profile <profile> remove @nimoxie/dsh-model-organizer
55
+ ```
56
+
57
+ 开发用:从 GitHub 或本地目录装
58
+
59
+ ```sh
60
+ dsh plugin --profile <profile> add github:NimoXie15/dsh-model-organizer
61
+ dsh plugin --profile <profile> add link:/absolute/path/to/dsh-model-organizer
62
+ ```
63
+
64
+ > **兼容性**:已在 DSH `0.1.7`、`0.2.0-rc.1`(web)、`0.2.0-rc.2`、`0.2.1-rc.2`(桌面端)上核验,`0.1.5` 起都可用。
65
+ >
66
+ > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
67
+
68
+ ## 想让「推理等级」可用,需要模型自己声明
69
+
70
+ 「推理等级」入口常驻菜单根层;只有模型声明了等级,它才可点、才会在按钮上显示当前等级。
71
+
72
+ 0.5.0 起在「模型详情」弹窗里直接设置:推理等级选「继承」(沿用该模型已有的声明)、「不支持」,或「自定义」——自定义时逐行添加等级,**键 = 菜单里显示的等级,值 = 选中时发给模型的 API 参数**(只有 `off` 可以留空)。
73
+
74
+ 也可以直接在 profile 配置里给这个模型声明(0.2.x 在 `cordis.patch.yml`,0.1.x 在 `settings.yaml`):
75
+
76
+ ```yaml
77
+ # 该供应商的 models[] 条目(0.2.x 在 profile 的 cordis.patch.yml;0.1.x 在 settings.yaml)
78
+ - id: <你的模型 ID>
79
+ reasoningEfforts: # 键=菜单里显示的等级,值=选中时发给模型的 API 参数;只有 off 可以留空
80
+ off:
81
+ high: high
82
+ ```
83
+
84
+ ## 已知限制
85
+
82
86
  - 「测试连接」验证的是**地址 / 密钥 / 网关可达性,以及这个模型 ID 是否在网关返回的列表里** —— 它不会真的发一条消息给模型(DSH 的远程接口没有这个能力,客户端也读不回密钥)。所以「✓」只代表这条路由和这个 ID 是通的,不等于这个模型一定能补全
83
- - 设置页右下角的排序浮窗**只列你自己添加的供应商**(写进配置的那些),官方路由**根本不进这个浮窗**;想在左栏看到官方路由,看顶部只读的「官方(内置)」分组
84
- - 浮窗里排出的供应商顺序会**镜像**到官方模型页的卡片上;官方卡片(DeepSeek 官方等)被刻意保持在最前面
85
- - 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在 DSH 的用户配置里,同一份 DSH 的所有设备共用——两台机器各装一份 DSH 则互不相通
86
- - 不修改 DSH 本体,只通过官方提供的扩展点接入
87
-
88
- ## 许可
89
-
90
- MIT
91
-
92
- ---
93
-
94
- 想自己改这个插件、或者想了解它内部怎么实现的,见 [DEVELOPMENT.md](./DEVELOPMENT.md)。
87
+ - 设置页右下角的排序浮窗**只列你自己添加的供应商**(写进配置的那些),官方路由**根本不进这个浮窗**;想在左栏看到官方路由,看顶部只读的「官方(内置)」分组
88
+ - 浮窗里排出的供应商顺序会**镜像**到官方模型页的卡片上;官方卡片(DeepSeek 官方等)被刻意保持在最前面
89
+ - 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在 DSH 的用户配置里,同一份 DSH 配置的所有设备共用——两台机器各装一份 DSH 则互不相通
90
+ - 不修改 DSH 本体,只通过官方提供的扩展点接入
91
+
92
+ ## 许可
93
+
94
+ MIT
95
+
96
+ ---
97
+
98
+ 想自己改这个插件、或者想了解它内部怎么实现的,见 [DEVELOPMENT.md](./DEVELOPMENT.md)。