dsh-model-organizer 0.3.1 → 0.3.3

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/DEVELOPING.md ADDED
@@ -0,0 +1,130 @@
1
+ # 开发笔记
2
+
3
+ 面向改这个插件的人(包括未来的我)。用户文档在 [README.md](./README.md)。
4
+
5
+ ## ⚠️ 改代码前务必先读:踩过的坑
6
+
7
+ 0. **服务改名会让座位"静默消失",必须注入新旧两个名字。**
8
+ DSH 0.1.7 把客户端设置镜像服务从 `settingsScope` 改名为 `configForms`(由 `@deepseek-ai/dsh-client-ui-settings` 提供,`describe()` 返回的还是同一个 mirror 对象)。
9
+ cordis 的 `ctx.inject([...])` **只要有一个服务永不出现,回调就永远不执行** —— 不报错、不打日志。所以 0.1.7 一升级,本插件的设置页浮窗就凭空消失了(输入框的模型菜单照常,因为它注入的 `slots/modelDirectories/sessions` 都没变)。
10
+ 现在 `apply()` 里同时注入两个名字,用一次性标志保证只挂载一次:
11
+ ```js
12
+ ctx.inject(["slots", "configForms", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.configForms.describe()));
13
+ ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.settingsScope.describe()));
14
+ ```
15
+ **推论**:升级 DSH 后如果某个座位不见了,第一件事是去新版本的 `dsh-client-ui-*` 包里 grep 自己注入的每个服务名。
16
+
17
+ 1. **`file:` 依赖是打包快照,`link:` 才是符号链接 —— 改仓库可能根本不生效。**
18
+ profile 里写 `"dsh-model-organizer": "file:../../../dsh-model-organizer"` 时,pnpm 把目录**打包复制**进自己的 store;只有 `pnpm add ./dir`(直接路径)或 `link:` 才是符号链接。
19
+ 症状:改完仓库代码、强刷浏览器,界面毫无变化;查 `window.__dshModelOrganizerBuild` 会发现跑的还是旧构建号,而服务端 bundle 里明明是新代码 —— 因为 DSH 加载的是 `profiles/<p>/node_modules/dsh-model-organizer/lib/client.js` 那份**副本**。
20
+ 排查:直接 grep 那份副本的 `BUILD` 常量,和仓库对比。
21
+ 修法:`dsh plugin --profile web remove dsh-model-organizer && dsh plugin --profile web add link:./dsh-model-organizer`(换成符号链接后只改一处)。在此之前必须**两份同步改**。
22
+
23
+ 2. **0.1.7 的菜单色 token 是半透明的,浮层必须配 `backdrop-filter`。**
24
+ `--dsw-specific-menu` 现在解析成 `rgba(48,49,54,0.5)`,官方菜单靠 `backdrop-filter: var(--dsw-menu-backdrop-filter)` 配合。只取颜色不取模糊 → 面板是「透视」的,底下的官方卡片按钮直接透上来。
25
+
26
+ 3. **浮层要 `ReactDOM.createPortal` 到 `document.body`。**
27
+ `settings.models.footer` 座位在设置页很深的子树里,`position: fixed` 会被困在那个子树的堆叠上下文里,被别的插件的悬浮组件(鲸鱼、地球图标等)盖住 —— 提高 z-index 也没用。挂到 body 之后 z-index 才是全局的(本插件用 10000,官方菜单是 1100)。
28
+
29
+ 4. **profile bundle 必须声明 `dsh.bundle.patch`**(指向 `cordis.patch.yml`)。缺了它 dsh 启动即报
30
+ `profile bundle "..." declares no dsh.bundle in its package.json`。
31
+
32
+ 2. **顶层 `inject` 必须声明座位 standardProps 会读取的每一个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。
33
+ 少一个 → 渲染时抛 `cannot get property "remote.session" without inject` → **条目被静默弃权(abdicate)并回退到内置 UI**,界面无任何报错。
34
+ 调用 `remote.settings.mutate` 还必须显式声明 **`remote.settings`**。
35
+
36
+ 3. **owner props 以组件 props 传入,不经过座位的 `inject` 工厂**:`settings.models.provider-card` 的 `inject` 不接收参数,必须读 `props.provider`。
37
+
38
+ 4. **`single` 座位优先级**:同优先级注册会**直接抛错**;运行时为非 chain 座位**自动分配**优先级(`--nextPriority` 递减),**后注册者胜出**。
39
+
40
+ 5. **drop 事件会在行与容器上各触发一次**(冒泡),需用 ref 加锁,否则重复写入。
41
+
42
+ 6. **边框必须用 longhand**(`borderWidth/borderStyle/borderColor`)。用 shorthand `border` 再叠加 `borderColor` 时,React 会把 shorthand 展开成 longhand;回退时移除 `borderColor`,`border-color` 就落回 **currentColor** —— 表现为「拖拽结束后高亮边框一直不消失」。
43
+
44
+ 7. **拖拽会把源行留在焦点上**。行加 `tabIndex:-1`、drop/dragend 时 `blur()`;并且**不要给行画 `:focus-visible` 背景**(官方 `option` 类自带一条,会变成「拖完还留一块灰底」)。
45
+
46
+ 8. **原生拖拽会让文字变成可拖对象**(浏览器弹「松开鼠标即可搜索」)。解决:`-webkit-user-drag:none` 加在**行的内容**上,不要加在行本身(加在行上会让行也拖不动)。这条是从侧边栏会话行的实现里学来的。
47
+
48
+ 9. **primitive 图标不转发 inline `style`**:需要旋转/变色请传 `className`(本插件为此注入一张极小的自有样式表)。
49
+
50
+ 10. **数组顺序可控,record 键顺序不可控**:`providers` 是 record,宿主要规范化键顺序,整体 `set` 与 `unset`+`set` 都实测无效 —— 所以供应商顺序只能存本机偏好。
51
+
52
+ 11. **面板里用到的每个局部变量都要真的声明**:曾在 `ProviderModelOrder` 里用了 `C`(官方类名映射)却没定义 → `ReferenceError` → 条目再次被静默弃权,表现为「面板整个不见了」。
53
+
54
+ 12. 浏览器半只需 `react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives`(都在 shell 的 PLATFORM_MODULES 种子里),无需 `dsh.client.external`。
55
+
56
+ ## 兼容性与鲁棒性(已核查)
57
+
58
+ ### 已发现并修复的真实冲突
59
+
60
+ **`settings.models.provider-card` 被 `@linxin666/dsh-client-ui-model-capabilities` 占用。**
61
+ 该座位是 **keyed**,key 由官方页面固定派发为供应商的 settings namespace(`llm-pi-ai`);而该插件已经用同一个 key 注册了「模型能力」面板。SlotCore 对 keyed 座位**一个 key 只保留一个占用者**,且同 key 同优先级会直接抛错 —— 它用 `try/catch` 吞掉异常,所以**本插件早期版本一直在静默压制它的「模型能力」面板**(实测:让出该座位后,每个供应商卡片立刻出现「模型能力」)。
62
+
63
+ **修复**:本插件不再占用该座位,模型排序改由自己拥有的 `settings.models.footer`(list 座位,`id: model-organizer-provider-order`)承载 —— list 座位按 id 去重,天然可与该插件的 `ui-model-capabilities` 共存。
64
+
65
+ ### 其它已做的加固
66
+
67
+ | 风险 | 处理 |
68
+ | :-- | :-- |
69
+ | 官方 CSS module 哈希变化 / 被别的模块误命中 | 运行时解析前缀,并用**只有该模块才有的第二个类**(`_optionCopy`)校验;缓存与样式表数量绑定;全部失败则退回内置回退样式 |
70
+ | 座位契约变更(prop 改名等) | **不拦截**:让渲染抛错 → SlotCore 弃权该条目 → 官方内置组件自动回填 |
71
+ | 座位被重命名 / 移除 | `slots.inject` 永不触发 → 对应面板静默消失,其它功能不受影响 |
72
+ | 单个座位注册失败 | 每个座位注册独立 `guard`,失败只记一条 warn,**不会拖垮整个插件** |
73
+ | 写入被拒绝 / ops 契约变化 | `mutate` 的同步抛错与 promise 拒绝都有捕获,并在表头显示失败原因 |
74
+ | 本机偏好 | `localStorage` key 带命名空间,读写都有 try/catch |
75
+ | 注入的样式表 | `<style id="dsh-model-organizer-style">`,类名统一 `dsh-mo-*` 前缀,注入幂等 |
76
+ | 服务端半边 | 只有空 `apply()`,不注册任何宿主能力 |
77
+
78
+ ### 仍然存在的风险(无法在插件侧消除)
79
+
80
+ - **`conversation.input.model` 是 single 座位**:将来若另一个插件以相同优先级注册,SlotCore 会抛错(本插件的 `guard` 会记 warn,官方内置组件仍会渲染)。
81
+ - **官方模型页若改变 keyed 派发约定**,依赖该座位的插件会失效 —— 本插件已不依赖它。
82
+ - **官方 CSS module 若改名**(例如删掉 `optionCopy`),校验会失败并退回回退样式:功能可用、外观降级。
83
+ - **卡片排序**是本插件唯一会改官方 DOM 的地方(给官方列表设 `display:flex` + 给 `li` 设 CSS `order`,不移动节点)。若 DSH 改那棵 DOM,该功能会静默降级,其余功能不受影响。
84
+
85
+ ## 排查手册
86
+
87
+ ### 让 agent 浏览器不显示窗口(消除抢焦点)
88
+
89
+ `dsh-ego-browser` 驱动的 Chromium 是 DSH 的子进程,默认以真实窗口存在,并且**每次动作工具都会 `Target.activateTarget` 把它顶到前台**(这就是「用着用着 Chrome 跳出来」的原因)。让它完全不出现窗口:
90
+
91
+ ```powershell
92
+ # 只对当前终端会话生效(先这样试)
93
+ $env:EGO_LINUX_HEADLESS = 1
94
+ dsh web
95
+
96
+ # 永久生效(设完必须重开终端,再启动 DSH)
97
+ setx EGO_LINUX_HEADLESS 1
98
+ ```
99
+
100
+ - 显式设置 `1/true/yes/on` **优先于**「有没有显示环境」的自动推断(含 Windows)。
101
+ - 浏览器是**单例常驻**进程:改完要重启 DSH(或 `ego-browser --stop`)才冷启动生效。
102
+ - **代价**:只损失「实时观察窗」;其余 `ego_*` 工具(导航/点击/输入/JS/截图/下载)全部照常。
103
+ - 不要用 `chromeArgs` 传 `--headless` —— 它在 `CHROME_BLOCKED` 黑名单里,会被过滤掉。
104
+
105
+ ### 改了插件代码但界面没变化
106
+
107
+ DSH 把客户端 bundle 以 `Cache-Control: public, max-age=31536000, immutable` 下发,而 URL 不随代码变化 —— 普通刷新会一直用缓存。**必须 Ctrl+Shift+R**(或在 DevTools → Network 勾 Disable cache)。
108
+
109
+ ## 发版流程
110
+
111
+ ```sh
112
+ # 1. 改代码后升版本号(npm 不允许覆盖已发布的版本)
113
+ # package.json 的 version:0.3.2 → 0.3.3
114
+ # 2. 提交并推送
115
+ git add -A && git commit -m "..." && git push
116
+ # 3. 发布
117
+ npm publish
118
+ ```
119
+
120
+ 发布需要带 **Bypass 2FA** 的 Granular Access Token(npm 2025-11 起只支持 granular token)。配置方式:
121
+ npmjs.com → 头像 → Access Tokens → Generate New Token → Packages 选 **All Packages + Read and write (publish and stage)**、**Organizations 选 No access**、勾 **Bypass two-factor authentication**;然后
122
+ `npm config set //registry.npmjs.org/:_authToken=npm_xxx`(写进用户级 `.npmrc`,不要放进仓库)。
123
+
124
+ ## 附:关于「(modlens vision)」供应商分组
125
+
126
+ 这些是 **`@liustack/modlens` 自动生成的「视觉变体」路由**,不是配置重复项:
127
+
128
+ - 机制:modlens 扫描已注册 provider,找出上游元数据声明为 `text` 且不含 image 的模型(默认按 `deepseek` / `glm` / `mimo` 前缀族匹配),为它们注册一条**声明支持图片输入**的伴随路由,界面名加 `(modlens vision)`。
129
+ - 用途:选中该分组下的模型后,粘贴/拖拽图片会走 DSH 原生附件流程,在调用(纯文本)模型前把图片转成证据文本。不选这个分组时,纯文本模型无法接收图片。
130
+ - 调整:编辑 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 里的 `modlens` 配置(`families` / `discover`),整条关掉用 `visionProvider: false`。视觉引擎配置在 `~/.modlens/config.json`。
package/README.md CHANGED
@@ -1,191 +1,71 @@
1
1
  # dsh-model-organizer
