@nimoxie/dsh-model-organizer 0.5.0 → 0.5.12

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,21 +133,135 @@ 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)
140
139
 
141
- ## 已知的非插件告警:permission preset "auto"
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`,否则只变底色看不出来。
158
+
159
+ ## 已知的非插件告警:permission preset "auto"(会连带「权限选择消失」和「工具通道断」)
142
160
 
143
161
  ```
144
162
  dsh: warning: 1 entry did not activate permission (@deepseek-ai/dsh-permission-presets):
145
163
  Error: permission: cannot restore preset "auto" without its active integration
146
164
  ```
147
165
 
148
- 来自官方 `dsh-permission-presets`,**与本插件无关**(本插件不写 permission 命名空间)。发布一个会话时 `pinInitialPermission()` 看到该会话的持久化事实是 `permission/preset = auto`,就要求 `autoAdmit` 存在 —— 它只能由 Auto 集成调用 `registerAuto()` 注册;当前 profile 的 bundles 里没有提供该集成的条目,于是抛错、该条目激活失败,被 loader 记成一条 warning。
166
+ 与本插件无关(本插件只写 `llm-pi-ai`,不碰 permission 命名空间)。它其实是**一个循环等待**,然后在两处现形。
167
+
168
+ **循环**:
169
+
170
+ 1. `dsh-permission-presets/lib/index.js` 的 `pinInitialPermission()`(L370-376)publish 会话时,若该会话的持久化事实是 `permissions.preset = "auto"`,就要求 `autoAdmit` 已存在,否则抛错;
171
+ 2. `autoAdmit` 唯一的注册点是 `registerAuto()`,唯一调用者是 `@deepseek-ai/dsh-experimental-auto-review`(其 `lib/index.js` L489);
172
+ 3. 但 auto-review 自己 `inject: ["permissionPresets"]` —— 正是第 1 步那个条目要提供的服务。所以它**永远不可能先激活**:permission-presets 抛错 → `permissionPresets` 不注册 → auto-review 起不来 → `autoAdmit` 永远没有。**同一轮运行内不会自愈**;重启看着"好了",只是因为新启动那一刻还没有 auto 会话被 publish,一旦又 resume 到它立刻复发。
173
+
174
+ **两张脸**:
175
+
176
+ - **对话栏的权限选择消失**:客户端座位 `conversation.input.permission`(`dsh-client-ui-permission-presets/lib/client.js`)里 `if (selection === void 0 || catalog === null) return null;` —— catalog 来自那条已经死掉的服务;
177
+ - **工具通道断**(`cannot get required service "sessionProjections" in inactive context`):cordis 的上下文代理(`cordis/lib/index.js` 的 `ReflectService.handler.get`,约 L688)在"某个 fiber 把该服务写进 `inject`、而整条 fiber 链上都找不到它"时抛这句话。`sessionProjections` 是**受害者不是凶手**(连 permission-presets 自己也 inject 它,全库约 30 个条目注入),服务图被挖洞后第一个被解析到的就是它。
178
+
179
+ **触发面**:`<DSH_HOME>/storages/session_projcache/sessions/*.json` 每个会话的 `record.rows.permissions.val`。实测 42 个缓存里 32 个带该行,其中 **23 个是 `preset: "auto"`** —— 所以不是偶发。
180
+
181
+ **处置**(缓存那条最稳,因为权限选择消失时 UI 里改不了):
182
+
183
+ 1. 彻底退出 DSH(不是关窗口,运行中它会把内存里的旧值写回去);
184
+ 2. 把那些缓存的 `record.rows.permissions.val` 置为 `null`(等于"没有持久化权限事实",下次打开该会话会按当前默认预设重新 pin),每个文件先备份 `.bak`;
185
+ 3. 重开客户端 → 权限选择回来、工具通道恢复、warning 消失。
186
+
187
+ 备选:按会话把权限预设从 auto 改掉,或直接删掉那几个会话。真正要修的是 DSH:让 auto-review 不再 `inject permissionPresets`,或者 pin 失败时降级而不是让整个条目激活失败 —— 插件侧改不了。
188
+
189
+ ### 0.5.2:弹窗的焦点陷阱(第一版没修好,第二版才修对)
190
+
191
+ **症状**:在「添加模型提供商」弹窗里按 Tab,焦点跳到弹窗背后的设置页。
192
+
193
+ **原因**:每个弹窗都用 `ReactDOM.createPortal` 挂到 `document.body`,在文档顺序里排在整页**之后**。两张脸:① Tab 从卡片最后一个控件继续往前走,浏览器绕回文档开头(也就是设置页的导航);② 打开瞬间焦点没落进弹窗(宿主壳自己的 focus 流程会把刚插入的 portal 的 `focus()` 抢走),于是第一次 Tab 直接从页面上的按钮继续走。
194
+
195
+ **0.5.1 为什么没修好**:第一版只在卡片上挂 `onKeyDown`,且**只在边界**(第一个/最后一个)才 `preventDefault`。中间步骤仍交给浏览器自己走;只要焦点不在卡片里(情形②),或者边界判定没命中,浏览器就照旧走到页面上去。**教训:不能让浏览器"帮我们走一步",必须整段接管。**
196
+
197
+ **0.5.2 的做法**(`DialogShell`,在 `ModelsPage` 之前):
198
+
199
+ 1. 卡片契约:`role="dialog"` + `aria-modal="true"` + `tabIndex:-1`(空弹窗的焦点兜底);
200
+ 2. **整段接管 Tab**:挂载时在 `document` 的**捕获阶段**监听 `keydown`,Tab/Shift+Tab 一律 `preventDefault` + `stopPropagation`,然后由 `trapTab()` 自己按卡片内的可聚焦列表前后移动并两端循环 —— 宿主壳(以及任何别的插件)的 keydown 根本看不到这个事件;
201
+ 3. **focusin 兜底**:捕获阶段监听 `focusin`,只要有焦点落到卡片之外(别的插件抢焦点、程序化 blur、宿主壳的 focus 管理),立刻拉回卡片第一个控件;
202
+ 4. **首帧重试**:挂载时立刻聚焦 `[data-mo-focus]`(添加供应商 → 路由 ID;选模型 → 搜索框;模型详情 → 模型 ID),再用 `requestAnimationFrame` 检查一次"焦点是否真的在卡片里",不在就再聚焦一次;
203
+ 5. `Escape` 在同一层关窗;卸载时把焦点还给打开它的元素(`isConnected` 为假就不还)。
204
+
205
+ `focusableIn()`(经 `__test` 暴露)要跳过浏览器根本聚焦不到的元素(`getClientRects().length === 0`),并且容忍 `null` 根与没有 `querySelectorAll` 的假 DOM——否则无头测试会直接抛。
206
+
207
+ **回归**:`test/smoke.mjs` 断言卡片契约、`data-mo-focus` 唯一性、Tab/Shift+Tab 手动前后步进与两端循环、焦点在卡片外时回拉、隐藏控件被跳过、Escape 关窗;并做过「把 Tab 分支改成直接 return → 冒烟必须失败」的反向对照。
208
+
209
+ **线上验证**:`window.__dshModelOrganizerBuild` 应为 `b56`;打开添加供应商弹窗后焦点直接在「路由 ID」,反复 Tab 只在弹窗内循环。
210
+
149
211
 
150
- 处置:**新建会话**(新会话用当前默认预设,不会去 restore auto),或在权限预设里把该会话切成非 auto;要保留 auto 就得装回提供 Auto 的集成(`@deepseek-ai/dsh-experimental-auto-review` 不是它)。
212
+ ### 0.5.3:新建供应商必须有模型
213
+
214
+ **症状**:新建供应商后宿主报 `llm-pi-ai: provider "X" resolves no models; the installed catalog does not describe this route, so its models must be listed in configuration`。
215
+
216
+ **原因**(DSH 侧硬规则,`dsh-llm-pi-ai/lib/index.js` 的 `resolveRouteModels()`):`entries = configured.length > 0 ? configured : [...catalog]`,然后 `if (entries.length === 0) invalid(provider, "resolves no models…")`。也就是说 **hand-declared 路由(目录里没有的)必须在配置里列出至少一个模型**,否则这条路由解析不出模型。我们原来的「+ 添加模型提供商」只写 `api/baseURL/displayName/apiKeyEnv`,正好踩中。
217
+
218
+ **做法**:
219
+
220
+ 1. 新建对话框新增必填的「模型 ID」输入(逗号/空格/换行分隔,`parseModelIds()` 去空去重),与 profile、密钥引用在**同一次 mutate** 里写入(`models: [{ id }]`,只写 id,不臆造字段);
221
+ 2. 同一对话框加「从网关获取」:用对话框里的草稿(`baseURL` + `api` + 一次性 `apiKey`)调 `remote.llm.discoverModels` —— 这条缝本来就接受"还不存在的路由"的草稿,所以能在创建前把 id 填进来;失败原样显示宿主给的原因;
222
+ 3. `writeModels()` 增加护栏:**不允许把某条路由的模型删空**(否则又回到 "resolves no models"),拒绝时给出同一条文案;
223
+ 4. 已有那种"空模型"路由的修法:打开该供应商 → 「获取可用模型」或「+ 添加模型」→ 填至少一个 ID → 保存。
224
+
225
+ **回归**:冒烟测试覆盖「不带模型提交 → 零写盘 + 明确文案」「从网关获取会填 id」「带 `glm-4.6, qwen-max` 创建 → 同一次 mutate 里 `models` 为两项」。
226
+
227
+ ### 0.5.4:行内「测试 / 删除」与删除供应商
228
+
229
+ **新增操作**:
230
+
231
+ 1. 供应商详情右上角、启用开关左侧:**删除供应商**(`page-provider-delete`)。走确认弹窗(`provider-delete-dialog`),确认后一次 `unset providers.<route>`,并先清掉该 profile 命名的凭据(`apiKeyEnv`,没有就用 `deriveKeyRef(route)`);同时把本机顺序表与「停用」表里的这条路由一起清掉。**官方路由不渲染这个按钮**(它们不归本页编辑)。
232
+ 2. 每条模型行右侧:**测试**(`page-model-test`)与**删除**(`page-model-delete`)。删除 = 过滤后一次写 `models`,仍然受「不允许删空」护栏约束。
233
+
234
+ **「测试连接」到底测了什么**(重要,别把它说成"发一条消息给模型"):宿主 `llm` 的远程接口只有 `listProviders / listConfigurableProviders / discoverModels`,**没有**"发送一次请求"的能力,客户端也读不回密钥(凭据只写),所以做不到真正的补全探测。这里的做法是复用 `discoverModels(namespace, { provider, baseURL, api })`(0.5.4 只发了 `provider`,导致全线失败,见 0.5.5):
235
+
236
+ - 自建(目录里没有的)路由:宿主会真的 `GET {baseURL}/models`,带已保存的凭据 → **这一步验证了地址、协议、密钥和网关可达性**;
237
+ - 目录已描述的路由:适配器直接回目录,不发网络请求;
238
+ - 拿到列表后检查该行 id 是否在列表中,结论落在按钮上(`data-mo-state` = `idle | busy | ok | miss | fail`),完整原因放在按钮的 `title` 里。
239
+
240
+ 也就是说:**「✓ 可用」= 网关可达且列表里有这个 ID**,不代表这个 ID 真的能补全;「⚠ 不在列表」= 网关可达但没列出该 ID(多半是拼错或网关没暴露);「✗ 失败」= 连不上/401/协议不支持(原因见 tooltip)。要做真正的补全探测,得在宿主侧加一个"试跑一次"的接口。
241
+
242
+ **已知取舍**:行是 `draggable` 的,行内按钮用 `stopPropagation` 阻止冒泡到行的"打开详情",但无法完全阻止 HTML5 从祖先元素发起拖拽(按钮很小,误拖代价也只是一次排序)。
243
+
244
+ ### 0.5.5:测试连接「全都失败」的真因 + 垃圾桶按钮
245
+
246
+ **症状**:每个模型的「测试」都失败(红色 ✗),包括明明能正常对话的供应商。
247
+
248
+ **真因**:`testModel()` 只发了 `{ provider }`,没带 `baseURL` / `api`。而 `dsh-llm-pi-ai` 的 `discoverModels()` 顺序是:
249
+
250
+ 1. 若 provider 在安装目录里 → 直接回目录(不发网络请求);
251
+ 2. 否则 `if (request.baseURL === void 0 || request.baseURL.length === 0) throw "pi-ai ships no catalog for provider X, so its models can only be listed by hand"`。
252
+
253
+ 也就是说:**目录里没有的自建路由(本插件的用户几乎全是这种)必须带 `baseURL`**,否则宿主直接拒绝 —— 不是网络不通,是参数不全。`fetchModels()` 一直带着它,所以「获取可用模型」是好的,只有新加的「测试」漏了。
254
+
255
+ **修法**:测试请求与 `fetchModels()` 对齐 —— `{ provider, baseURL, api }`(优先取详情面板里正在编辑的值,其次取已保存的 profile);并把 `ships no catalog` 这条宿主原文映射成一句可操作的中文(先在 API 地址里填上地址再测)。
256
+
257
+ **教训**:凡是「问宿主网关」的调用,参数要和已经跑通的那条路径逐项对一遍;冒烟测试里固定断言 `request.baseURL`,防止再退化。
258
+
259
+ ### 0.5.6–0.5.11:行内图标键的四次返工(教训)
260
+
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 的真因);凡是问宿主的调用,参数与已经跑通的那条路径逐项对齐。
151
265
 
152
266
  ## 测试
153
267
 
@@ -159,6 +273,13 @@ node test/smoke.mjs # 或 npm test
159
273
 
160
274
  为什么值得留着:座位渲染时抛错会被**静默弃权**(官方 UI 悄悄回填),在浏览器里只看到「面板不见了」。这是提交前唯一能撞出这类错误的手段;改座位注册或页面结构后先跑它。
161
275
 
276
+ 0.5.1–0.5.12 新增的覆盖面:
277
+
278
+ - **弹窗焦点**:卡片契约(`role=dialog` + `aria-modal` + `tabIndex -1`)、`data-mo-focus` 唯一、Tab/Shift+Tab **手动步进**与两端循环、焦点在卡片外时回拉、隐藏控件被跳过、Escape 关窗;
279
+ - **新建供应商**:缺模型时**零写盘**并给文案、`从网关获取` 会填 id、一次 mutate 同时带 `models` 与 `apiKeyEnv`;
280
+ - **删除**:供应商删除必须**先确认**(取消不写盘)、确认后 `unset providers.<route>` + 清派生凭据、模型删除写回剩余列表;
281
+ - **测试连接**:请求必须带 `baseURL`、点击**同帧**起转(`dsh-mo-act-busy` + `dsh-mo-spin`)、busy 图标是 `loading`、≥450ms 后落到结论、结论把图标换成 `check`/`warn`;
282
+ - **图标与布局**:图标来自 primitives 桩(断言 `data-icon`);三颗图标键同一个盒子(19×17);删除键红框、测速键浅底;悬停规则、开关光环与基线对齐(`alignSelf: center`);悬停文案 ≤16 字、结论 ≤18 字。
162
283
 
163
284
  ### 0.4.0 验证范围
164
285
 
@@ -186,13 +307,13 @@ node test/smoke.mjs # 或 npm test
186
307
 
187
308
  调研时倾向的「portal 进官方内容列」**没有被采用**:接管靠优先级换格,因此不依赖官方 DOM 结构,也不用往官方内容列里插节点;代价是本插件自己那一格要常驻隐藏(B2 里的双向自愈)。
188
309
 
189
- ### 设置页还没对齐截图的部分(按实施顺序)
310
+ ### 截图里剩下的差异(逐条结案)
190
311
 
191
- 1. **行图标**:供应商行左侧图标。必须先从 primitives 的导出表核对真实名字(`IconXxxOutlineRegular` 形状),猜名会静默返回 null。
192
- 2. **每模型行的操作图标**(0.5.0 已定):模型行改成整行单击开「模型详情」弹窗,行上不再有「链接 / 铅笔 / 垃圾桶」三枚图标,删除收进弹窗。
193
- 3. **每模型 / 每供应商开关**(0.5.0 已定):pi-ai 的 `modelFields` 与 profile 都**没有** enabled/disabled 字段,所以开关落在插件侧存储(`dsh-model-organizer.hiddenProviders` / `.hiddenModels`),语义只有「从本插件的菜单里移除」;表现统一成每行左侧的绿点。
194
- 4. **顶部「+ 添加供应商」**:新建路由要同时写 profile(baseURL/api)与凭据(`deriveKeyRef`),还要处理路由重名与 `settings/conflict` 重试。
195
- 5. ~~**顶部右侧「切回官方页」开关**~~(已做):本页页头右侧的「官方模型页」按钮就是它,动作是 `setTakeover(false)`(只对这一次打开设置有效)。
312
+ 1. **供应商行左侧的行图标 —— 已否决,不做**。当初的设想是给每条供应商行加一个**供应商 logo / 品牌图标**帮助辨识;结论是不做:这些图标只能按路由名去猜,猜不到就是随机图形,反而比纯文字更乱,而行首的绿点已经承担了"启用/停用"这个唯一有意义的视觉信号。(若哪天真要做,名字表是现成的:0.5.8 核对过 primitives 的 98 个图标,取不到时 `icon()` 返回 null、`kitIcon()` 有文字兜底。)
313
+ 2. **每模型行的操作图标 —— 已定(0.5.0)**:模型行改成整行单击开「模型详情」弹窗,行上不再挂「链接 / 铅笔 / 垃圾桶」三枚图标,删除收进弹窗。(0.5.4 之后行尾只保留「测试连接 + 删除」两枚,理由是这两个是高频动作。)
314
+ 3. **每模型 / 每供应商开关 —— 已定(0.5.0)**:pi-ai 的 `modelFields` 与 profile 都**没有** enabled/disabled 字段,所以开关落在插件侧存储(`dsh-model-organizer.hiddenProviders` / `.hiddenModels`),语义只有「从本插件的菜单里移除」;表现统一成每行左侧的绿点。
315
+ 4. **顶部「+ 添加供应商」—— 已做(0.5.0 起,0.5.3 补上模型必填)**:一次 mutate 同时写 profile(baseURL/api/models)与凭据引用(`deriveKeyRef`),路由重名与字符集在写之前拦;模型 ID 必填(宿主对目录外的路由硬要求),可「从网关获取」自动填。
316
+ 5. **顶部右侧「切回官方页」开关 —— 已做**:本页页头右侧的「官方模型页」按钮就是它,动作是 `setTakeover(false)`(只对这一次打开设置有效)。
196
317
 
197
318
  已对齐截图的部分:两栏主从布局、供应商分组(官方目录 / 自定义供应商,来自 `remote.llm.listConfigurableProviders`)、API 地址 / API 格式 / API 密钥三行、模型列表的增删改与拖拽、浮窗同款的悬停与抓握光标、模型列表标题右侧的「获取可用模型」(`remote.llm.discoverModels`)—— 交互与官方一致:**弹窗勾选**,只列配置里还没有的模型、默认全选、可搜索与全选/取消全选,**取消/关闭都不写配置**,只有「添加所选」才落盘,以及**模型显示名**的处理(0.4.0 不写 `name` 字段、宿主回退到 id;0.5.0 起在「模型详情」弹窗里可以直接填显示名,保存时写进该模型的 `name`)。
198
319
 
@@ -201,10 +322,16 @@ node test/smoke.mjs # 或 npm test
201
322
  ```sh
202
323
  # npm 不允许覆盖已发布的版本号:先升版本,再提交,最后发布
203
324
  npm version patch # 或手改 package.json 的 version
204
- git add -A && git commit -m "..." && git push
325
+ git add -A
326
+ git commit -m "..."
327
+ git push origin main
328
+ git tag v0.5.12 # 与 package.json 的 version 对齐
329
+ git push origin v0.5.12
205
330
  npm publish
206
331
  ```
207
332
 
333
+ 发布前先 `npm whoami`:`401 Unauthorized` 说明 token 已失效(例如重置过 2FA),重新登录或按下一段换 token 再发。
334
+
208
335
  发布需要带 **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`,不要放进仓库)。
209
336
 
210
337
  包是 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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 NimoXie
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NimoXie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -8,46 +8,52 @@
8
8
 
9
9
  ## 功能
10
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
- - 模型列表右上角有「**获取可用模型**」:调用官方发现接口(catalog 供应商零网络直出,网关走 `GET {baseURL}/models`),弹出「选择要添加的模型」——可搜索、可全选,**勾选后点「添加所选」才写回配置**,取消或关闭不影响现有配置
26
- - 左栏列表末尾有虚线框「**+ 添加模型提供商**」(与官方模型页同一形态):填路由 ID、显示名称、API 地址、API 格式、密钥即可新建一条供应商路由;路由 ID 会做非空 / 字符集 / 重名校验,配置与密钥引用在**同一次写入**里落地
27
- - 每行左侧的**绿点就是启用开关**:亮=启用,暗=停用。**启用 = 这一项出现在本插件的模型菜单里**;停用只是把它从**本插件的模型选择菜单**里移除(聊天框的模型菜单不再列出),**DSH 配置原样保留、随时可以点回来**。供应商行和模型行是同一个语义;状态与拖拽顺序一样存在本机
28
- - **默认接管官方「模型」页**:设置里**永远只有「模型」一行**(我们那格在两种状态下都隐藏),点它打开的就是被接管的那一页。**没有开关,按钮即状态**:本页标题右侧的「官方模型页」= 把这一行交还官方页;官方页浮窗里的「模型管理页」= 重新接替。两个方向都不改变菜单,只切换这一行背后的页
29
- - **每次打开「模型」都是我们的页**:接管状态不写盘(装了插件就以本插件的页为准);「交还官方页」**只对这一次打开设置有效** —— 关掉设置再进来,又是本插件的页(重载/重启同理)。判定照官方条目的优先级**相对计算**(官方 −1 赢 / 官方 +1 交还),不写死数值
30
- - 两页互相能到:本页头部有「官方模型页」,官方模型页右下角的浮窗里有「模型管理页」(实现见 DEVELOPMENT.md 的 B3 一节)。浮窗的头部**始终渲染**:本插件能排序的供应商少于 2 个时只收起列表,返回本页的按钮不会跟着消失
31
- - 本插件不删改官方页面,只接管「模型」这一行的落点;导航里始终只有「模型」一行(做法见 [DEVELOPMENT.md](./DEVELOPMENT.md) 的 B2 / B3 两节)
11
+ ### 聊天框的模型菜单
12
+ - 根层与官方一致:「模型」「推理等级」两个入口(「推理等级」常驻,模型未声明等级时置灰)
13
+ - 「模型」按供应商分组、可折叠,顺序与设置页浮窗一致;当前模型加粗
14
+ - 模型多于 4 个时出现官方同款的「搜索模型」框
15
+ - 外观完全沿用官方菜单(同一表面、同一套图标、同一毛玻璃背景)
16
+
17
+ ### 设置页「模型」:本插件接管
18
+ - 设置里**始终只有「模型」一行**,点开就是本插件的页;每次打开设置都默认回到本页
19
+ - 交还官方页:本页标题右侧的「官方模型页」= 这一次打开期间交还;官方页右下角浮窗里的「模型管理页」= 再接管回来
20
+ - 左栏:置顶只读的「官方(内置)」路由(点击切回官方页)+ 自建供应商(分「官方目录 / 自定义供应商」,可拖动排序,顺序只记本机)
21
+ - 右栏:API 地址、API 格式(三种协议)改动即时保存;密钥写入凭据库(读不回明文,可清除)
22
+ - 模型列表一行一个:**整行可拖动排序**,**单击打开「模型详情」**——ID、显示名、上下文窗口、最大输出、输入模态、推理等级、启用/删除;保存立即写回 DSH 配置
23
+ - 「获取可用模型」:调官方发现接口(目录里已有的供应商不联网,自己加的网关才问 `GET {baseURL}/models`),弹窗可搜索、全选,**勾选后点「添加所选」才写回**
24
+ - 左栏末尾虚线框「**+ 添加模型提供商**」:路由 ID、显示名、API 地址、格式、密钥、**至少一个模型 ID**;ID 校验非空/字符集/重名,配置与密钥引用一次写入
25
+ - 删除供应商:详情右上角垃圾桶,先确认;删配置并清理本页派生的密钥引用(别处配置的密钥不动),官方路由没有这个按钮
26
+ - 每行的**绿点就是启用开关**:亮=出现在本插件菜单,暗=从本插件菜单移除;**DSH 配置原样保留**,状态存本机
27
+
28
+ ### 排序、按钮与细节
29
+ - 设置页右下角浮窗:**直接拖动**供应商行/模型行排序;模型顺序写进 DSH 配置(同一份配置的所有设备共用),供应商顺序与浮窗位置只记在当前浏览器
30
+ - 页面上所有按钮(含所有弹窗)都有**悬停反馈**:普通按钮提亮、主要操作保持强调色、危险操作泛红、开关用光环(不改开/关底色,避免被误读为状态变化)
31
+ - 模型行两颗图标键:🔗 **测速**(问网关该模型在不在列表里,结论 ✓/⚠/✗ 变色)、🗑 **删除**;两者同尺寸,测速键是普通按钮的浅底,删除键是红框
32
+ - 悬停文案只说明「这个按钮做什么 / 现在什么状态」,不写实现细节
32
33
 
33
34
  ## 安装
34
35
 
35
- `<profile>` 是你的目标环境:浏览器版用 `web`,桌面客户端用 `desktop`。
36
+ 装到哪个环境,就用对应的方法:
37
+
38
+ **浏览器版(`web`,CLI 管理)**
36
39
 
37
40
  ```sh
38
- dsh plugin --profile <profile> add @nimoxie/dsh-model-organizer
41
+ dsh plugin --profile web add @nimoxie/dsh-model-organizer
39
42
  ```
40
43
 
41
- - 浏览器版(`web`):装完重启服务(关掉再 `dsh web`)。
42
- - 桌面客户端(`desktop`):桌面 profile 由客户端独占管理,请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-model-organizer`)。
44
+ 装完重启服务(关掉再 `dsh web`)。`--profile` 后面是目标 profile 名:浏览器版是 `web`,你自己另外创建的 profile 填对应的名字。
45
+
46
+ **桌面客户端**
47
+
48
+ 桌面 profile 由客户端独占管理,**不要用上面的 CLI 命令**。请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-model-organizer`)。
43
49
 
44
- 卸载(CLI 可管理的环境):
50
+ 卸载(CLI 环境):
45
51
 
46
52
  ```sh
47
53
  dsh plugin --profile <profile> remove @nimoxie/dsh-model-organizer
48
54
  ```
49
55
 
50
- 也可以从 GitHub 或本地目录装(开发用):
56
+ 开发用:从 GitHub 或本地目录装
51
57
 
52
58
  ```sh
53
59
  dsh plugin --profile <profile> add github:NimoXie15/dsh-model-organizer
@@ -76,9 +82,10 @@ dsh plugin --profile <profile> add link:/absolute/path/to/dsh-model-organizer
76
82
 
77
83
  ## 已知限制
78
84
 
85
+ - 「测试连接」验证的是**地址 / 密钥 / 网关可达性,以及这个模型 ID 是否在网关返回的列表里** —— 它不会真的发一条消息给模型(DSH 的远程接口没有这个能力,客户端也读不回密钥)。所以「✓」只代表这条路由和这个 ID 是通的,不等于这个模型一定能补全
79
86
  - 设置页右下角的排序浮窗**只列你自己添加的供应商**(写进配置的那些),官方路由**根本不进这个浮窗**;想在左栏看到官方路由,看顶部只读的「官方(内置)」分组
80
87
  - 浮窗里排出的供应商顺序会**镜像**到官方模型页的卡片上;官方卡片(DeepSeek 官方等)被刻意保持在最前面
81
- - 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在 DSH 的用户配置里,同一份 DSH 的所有设备共用——两台机器各装一份 DSH 则互不相通
88
+ - 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在 DSH 的用户配置里,同一份 DSH 配置的所有设备共用——两台机器各装一份 DSH 则互不相通
82
89
  - 不修改 DSH 本体,只通过官方提供的扩展点接入
83
90
 
84
91
  ## 许可
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
- # @nimoxie/dsh-model-organizer bundle patch: one host row over the web profile.
2
- # The row name resolves from the profile's own node_modules where
3
- # `dsh plugin --profile web add <path>` installed this package.
4
- - insert:
5
- - id: model-organizer
1
+ # @nimoxie/dsh-model-organizer bundle patch: one host row over the web profile.
2
+ # The row name resolves from the profile's own node_modules where
3
+ # `dsh plugin --profile web add <path>` installed this package.
4
+ - insert:
5
+ - id: model-organizer
6
6
  name: "@nimoxie/dsh-model-organizer"