2
2
 
3
- DSH(DeepSeek Harness)模型整理插件。全部通过**官方座位(slot)扩展点**实现,不改动任何官方代码。
3
+ 给 DSH(DeepSeek Harness)换个更好用的模型选择器:**按供应商分组、可折叠、可拖拽排序**。
4
4
 
5
- ## 功能
6
-
7
- ### 1. 输入框的模型选择:官方同款外观 + 供应商分组折叠 + 推理强度
8
- 覆盖座位 `conversation.input.model`。触发器与弹层**直接复用官方 `dsh-client-ui-model-selection` 的 CSS-module 类名与 primitives 图标**:
9
-
10
- - 触发器 `_…_trigger`(28px 胶囊 + `IconDataOutline16` + `IconChevronDownOutline14`,展开时官方 `chevronOpen` 翻转);模型有推理等级时按官方格式显示 `模型 · 强度`(`triggerLabel` + `triggerEffort` 两个 span)。
11
- - 弹层 `_…_menu`(20px 圆角、官方定位算法:右对齐、向上弹出、12px 夹取、滚动/缩放重定位、`z-index 1100`)。
12
- - **供应商行**用官方 `cell` 下钻行(`cellLabel` + `cellValue` 计数 + `cellChevron`),点击展开/收起时箭头旋转 90°;**模型行**用官方 `option`/`optionCopy`/`modelName`/`check`,当前模型打 ✓。
13
- - **推理强度**:弹层底部一行官方 `cell`(`推理等级` + 当前值 + 箭头)→ 切到等级面板,等级用官方 `option` 行渲染并给当前等级打 ✓;选等级会以 `{provider, model, reasoningEffort}` **整份重提**(与官方一致);Esc 先返回模型面板、再按一次才关闭。
14
-
15
- 默认只列出**供应商**、当前供应商自动展开、其余折叠(这是相对官方扁平列表的增强)。
16
-
17
- > 类名前缀在运行时从已注入的样式表里解析(锚定 `_triggerEffort` 等只有该模块才有的类名——直接找 `_trigger` 会误命中 updater 模块的 `fThDlq_trigger`),dsh 升级导致哈希变化也不会静默丢样式;解析失败有内置回退样式。
18
-
19
- **推理强度行什么时候出现**:与官方一致——只有**当前模型提供推理等级**时才显示。等级来源(宿主 `resolveModelReasoning`):模型写了 `reasoningEfforts` 就用它;写 `false` 表示非推理模型;**没写则回退到内置 catalog 的能力**(`base.reasoning`),所以自建/改名的模型 ID 通常拿不到等级,需要显式声明:
20
-
21
- ```yaml
22
- # settings.yaml → llm-pi-ai.providers.<provider>.models[]
23
- - id: <你的模型 ID>
24
- name: DeepSeek-V4-Pro
25
- reasoningEfforts: # 键=可选等级,值=发给上游的线值;只有 off 可以留空
26
- off:
27
- high: high
28
- max: ultra
29
- ```
30
-
31
- ### 2. 模型顺序拖拽(写入设置,持久化)
32
- 在**页脚面板**(见功能 3)里,每个供应商行可展开,展开后列出它的模型并可直接拖拽排序:
33
-
34
- - **默认折叠**(表头只显示 `模型顺序 | 拖动 ⠿ 调整,松手即保存`),点击表头展开/收起,箭头随之旋转 90°;
35
- - 表头与模型行都用官方 `cell` / `option` 类渲染(与官方模型页同一套样式);
36
- - 状态提示(`已保存 ✓` / `有未保存改动` / 只读)跟在表头文字**右侧**,不会挤压列表布局;
37
- - 拖动 ⠿ 时列表**实时按目标顺序重排**(其他行让位),**松手即保存**并写回 `llm-pi-ai.providers.<id>.models`(实测 `settings.yaml` 随顺序变化);
38
- - 无上下箭头;仅 ≥2 个模型的供应商显示。
5
+ ## 它解决什么问题
39
6
 
40
- ### 3. 供应商与模型顺序面板(模型页内的浮动面板)
41
- 注册座位 `settings.models.footer`(**list** 座位,用本插件自己的 `id`,可与其它插件共存),并以**固定浮动面板**呈现(右下角 `fixed`,380px,`z-index 900`):这样在上面配置供应商 API / 模型的同时,就能在右下角直接拖拽排序,不必滚到页面底部。**默认折叠**(只留说明行 + 一个折叠按钮),展开状态记在 `localStorage: dsh-model-organizer.panelOpen`,点一次以后保持。
7
+ 官方那个模型下拉是一个扁平长列表——供应商只是粘性小标题,模型多的时候要滚很久,也没法按自己的习惯排序。这个插件把它改成两级:**先看供应商,点开才看模型**,并且供应商和模型的顺序都能直接拖。
42
8
 
43
- 面板提供:
44
-
45
- - 供应商行(官方 `cell` 样式 + 可点击展开,箭头旋转 90°),**默认折叠**;
46
- - 展开后嵌套该供应商的模型行(官方 `option` 样式 + 前置圆点 + 缩进,与供应商行视觉区分),可拖拽排序并写入设置;
47
- - 供应商行本身也可拖拽(调整显示顺序);
48
- - 说明行**没有任何悬停反应**(它是说明,不是控件);折叠按钮是独立的按钮,只有它有悬停/上浮反馈;拖拽手柄 ⠿ 做成按钮样式(悬停出现描边 + 上浮 1px + `cursor:grab`)。
9
+ ## 功能
49
10
 
50
- **为什么不写进 settings**:`providers` 是 record,宿主要规范化它的键顺序——整体 `set` 与 `unset`+`set` 两种写法都实测无效(返回 ok、文件被重写,但键顺序与界面顺序都不变)。因此该顺序存储为本插件的本机偏好(`localStorage: dsh-model-organizer.providerOrder`),作用于本插件渲染的模型菜单排列,拖完立即生效(每次渲染读取,无需刷新)。
11
+ **输入框的模型菜单**
12
+ - 默认只列供应商,点一下展开它的模型,当前模型打 ✓
13
+ - 触发器显示 `模型 · 推理等级`,等级可以在菜单底部单独切换
14
+ - 外观沿用官方样式(同一个弹层、同一套图标),不会突兀
51
15
 
52
- ## 安装 / 卸载
16
+ **排序**
17
+ - 在「设置 → 模型」右下角的浮窗里,**直接拖动**供应商行或模型行
18
+ - 模型顺序写进 `settings.yaml`(永久、跨设备);供应商顺序存在本机
53
19
 
54
- `dsh plugin` 只是把参数转发给 profile 目录里的 pnpm,所以下面三种来源都能装(**不需要发布到 npm**):
20
+ ## 安装
55
21
 
56
22
  ```sh
57
- # 1) npm(发布后可用,最快)
58
23
  dsh plugin --profile web add dsh-model-organizer
59
-
60
- # 2) GitHub 仓库
61
- dsh plugin --profile web add github:NimoXie15/dsh-model-organizer
62
-
63
- # 3) 本地目录(开发时用)
64
- dsh plugin --profile web add link:/absolute/path/to/dsh-model-organizer
65
24
  ```
66
25
 
67
- 装完**重启 web 服务**(关闭后重新 `dsh web`)。卸载:
26
+ 装完重启 web 服务(关掉再 `dsh web`)。卸载:
68
27
 
69
28
  ```sh
70
29
  dsh plugin --profile web remove dsh-model-organizer
71
30
  ```
72
31
 
73
- > 改完插件代码如果界面上没变化,先 **Ctrl+Shift+R** 强刷:DSH 把客户端 bundle 以 `Cache-Control: immutable, max-age=31536000` 下发,而 URL 不随代码变化,普通刷新会一直用缓存。
32
+ 也可以从 GitHub 或本地目录装(`dsh plugin` 只是转发给 pnpm,不需要发布到 npm):
74
33
 
75
- ## 截图
34
+ ```sh
35
+ dsh plugin --profile web add github:NimoXie15/dsh-model-organizer
36
+ dsh plugin --profile web add link:/absolute/path/to/dsh-model-organizer
37
+ ```
76
38
 
77
- > 📷 **截图占位**:待补充。上一版截图包含真实供应商 / 会话名称,已全部移除,将在脱敏环境(干净的 profile、示例供应商名)重新截图后补上。
39
+ > **兼容性**:已在 DSH `0.1.7-rc.1` 上验证,同时兼容 `0.1.5`/`0.1.6`(0.1.7 把客户端设置镜像服务从 `settingsScope` 改名为 `configForms`,本插件两个名字都注入,所以两个版本都能用)。
78
40
  >
79
- > 想要的效果预览:
80
- > - **模型选择菜单**:供应商分组行(可点击展开/收起,箭头旋转)→ 展开后是缩进的模型行(前置圆点、当前项打勾)→ 底部一行「推理等级」入口,点进去是等级列表。
81
- > - **模型设置页浮窗**:右下角固定面板,表头一行「供应商与模型顺序 · 拖动行即可调整顺序」;展开后是供应商行(右侧显示「N 个模型」),点开某个供应商可以看到它下面的模型行;供应商行和模型行都能直接拖动排序。
82
-
83
- ## ⚠️ 开发要点(踩过的坑,改代码前务必先读)
84
-
85
- 1. **profile bundle 必须声明 `dsh.bundle.patch`**(指向 `cordis.patch.yml`)。缺了它 dsh 启动即报
86
- `profile bundle "..." declares no dsh.bundle in its package.json`。
87
-
88
- 2. **顶层 `inject` 必须声明座位 standardProps 会读取的每一个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。
89
- 少一个 → 渲染时抛 `cannot get property "remote.session" without inject` → **条目被静默弃权(abdicate)并回退到内置 UI**,界面无任何报错。
90
- 调用 `remote.settings.mutate` 还必须显式声明 **`remote.settings`**。
91
-
92
- 3. **owner props 以组件 props 传入,不经过座位的 `inject` 工厂**:`settings.models.provider-card` 的 `inject` 不接收参数,必须读 `props.provider`。
41
+ > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(DSH 的客户端 bundle 带一年 immutable 缓存,普通刷新不会重新下载)。
93
42
 
94
- 4. **`single` 座位优先级**:同优先级注册会**直接抛错**;运行时为非 chain 座位**自动分配**优先级(`--nextPriority` 递减),**后注册者胜出**。
95
-
96
- 5. **drop 事件会在行与容器上各触发一次**(冒泡),需用 ref 加锁,否则重复写入。
97
-
98
- 6. **边框必须用 longhand**(`borderWidth/borderStyle/borderColor`)。
99
- 用 shorthand `border` 再叠加 `borderColor` 时,React 会把 shorthand 展开成 longhand;回退时移除 `borderColor`,`border-color` 就落回 **currentColor** ——表现为"拖拽结束后高亮边框一直不消失"。这是本插件踩过的真实坑。
100
-
101
- 7. **拖拽后要清焦点**:浏览器会把被拖节点留在焦点上。行加 `outline:none` + `tabIndex:-1`,drop/dragend 时 `blur()`。
102
-
103
- 7b. **面板里用到的每个局部变量都要真的声明**:曾在 `ProviderModelOrder` 里用了 `C`(官方类名映射)却没定义 → `ReferenceError` → 条目再次被静默弃权,表现为"面板整个不见了"。
104
-
105
- 8. **primitive 图标不转发 inline `style`**:需要旋转/变色请传 `className`(本插件为此注入一张极小的自有样式表)。
106
-
107
- 9. **数组顺序可控,record 键顺序不可控**(见功能 3)。
108
-
109
- 10. 浏览器半只需 `react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives`(都在 shell 的 PLATFORM_MODULES 种子里),无需 `dsh.client.external`。
110
-
111
- ## 关于「(modlens vision)」供应商分组
112
-
113
- 这些是 **`@liustack/modlens` 自动生成的「视觉变体」路由**,不是你的配置重复项(依据:modlens 自带文档 `docs/harness-setup.zh-CN.md`):
114
-
115
- - 机制:modlens 扫描所有已注册 provider,找出上游元数据声明为 `text` 且**不含 image** 的模型(默认按 `deepseek` / `glm` / `mimo` 前缀族匹配),为它们注册一条**声明支持图片输入**的伴随路由,界面名加上 `(modlens vision)`。
116
- - 用途:**选中该分组下的模型后**,粘贴/拖拽图片与附件按钮会走 DSH 原生附件流程,在调用(纯文本)模型前把图片转成**证据文本**。不选这个分组时,纯文本模型根本无法接收图片。
117
- - 调整范围:编辑 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:
118
- ```yaml
119
- - id: modlens
120
- config:
121
- families: ['deepseek', 'glm'] # 缩小自动发现前缀(默认 deepseek/glm/mimo)
122
- discover: ['openrouter', 'nvidia'] # 只在这些已注册 provider 里发现
123
- ```
124
- 想整条关掉可用 `visionProvider: false`。视觉引擎(用哪个模型读图)配置在 `~/.modlens/config.json`,也可在 **设置 → 插件 → 插件配置** 的卡片里改。
125
-
126
- ## 兼容性与鲁棒性(已核查)
127
-
128
- ### 已发现并修复的真实冲突
129
-
130
- **`settings.models.provider-card` 被 `@linxin666/dsh-client-ui-model-capabilities` 占用。**
131
- 该座位是 **keyed**,key 由官方页面固定派发为供应商的 settings namespace(`llm-pi-ai`);而该插件已经用同一个 key 注册了「模型能力」面板。SlotCore 对 keyed 座位**一个 key 只保留一个占用者**,且同 key 同优先级会直接抛错 —— 它用 `try/catch` 吞掉异常,所以**本插件早期版本一直在静默压制它的「模型能力」面板**(实测:让出该座位后,每个供应商卡片立刻出现「模型能力」)。
132
-
133
- **修复**:本插件不再占用该座位,模型排序改由自己拥有的 `settings.models.footer`(list 座位,`id: model-organizer-provider-order`)承载 —— list 座位按 id 去重,天然可与该插件的 `ui-model-capabilities` 共存。
134
-
135
- ### 其它已做的加固
136
-
137
- | 风险 | 处理 |
138
- | :-- | :-- |
139
- | 官方 CSS module 哈希变化 / 被别的模块误命中 | 运行时解析前缀,并用**只有该模块才有的第二个类**(`_optionCopy`)校验;缓存与样式表数量绑定,晚注入的样式表仍能被发现;全部失败则退回内置回退样式 |
140
- | 座位契约变更(prop 改名等) | **不拦截**:让渲染抛错 → SlotCore 弃权该条目 → 官方内置组件自动回填,用户始终有可用的选择器 |
141
- | 座位被重命名 / 移除 | `slots.inject` 永不触发 → 对应面板静默消失,其它功能不受影响 |
142
- | 单个座位注册失败(含未来与别的插件抢 single/keyed 单元) | 每个座位注册独立 `guard`,失败只记一条 warn,**不会拖垮整个插件** |
143
- | 写入被拒绝 / ops 契约变化 | `mutate` 的同步抛错与 promise 拒绝都有捕获,并在表头显示失败原因 |
144
- | 本机偏好 | `localStorage` key 为 `dsh-model-organizer.providerOrder`,带命名空间、读写都有 try/catch |
145
- | 注入的样式表 | `<style id="dsh-model-organizer-style">`,类名统一 `dsh-mo-*` 前缀,注入幂等 |
146
- | 服务端半边 | 只有空 `apply()`,不注册任何宿主能力 |
147
-
148
- ### 仍然存在的风险(无法在插件侧消除)
149
-
150
- - **`conversation.input.model` 是 single 座位**:如果将来另一个插件也以相同优先级注册,SlotCore 会抛错(本插件的 `guard` 会记 warn,官方内置组件仍会渲染)。
151
- - **官方模型页若改变 keyed 派发约定**(不再用 settingsNs 作 key),依赖该座位的插件(含本插件的早期形态)会失效——本插件已不依赖它。
152
- - **官方 CSS module 若改名**(例如删掉 `optionCopy`),校验会失败并退回回退样式:功能可用、外观降级。
153
-
154
- ### 与官方页面的耦合(审计结论,非 bug 但要知道)
155
-
156
- - **卡片排序**是本插件唯一会改官方 DOM 的地方:给官方供应商列表设 `display:flex; flex-direction:column`,再给每个 `li` 设 CSS `order`(不移动节点)。若 DSH 改动那棵 DOM 的结构,该功能会静默降级(卡片回到文档顺序),其余功能不受影响。
157
- - **官方 CSS-module 类名**靠运行时解析(锚定只有该模块才有的类名,并用第二个类名校验);解析失败会退回内置回退样式:功能可用、外观降级。
158
- - **座位占用只有两个**:`conversation.input.model`(single,priority -1)与 `settings.models.footer`(list,id `model-organizer-provider-order`)。与 `@linxin666/dsh-client-ui-model-capabilities` 冲突的 `settings.models.provider-card` 座位**已被本插件让出**。
159
- - 只写一个全局:`window.__dshModelOrganizerBuild`(构建标记,用于判断浏览器是否在跑缓存的旧 bundle);只注入一张 `<style id="dsh-model-organizer-style">`(幂等)。
160
-
161
- ## 附:环境变量与排查
43
+ ## 截图
162
44
 
163
- ### 让 agent 浏览器不显示窗口(消除抢焦点)
45
+ 📷 待补充。
164
46
 
165
- `dsh-ego-browser` 驱动的那个 Chromium 是 DSH 的子进程,默认会以一个真实窗口存在,并且**每次动作工具都会 `Target.activateTarget` 把它顶到前台**(这就是"用着用着 Chrome 跳出来"的原因)。让它完全不出现窗口:
47
+ ## 想让「推理等级」出现,需要模型自己声明
166
48
 
167
- ```powershell
168
- # 只对当前终端会话生效(先这样试)
169
- $env:EGO_LINUX_HEADLESS = 1
170
- dsh web
49
+ 只有模型提供了等级,菜单里才会出现「推理等级」入口——这与官方行为一致。在你的 `settings.yaml` 里给模型加上:
171
50
 
172
- # 永久生效(设完必须重开终端,再启动 DSH)
173
- setx EGO_LINUX_HEADLESS 1
51
+ ```yaml
52
+ # settings.yaml → llm-pi-ai.providers.<供应商>.models[]
53
+ - id: <你的模型 ID>
54
+ reasoningEfforts: # 键=可选等级,值=发给上游的线值;只有 off 可以留空
55
+ off:
56
+ high: high
174
57
  ```
175
58
 
176
- - 显式设置 `1/true/yes/on` 会**优先于**"有没有显示环境"的自动推断(含 Windows),所以桌面环境下也能强制无头。
177
- - 浏览器是**单例常驻**进程:改完要重启 DSH(或运行 `ego-browser --stop`)才会冷启动生效。
178
- - **代价**:只损失「实时观察窗」这一项;其余 `ego_*` 工具(导航/点击/输入/JS/截图/下载)全部照常。
179
- - 想恢复观察窗:去掉该环境变量并重启 DSH。
180
- - 不要试图用插件设置里的 `chromeArgs` 传 `--headless` —— 它被列在 `CHROME_BLOCKED` 黑名单里,会被过滤掉。
59
+ ## 已知限制
181
60
 
182
- ### 改了插件代码但界面没变化
61
+ - 只处理 `llm-pi-ai` 命名空间下的供应商(DeepSeek 官方供应商不在其中)
62
+ - 供应商顺序、浮窗位置是本机偏好,不跨设备同步
63
+ - 只占用两个官方扩展点(`conversation.input.model`、`settings.models.footer`),不改官方代码
183
64
 
184
- DSH 把客户端 bundle 以 `Cache-Control: public, max-age=31536000, immutable` 下发,而 bundle 的 URL 不随代码变化 —— 普通刷新会一直用缓存。**必须 Ctrl+Shift+R 强刷**(或在 DevTools → Network 勾 Disable cache)。
65
+ ## 许可
185
66
 
186
- ## 已知限制
67
+ MIT
68
+
69
+ ---
187
70
 
188
- - 只处理 `llm-pi-ai` 命名空间;DeepSeek 官方供应商(`dsh-llm-deepseek`)不在其内。
189
- - 供应商顺序是本机偏好,不同步到其它浏览器/设备;模型顺序写入设置文档,跨设备同步。
190
- - 推理强度行需要模型声明 `reasoningEfforts`(或 catalog 已知该模型 ID),否则不显示——与官方行为一致。
191
- - 浮窗位置(拖到哪)也是本机偏好;想复位就清掉 `localStorage` 里的 `dsh-model-organizer.panelPos`。
71
+ 开发笔记、兼容性审计、排查手册见 [DEVELOPING.md](./DEVELOPING.md)。
package/lib/client.js CHANGED
@@ -90,7 +90,7 @@ window.__ModuleLoader__.load({
90
90
  code changes, so a browser can keep running an old build until a hard reload.
91
91
  Kept OFF the UI (it is not user-facing): read it as
92
92
  `window.__dshModelOrganizerBuild` when a stale build is suspected. */
93
- const BUILD = "b23";
93
+ const BUILD = "b24";
94
94
 
95
95
  let officialClasses = null;
96
96
  let officialClassesSheets = -1;
@@ -265,11 +265,16 @@ window.__ModuleLoader__.load({
265
265
  /* Floating presentation: the panel sits in the corner of the Models page so
266
266
  provider/API configuration and reordering are visible at the same time. */
267
267
  floatPanel: {
268
- position: "fixed", right: 20, bottom: 20, zIndex: 900, width: 380, maxWidth: "calc(100vw - 40px)",
268
+ position: "fixed", right: 20, bottom: 20, zIndex: 10000, width: 380, maxWidth: "calc(100vw - 40px)",
269
269
  display: "flex", flexDirection: "column", padding: 4,
270
270
  border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 14,
271
271
  background: "var(--dsw-specific-menu, var(--dsw-alias-bg-module, #222))",
272
- boxShadow: "0 10px 30px rgba(0,0,0,.34)", color: "var(--dsw-alias-label-primary)"
272
+ /* 0.1.7's menu token is translucent and the shipped menu pairs it with this
273
+ blur; without it the panel reads as see-through and the cards behind it
274
+ bleed through. The elevation shadow matches the shipped menu too. */
275
+ backdropFilter: "var(--dsw-menu-backdrop-filter)",
276
+ boxShadow: "var(--dsw-elevation-prominent, 0 10px 30px rgba(0,0,0,.34))",
277
+ color: "var(--dsw-alias-label-primary)"
273
278
  },
274
279
  /* The shipped `cell` sets min-width:100%; the head must stay inside the panel. */
275
280
  panelHeadCell: { display: "flex", alignItems: "center", gap: 6, flex: "1 1 auto", minWidth: 0 },
@@ -1105,6 +1110,38 @@ window.__ModuleLoader__.load({
1105
1110
  return known.concat(rest);
1106
1111
  }
1107
1112
 
1113
+ /* A seat entry that throws is silently abdicated, so "my panel is missing" and
1114
+ "my panel threw" look identical from the page. This boundary turns the throw
1115
+ into one visible line, so the failure can be reported instead of guessed. */
1116
+ class PanelBoundary extends React.Component {
1117
+ constructor(props) {
1118
+ super(props);
1119
+ this.state = { err: null };
1120
+ }
1121
+ static getDerivedStateFromError(err) {
1122
+ return { err: err };
1123
+ }
1124
+ render() {
1125
+ if (this.state.err !== null) {
1126
+ const message = String((this.state.err && this.state.err.message) || this.state.err);
1127
+ return h("div", {
1128
+ "data-mo": "panel-error",
1129
+ style: {
1130
+ margin: "8px 0", padding: "6px 10px", borderRadius: 8, fontSize: 12, lineHeight: "18px",
1131
+ whiteSpace: "pre-wrap",
1132
+ border: "1px solid var(--dsw-alias-state-error-primary, #f85149)",
1133
+ color: "var(--dsw-alias-state-error-primary, #f85149)"
1134
+ }
1135
+ }, "dsh-model-organizer 面板渲染失败:" + message);
1136
+ }
1137
+ return this.props.children;
1138
+ }
1139
+ }
1140
+
1141
+ function ProviderOrderPanelGuarded(props) {
1142
+ return h(PanelBoundary, null, h(ProviderOrderPanel, props));
1143
+ }
1144
+
1108
1145
  function ProviderOrderPanel(props) {
1109
1146
  const C = resolveOfficialClasses();
1110
1147
  ensureOwnStyles();
@@ -1164,21 +1201,9 @@ window.__ModuleLoader__.load({
1164
1201
  const idsKey = ids.join("|");
1165
1202
  const dirty = order !== null && order.join("|") !== sourceKey;
1166
1203
 
1167
- /* Mirror the stored order onto the shipped provider cards (CSS `order` only —
1168
- the DOM React rendered is never touched). The cards may be (re)rendered after
1169
- this effect runs, which drops the inline styles, so it is applied again on the
1170
- next frame and once more shortly after. */
1171
- useEffect(function () {
1172
- const list = idsKey.length === 0 ? [] : idsKey.split("|");
1173
- applyCardOrder(display, list);
1174
- const frame = requestAnimationFrame(function () { applyCardOrder(display, list); });
1175
- const timer = setTimeout(function () { applyCardOrder(display, list); }, 400);
1176
- return function () {
1177
- cancelAnimationFrame(frame);
1178
- clearTimeout(timer);
1179
- };
1180
- }, [sourceKey, idsKey]);
1181
-
1204
+ /* The shipped provider cards are left exactly as React renders them. Ordering used
1205
+ to be mirrored onto them with inline `order`; that is gone — this plugin adds a
1206
+ floating panel to the Models page and nothing else. */
1182
1207
  function commit(next) {
1183
1208
  if (next.join("|") === baseIds.join("|")) { setOrder(null); return; }
1184
1209
  writeProviderOrder(next);
@@ -1352,56 +1377,19 @@ window.__ModuleLoader__.load({
1352
1377
  : Object.assign({}, S.floatPanel, { left: panelPos.left, top: panelPos.top, right: "auto", bottom: "auto" });
1353
1378
  /* Original layout (header first, list below) with a TOP-anchored panel: the
1354
1379
  header never moves and collapsing pulls the bottom edge up. */
1355
- return h("div", { ref: panelRef, style: panelStyle },
1356
- head,
1357
- panelOpen ? h("div", { style: S.floatBody }, list) : null
1380
+ /* Portalled to <body>: the footer seat lives deep inside the settings page, so a
1381
+ fixed panel there is trapped in that subtree's stacking context and any other
1382
+ plugin's floating widget paints over it. At body level the z-index is global. */
1383
+ return ReactDOM.createPortal(
1384
+ h("div", { ref: panelRef, style: panelStyle, "data-mo": "panel" },
1385
+ head,
1386
+ panelOpen ? h("div", { style: S.floatBody }, list) : null
1387
+ ),
1388
+ document.body
1358
1389
  );
1359
1390
  }
1360
1391
 
1361
1392
  /* -------------------------------- helpers --------------------------------- */
1362
- /* The Models page renders one <li> per provider inside its own <ul>. The host
1363
- owns that order (the settings document cannot express it — verified), so the
1364
- stored preference is applied as CSS `order` on a flex column: the DOM is left
1365
- exactly as React rendered it, only the visual sequence changes. Cards that do
1366
- not correspond to a settings provider (catalog-only rows) stay pinned first. */
1367
- function applyCardOrder(providers, ids) {
1368
- try {
1369
- const names = [];
1370
- for (const id in providers) {
1371
- const dn = providers[id] ? providers[id].displayName : undefined;
1372
- if (typeof dn === "string" && dn.length > 0) names.push([id, dn]);
1373
- }
1374
- if (names.length === 0) return;
1375
- names.sort(function (a, b) { return b[1].length - a[1].length; });
1376
-
1377
- let list = null;
1378
- let best = 0;
1379
- const lists = document.querySelectorAll("ul");
1380
- for (let i = 0; i < lists.length; i++) {
1381
- const items = lists[i].children;
1382
- let hits = 0;
1383
- for (let j = 0; j < items.length; j++) {
1384
- const text = items[j].textContent || "";
1385
- for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { hits++; break; }
1386
- }
1387
- if (hits >= 2 && hits > best) { best = hits; list = lists[i]; }
1388
- }
1389
- if (list === null) return;
1390
-
1391
- list.style.display = "flex";
1392
- list.style.flexDirection = "column";
1393
- const rank = {};
1394
- for (let i = 0; i < ids.length; i++) rank[ids[i]] = i;
1395
- const items = list.children;
1396
- for (let i = 0; i < items.length; i++) {
1397
- const text = items[i].textContent || "";
1398
- let matched;
1399
- for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { matched = names[k][0]; break; }
1400
- const r = matched === undefined ? undefined : rank[matched];
1401
- items[i].style.order = String(r === undefined ? -1 : r);
1402
- }
1403
- } catch (e) {}
1404
- }
1405
1393
 
1406
1394
  function namespaceView(view, namespace) {
1407
1395
  if (!view || !view.namespaces) return null;
@@ -1450,6 +1438,18 @@ window.__ModuleLoader__.load({
1450
1438
 
1451
1439
  function apply(ctx) {
1452
1440
  try { window.__dshModelOrganizerBuild = BUILD; } catch (e) {}
1441
+ /* A DOM marker proving this bundle actually applied, independent of any seat.
1442
+ Seat registration is silent by design (a throw abdicates the entry), so
1443
+ "is the new code even loaded?" is otherwise unanswerable from the page. */
1444
+ try {
1445
+ if (document.body !== null && document.querySelector('[data-mo="boot"]') === null) {
1446
+ const boot = document.createElement("div");
1447
+ boot.setAttribute("data-mo", "boot");
1448
+ boot.setAttribute("data-mo-build", BUILD);
1449
+ boot.hidden = true;
1450
+ document.body.appendChild(boot);
1451
+ }
1452
+ } catch (e) {}
1453
1453
  ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-model-organizer: dictionaries");
1454
1454
  const t = ctx.locale.bind(NS);
1455
1455
 
@@ -1481,8 +1481,27 @@ window.__ModuleLoader__.load({
1481
1481
  });
1482
1482
  });
1483
1483
 
1484
- ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], function (scope) {
1485
- const mirror = scope.settingsScope.describe();
1484
+ /* The settings mirror moved homes: DSH 0.1.7 exposes it as `configForms`
1485
+ (provided by ui-settings); 0.1.5/0.1.6 exposed the same object as
1486
+ `settingsScope`. A cordis `ctx.inject` whose service never appears simply
1487
+ never runs, so injecting only the old name made this whole seat vanish
1488
+ without a single error once DSH renamed it. Inject both names and mount
1489
+ once — whichever resolves first wins, the other stays dormant. */
1490
+ let orderPanelMounted = false;
1491
+
1492
+ function mountOrderPanel(scope, mirror) {
1493
+ if (orderPanelMounted) return;
1494
+ orderPanelMounted = true;
1495
+ /* Mark that the service resolved and the panel is being mounted — this
1496
+ separates "the seat never rendered" from "the inject never fired". */
1497
+ try {
1498
+ if (document.body !== null && document.querySelector('[data-mo="panel-mounted"]') === null) {
1499
+ const mark = document.createElement("div");
1500
+ mark.setAttribute("data-mo", "panel-mounted");
1501
+ mark.hidden = true;
1502
+ document.body.appendChild(mark);
1503
+ }
1504
+ } catch (e) {}
1486
1505
  mirror.ensure();
1487
1506
  const mutate = function (ops, revision) { return scope.remote.settings.mutate(PI_AI_NS, ops, revision); };
1488
1507
 
@@ -1504,9 +1523,16 @@ window.__ModuleLoader__.load({
1504
1523
  id: "model-organizer-provider-order",
1505
1524
  order: 100,
1506
1525
  inject: face
1507
- }, ProviderOrderPanel);
1526
+ }, ProviderOrderPanelGuarded);
1508
1527
  });
1509
1528
  });
1529
+ }
1530
+
1531
+ ctx.inject(["slots", "configForms", "remote", "remote.settings"], function (scope) {
1532
+ mountOrderPanel(scope, scope.configForms.describe());
1533
+ });
1534
+ ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], function (scope) {
1535
+ mountOrderPanel(scope, scope.settingsScope.describe());
1510
1536
  });
1511
1537
  }
1512
1538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-organizer",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "DSH 模型整理插件:输入框的模型下拉改为「供应商分组 + 模型二级折叠菜单」(含推理等级),并在模型设置页提供一个可拖拽的「供应商与模型顺序」浮窗。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -17,6 +17,7 @@
17
17
  "lib",
18
18
  "cordis.patch.yml",
19
19
  "README.md",
20
+ "DEVELOPING.md",
20
21
  "LICENSE"
21
22
  ],
22
23
  "keywords": [