@nimoxie/dsh-model-organizer 0.3.4 → 0.3.6
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 → DEVELOPMENT.md} +75 -72
- package/README.md +22 -21
- package/cordis.patch.yml +2 -2
- package/lib/client.js +100 -65
- package/package.json +2 -2
|
@@ -1,72 +1,75 @@
|
|
|
1
|
-
# 开发笔记
|
|
2
|
-
|
|
3
|
-
面向改这个插件的人。用户文档在 [README.md](./README.md)。
|
|
4
|
-
|
|
5
|
-
## 升级 DSH 后座位凭空消失?先查服务名
|
|
6
|
-
|
|
7
|
-
cordis 的 `ctx.inject([...])` **只要有一个服务永不出现,回调就永远不执行** —— 不报错、不打日志。所以服务改名会让整个座位静默消失。
|
|
8
|
-
|
|
9
|
-
已发生一次:0.1.7 把客户端设置镜像服务 `settingsScope` 改名为 `configForms`(提供者仍是 `@deepseek-ai/dsh-client-ui-settings`,`describe()` 返回同一个 mirror)。设置页浮窗因此消失,而输入框菜单照常(它注入的 `slots/modelDirectories/sessions` 没变)。
|
|
10
|
-
|
|
11
|
-
`apply()` 里同时注入两个名字,一次性标志保证只挂载一次:
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
ctx.inject(["slots", "configForms", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.configForms.describe()));
|
|
15
|
-
ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.settingsScope.describe()));
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
**规律**:升级后座位不见了,第一件事是去新版 `dsh-client-ui-*` 包里 grep 自己注入的每个服务名。
|
|
19
|
-
|
|
20
|
-
## 座位的失败模式(决定了代码结构)
|
|
21
|
-
|
|
22
|
-
- **条目渲染时抛错 → 被静默弃权,官方内置 UI 自动回填**,界面上没有任何提示。
|
|
23
|
-
- **座位注册本身失败 → 该座位静默消失**,其它功能不受影响。
|
|
24
|
-
- 所以:每个座位注册各套一层 `guard`(失败只记 warn,不拖垮插件);组件外套 `PanelBoundary`(把抛错变成页面上的一行红字,而不是"凭空消失")。
|
|
25
|
-
- **顶层 `inject` 必须列全座位 standardProps 会读的每个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。少一个 → 渲染抛 `cannot get property "..." without inject` → 弃权。用 `remote.settings.mutate` 必须显式声明 `remote.settings`。
|
|
26
|
-
- `single` 座位在**同一优先级已有注册**时会直接抛错。不写 `priority` 时,运行时会为非 chain 座位自动分配一个唯一的优先级(后注册的更低,渲染时胜出),所以只有显式写了撞车的 `priority` 才会抛。
|
|
27
|
-
- 未声明的局部变量(`ReferenceError`)同样会导致弃权,表现就是"整个面板不见了"。
|
|
28
|
-
|
|
29
|
-
## 与官方模块的耦合
|
|
30
|
-
|
|
31
|
-
- **官方 CSS-module 类名靠运行时解析**:锚定只有该模块才有的类名,并用第二个类名(如 `_optionCopy`)校验;解析失败退回内置回退样式(功能可用、外观降级)。
|
|
32
|
-
- **官方菜单规则长期缺 `background` 和 `border-radius`**:0.1.7-rc.1 的构建把这两条属性丢了(源码里是 `var(--dsw-specific-menu)` 和 `16px`),0.2.0-rc.1 依旧如此、类名 hash 还是 `_7KE1Ra`,官方菜单本身仍是全透明+直角。本插件不依赖那条规则:运行时从 `document.body` 解析 `--dsw-specific-menu` 内联补上背景,并补 `var(--dsw-radius-lg)` 圆角。
|
|
33
|
-
- **0.2.0 起 `--dsw-specific-menu` 变成半透明**(=`--dsw-menu-surface-fill`,浅色 `#f8f9fa94` / 深色 `#43454a73`)。官方菜单的"磨砂"其实是 **`MenuSurface` 原语**的 material 层:半透明填充 + `--dsw-menu-backdrop-filter` 模糊;`_menu` 类只管几何(且长期缺 background)。只把 token 内联当背景、没有模糊,观感是"透"而不是"磨砂"——composer 菜单已改走 `MenuSurface`,内联 token 只作为原语缺失时的回退。
|
|
34
|
-
- **composer 菜单的层级跟官方 `ModelSelect` 对齐**:根层 = 「模型 / 推理等级」两个 cell,各 drill 进自己的列表;「模型」层放本插件的改进(供应商分组、可折叠、按保存顺序),「推理等级」层是官方样式的单选列表。无返回行(官方就没有):Esc 逐层返回,选中或点外部关闭。↑/↓ 在当前层循环移焦点,drill/返回时焦点交接(进层聚焦首行或搜索框,回根层聚焦首 cell)。搜索框(rc.2+)也照搬:>4 个模型才出现、`rankByName` 排序、清除按钮回焦输入框、搜索时强制展开全部分组;rc.1 的样式表没有 search 类,解析器以 `hasSearchRow` 探测,没有就不渲染。**portal 必须作为根 div 的 React 子节点**(合成事件沿 React 树冒泡而非 DOM 树),否则菜单内的 ↑/↓ 收不到。
|
|
35
|
-
- **0.2.0-rc.2 的 CSS hash 从 `_7KE1Ra` 变为 `_wq12jW`**(新增 search 类所致),`.menu` 规则逐属性未变——运行时解析不受影响,但别在任何地方硬编码 hash。
|
|
36
|
-
- **选择失败走官方 Toast**(`P.Toast`,`text/icon/anchor/onDone`)报错,不静默;select 面具把 `RemoteResult.error` 透传给组件。**设置页卡片顺序用 CSS `order` 镜像**(`.rows` 本就是 flex column):卡片 DOM 里没有供应商标识,按 `rowName` 文本↔`displayName` 匹配,同名供应商会歧义;镜像锚在 seat 原位的隐藏标记上,MutationObserver
|
|
37
|
-
- **0.2.0-rc.1 核验记录**:`configForms` 仍在、`settingsScope` 已彻底移除(`apply()` 里双注入两个名字的写法继续有效);`settings.models.footer` 与 `conversation.input.model` 两个座位均未改名;官方 CSS hash 未变,运行时类名解析不用改。
|
|
38
|
-
- **浮层必须 `ReactDOM.createPortal` 到 `document.body`**:座位在设置页很深的子树里,`position: fixed` 会被困在那个子树的堆叠上下文,被别的插件的悬浮组件盖住 —— 提高 z-index 也没用。
|
|
39
|
-
- **不要占用 `settings.models.provider-card`**:它是 keyed 座位,key 是供应商的 settings namespace,已被 `@linxin666/dsh-client-ui-model-capabilities` 占用;keyed 座位一个 key 只留一个占用者,同 key 同优先级注册会抛错(而且对方用 try/catch 吞掉,等于静默压制它)。本插件只用自己的 `settings.models.footer`(list 座位,按 id 去重)。
|
|
40
|
-
|
|
41
|
-
## 开发环境
|
|
42
|
-
|
|
43
|
-
- **`file:` 依赖是打包快照,`link:` 才是符号链接。** profile 里写 `"file:../../../dsh-model-organizer"` 时 pnpm 把目录打包**复制**进 store,DSH 加载的是 `profiles/<p>/node_modules/.../lib/client.js` 那份副本 —— 改仓库**不生效**。
|
|
44
|
-
排查:grep 那份副本的 `BUILD` 常量,和仓库对比。
|
|
45
|
-
修法:`dsh plugin --profile
|
|
46
|
-
- **改完要 Ctrl+Shift+R**:客户端 bundle 以 `Cache-Control: immutable, max-age=31536000` 下发,且 URL 不随代码变化,普通刷新一直用缓存。
|
|
47
|
-
- **浏览器侧(`lib/client.js`)只需 `react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives`**——三者都是 shell 内置提供给插件的模块(PLATFORM_MODULES),所以 `dsh.client` 里不需要 `external` 声明。
|
|
48
|
-
- profile bundle 必须声明 `dsh.bundle.patch`(指向 `cordis.patch.yml`),否则 dsh 启动即报 `declares no dsh.bundle`。
|
|
49
|
-
|
|
50
|
-
## 写 UI 时踩过的坑
|
|
51
|
-
|
|
52
|
-
- **primitives 的新组件(MenuSurface/Input/StateDot 等)是 `React.forwardRef` 包装的——是对象不是函数**。`typeof X === "function"` 永远为 false,所有这类门控静默失败、全部走降级路径(本插件因此丢过 MenuSurface 的磨砂和整个搜索框)。组件判定用 `isComponent`:函数,或带 `.render` 函数的对象。`rankByName` 这类工具函数才是真函数。
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- **
|
|
59
|
-
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
# 开发笔记
|
|
2
|
+
|
|
3
|
+
面向改这个插件的人。用户文档在 [README.md](./README.md)。
|
|
4
|
+
|
|
5
|
+
## 升级 DSH 后座位凭空消失?先查服务名
|
|
6
|
+
|
|
7
|
+
cordis 的 `ctx.inject([...])` **只要有一个服务永不出现,回调就永远不执行** —— 不报错、不打日志。所以服务改名会让整个座位静默消失。
|
|
8
|
+
|
|
9
|
+
已发生一次:0.1.7 把客户端设置镜像服务 `settingsScope` 改名为 `configForms`(提供者仍是 `@deepseek-ai/dsh-client-ui-settings`,`describe()` 返回同一个 mirror)。设置页浮窗因此消失,而输入框菜单照常(它注入的 `slots/modelDirectories/sessions` 没变)。
|
|
10
|
+
|
|
11
|
+
`apply()` 里同时注入两个名字,一次性标志保证只挂载一次:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
ctx.inject(["slots", "configForms", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.configForms.describe()));
|
|
15
|
+
ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.settingsScope.describe()));
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**规律**:升级后座位不见了,第一件事是去新版 `dsh-client-ui-*` 包里 grep 自己注入的每个服务名。
|
|
19
|
+
|
|
20
|
+
## 座位的失败模式(决定了代码结构)
|
|
21
|
+
|
|
22
|
+
- **条目渲染时抛错 → 被静默弃权,官方内置 UI 自动回填**,界面上没有任何提示。
|
|
23
|
+
- **座位注册本身失败 → 该座位静默消失**,其它功能不受影响。
|
|
24
|
+
- 所以:每个座位注册各套一层 `guard`(失败只记 warn,不拖垮插件);组件外套 `PanelBoundary`(把抛错变成页面上的一行红字,而不是"凭空消失")。
|
|
25
|
+
- **顶层 `inject` 必须列全座位 standardProps 会读的每个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。少一个 → 渲染抛 `cannot get property "..." without inject` → 弃权。用 `remote.settings.mutate` 必须显式声明 `remote.settings`。
|
|
26
|
+
- `single` 座位在**同一优先级已有注册**时会直接抛错。不写 `priority` 时,运行时会为非 chain 座位自动分配一个唯一的优先级(后注册的更低,渲染时胜出),所以只有显式写了撞车的 `priority` 才会抛。
|
|
27
|
+
- 未声明的局部变量(`ReferenceError`)同样会导致弃权,表现就是"整个面板不见了"。
|
|
28
|
+
|
|
29
|
+
## 与官方模块的耦合
|
|
30
|
+
|
|
31
|
+
- **官方 CSS-module 类名靠运行时解析**:锚定只有该模块才有的类名,并用第二个类名(如 `_optionCopy`)校验;解析失败退回内置回退样式(功能可用、外观降级)。
|
|
32
|
+
- **官方菜单规则长期缺 `background` 和 `border-radius`**:0.1.7-rc.1 的构建把这两条属性丢了(源码里是 `var(--dsw-specific-menu)` 和 `16px`),0.2.0-rc.1 依旧如此、类名 hash 还是 `_7KE1Ra`,官方菜单本身仍是全透明+直角。本插件不依赖那条规则:运行时从 `document.body` 解析 `--dsw-specific-menu` 内联补上背景,并补 `var(--dsw-radius-lg)` 圆角。
|
|
33
|
+
- **0.2.0 起 `--dsw-specific-menu` 变成半透明**(=`--dsw-menu-surface-fill`,浅色 `#f8f9fa94` / 深色 `#43454a73`)。官方菜单的"磨砂"其实是 **`MenuSurface` 原语**的 material 层:半透明填充 + `--dsw-menu-backdrop-filter` 模糊;`_menu` 类只管几何(且长期缺 background)。只把 token 内联当背景、没有模糊,观感是"透"而不是"磨砂"——composer 菜单已改走 `MenuSurface`,内联 token 只作为原语缺失时的回退。
|
|
34
|
+
- **composer 菜单的层级跟官方 `ModelSelect` 对齐**:根层 = 「模型 / 推理等级」两个 cell,各 drill 进自己的列表;「模型」层放本插件的改进(供应商分组、可折叠、按保存顺序),「推理等级」层是官方样式的单选列表。无返回行(官方就没有):Esc 逐层返回,选中或点外部关闭。↑/↓ 在当前层循环移焦点,drill/返回时焦点交接(进层聚焦首行或搜索框,回根层聚焦首 cell)。搜索框(rc.2+)也照搬:>4 个模型才出现、`rankByName` 排序、清除按钮回焦输入框、搜索时强制展开全部分组;rc.1 的样式表没有 search 类,解析器以 `hasSearchRow` 探测,没有就不渲染。**portal 必须作为根 div 的 React 子节点**(合成事件沿 React 树冒泡而非 DOM 树),否则菜单内的 ↑/↓ 收不到。
|
|
35
|
+
- **0.2.0-rc.2 的 CSS hash 从 `_7KE1Ra` 变为 `_wq12jW`**(新增 search 类所致),`.menu` 规则逐属性未变——运行时解析不受影响,但别在任何地方硬编码 hash。
|
|
36
|
+
- **选择失败走官方 Toast**(`P.Toast`,`text/icon/anchor/onDone`)报错,不静默;select 面具把 `RemoteResult.error` 透传给组件。**设置页卡片顺序用 CSS `order` 镜像**(`.rows` 本就是 flex column):卡片 DOM 里没有供应商标识,按 `rowName` 文本↔`displayName` 匹配,同名供应商会歧义;镜像锚在 seat 原位的隐藏标记上,MutationObserver 兜底卡片增删。**未匹配的卡片(官方供应商、添加草稿)刻意钉在最前**:给它们写空 `order`(默认值 0)而不是 `1000+`,既排在已配置供应商之前,又顺手清掉历史脏值。观察器盯的是**容纳列表的区段容器**、而不是列表本身——列表会被 React 整体替换,盯它就会在替换后失效;另加 rAF + 300ms 两次兜底,覆盖首屏卡片晚到的那一趟。**对话框菜单里未匹配的供应商同样置顶**:`applyProviderOrder(ids, preferred, true)`,且 id 按最后一段比较(菜单给的是路由 id `llm-pi-ai/amd`,设置页存的是 `amd`)。
|
|
37
|
+
- **0.2.0-rc.1 核验记录**:`configForms` 仍在、`settingsScope` 已彻底移除(`apply()` 里双注入两个名字的写法继续有效);`settings.models.footer` 与 `conversation.input.model` 两个座位均未改名;官方 CSS hash 未变,运行时类名解析不用改。
|
|
38
|
+
- **浮层必须 `ReactDOM.createPortal` 到 `document.body`**:座位在设置页很深的子树里,`position: fixed` 会被困在那个子树的堆叠上下文,被别的插件的悬浮组件盖住 —— 提高 z-index 也没用。
|
|
39
|
+
- **不要占用 `settings.models.provider-card`**:它是 keyed 座位,key 是供应商的 settings namespace,已被 `@linxin666/dsh-client-ui-model-capabilities` 占用;keyed 座位一个 key 只留一个占用者,同 key 同优先级注册会抛错(而且对方用 try/catch 吞掉,等于静默压制它)。本插件只用自己的 `settings.models.footer`(list 座位,按 id 去重)。
|
|
40
|
+
|
|
41
|
+
## 开发环境
|
|
42
|
+
|
|
43
|
+
- **`file:` 依赖是打包快照,`link:` 才是符号链接。** profile 里写 `"file:../../../dsh-model-organizer"` 时 pnpm 把目录打包**复制**进 store,DSH 加载的是 `profiles/<p>/node_modules/.../lib/client.js` 那份副本 —— 改仓库**不生效**。
|
|
44
|
+
排查:grep 那份副本的 `BUILD` 常量,和仓库对比。
|
|
45
|
+
修法:`dsh plugin --profile <profile> remove @nimoxie/dsh-model-organizer && dsh plugin --profile <profile> add link:./dsh-model-organizer`
|
|
46
|
+
- **改完要 Ctrl+Shift+R**:客户端 bundle 以 `Cache-Control: immutable, max-age=31536000` 下发,且 URL 不随代码变化,普通刷新一直用缓存。
|
|
47
|
+
- **浏览器侧(`lib/client.js`)只需 `react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives`**——三者都是 shell 内置提供给插件的模块(PLATFORM_MODULES),所以 `dsh.client` 里不需要 `external` 声明。
|
|
48
|
+
- profile bundle 必须声明 `dsh.bundle.patch`(指向 `cordis.patch.yml`),否则 dsh 启动即报 `declares no dsh.bundle`。
|
|
49
|
+
|
|
50
|
+
## 写 UI 时踩过的坑
|
|
51
|
+
|
|
52
|
+
- **primitives 的新组件(MenuSurface/Input/StateDot 等)是 `React.forwardRef` 包装的——是对象不是函数**。`typeof X === "function"` 永远为 false,所有这类门控静默失败、全部走降级路径(本插件因此丢过 MenuSurface 的磨砂和整个搜索框)。组件判定用 `isComponent`:函数,或带 `.render` 函数的对象。`rankByName` 这类工具函数才是真函数。
|
|
53
|
+
- **图标名要用 primitives 实际导出的名字**:只有 `IconXxxOutlineRegular` / `IconXxxOutlineMedium` / `IconXxxFillRegular` 这类后缀,**没有 `IconXxxOutline16`/`14` 尺寸数字名**。猜错名字 `icon()` 静默返回 null——本插件的勾、分组箭头因此从第一版起就没渲染过,靠触发器的文字兜底掩盖至今。发新图标前先 grep 导出表。
|
|
54
|
+
|
|
55
|
+
- **边框必须用 longhand**(`borderWidth/borderStyle/borderColor`)。用 shorthand `border` 再叠加 `borderColor` 时 React 会展开 shorthand;回退时移除 `borderColor`,`border-color` 落回 `currentColor` → "拖拽结束后高亮边框一直不消失"。
|
|
56
|
+
- **拖拽会把源行留在焦点上**:行加 `tabIndex:-1`,drop/dragend 时 `blur()`;并且不要给行画 `:focus-visible` 背景(官方 `option` 类自带一条 → "拖完还留一块灰底")。
|
|
57
|
+
- **原生拖拽会让文字变成可拖对象**(浏览器弹「松开鼠标即可搜索」):`-webkit-user-drag:none` 加在**行的内容**上,不要加在行本身(加在行上会让行也拖不动)。
|
|
58
|
+
- **drop 会在行与容器上各触发一次**(冒泡),需加锁,否则重复写入。
|
|
59
|
+
- **primitive 图标不转发 inline `style`**:需要旋转/变色请传 `className`。
|
|
60
|
+
- **`providers` 是 record,键顺序由宿主规范化**:整体 `set` 与 `unset`+`set` 都实测无效,所以供应商顺序只能存 `localStorage`(模型顺序是数组,可以写进文档)。
|
|
61
|
+
|
|
62
|
+
## 发版
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
# npm 不允许覆盖已发布的版本号:先升版本,再提交,最后发布
|
|
66
|
+
npm version patch # 或手改 package.json 的 version
|
|
67
|
+
git add -A && git commit -m "..." && git push
|
|
68
|
+
npm publish
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
发布需要带 **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`,不要放进仓库)。
|
|
72
|
+
|
|
73
|
+
包是 scoped 名(`@nimoxie/dsh-model-organizer`,用户作用域),`publishConfig.access: "public"` 已写进 package.json,`npm publish` 不用再带参数。老的无作用域名 `dsh-model-organizer` 已停更,记得发一次 `npm deprecate dsh-model-organizer "已迁移到 @nimoxie/dsh-model-organizer"` 留路标。
|
|
74
|
+
|
|
75
|
+
**改包名是牵一发动全身的**——包名是宿主解析插件的文件系统坐标,不止 package.json:`cordis.patch.yml` 的 `insert.name`(按包名从 node_modules 找插件,找不到则插件整体不加载)、`lib/client.js` 的 `__ModuleLoader__.load({ id })`(模块 id 必须等于包名,否则 bundle 二次执行时被判定为重复抢注,boot 直接失败)。localStorage 键和 locale 命名空间是纯数据键,刻意不改(改了丢用户排序数据)。
|
package/README.md
CHANGED
|
@@ -4,53 +4,54 @@
|
|
|
4
4
|
|
|
5
5
|
## 它解决什么问题
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
官方那个模型下拉是一个扁平长列表——供应商只是粘性小标题,模型多的时候要滚很久,也没法按自己的习惯排序。这个插件把它改成两级:**根层先选「模型」或「推理等级」,模型层里按供应商分组**,供应商和模型的顺序都能直接拖。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
11
|
**输入框的模型菜单**
|
|
12
|
-
-
|
|
13
|
-
-
|
|
12
|
+
- 根层与官方一致:「模型」「推理等级」两个入口——「推理等级」**常驻**,未配置等级的模型上置灰、不可点
|
|
13
|
+
- 「模型」里保留本插件的改进:按供应商分组、可折叠,顺序与设置页浮窗一致,当前模型字体加粗高亮
|
|
14
14
|
- 模型多于 4 个时,「模型」层顶部有官方同款的「搜索模型」框(0.2.0-rc.2 起官方才有此样式,旧版自动隐藏)
|
|
15
15
|
- 外观完全沿用官方样式(同一个菜单表面、同一套图标,含官方的毛玻璃背景)
|
|
16
16
|
|
|
17
17
|
**排序**
|
|
18
18
|
- 在「设置 → 模型」右下角的浮窗里,**直接拖动**供应商行或模型行
|
|
19
|
-
-
|
|
19
|
+
- 模型顺序通过官方设置接口持久化(跟着 DSH 的配置走,不存浏览器);供应商顺序只记在当前浏览器里
|
|
20
20
|
|
|
21
21
|
## 安装
|
|
22
22
|
|
|
23
|
+
`<profile>` 是你的目标环境:浏览器版用 `web`,桌面客户端用 `desktop`。
|
|
24
|
+
|
|
23
25
|
```sh
|
|
24
|
-
dsh plugin --profile
|
|
26
|
+
dsh plugin --profile <profile> add @nimoxie/dsh-model-organizer
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
- 浏览器版(`web`):装完重启服务(关掉再 `dsh web`)。
|
|
30
|
+
- 桌面客户端(`desktop`):桌面 profile 由客户端独占管理,请在客户端的「插件管理」页添加本包(包名 `@nimoxie/dsh-model-organizer`)。
|
|
31
|
+
|
|
32
|
+
卸载(CLI 可管理的环境):
|
|
28
33
|
|
|
29
34
|
```sh
|
|
30
|
-
dsh plugin --profile
|
|
35
|
+
dsh plugin --profile <profile> remove @nimoxie/dsh-model-organizer
|
|
31
36
|
```
|
|
32
37
|
|
|
33
|
-
也可以从 GitHub
|
|
38
|
+
也可以从 GitHub 或本地目录装(开发用):
|
|
34
39
|
|
|
35
40
|
```sh
|
|
36
|
-
dsh plugin --profile
|
|
37
|
-
dsh plugin --profile
|
|
41
|
+
dsh plugin --profile <profile> add github:NimoXie15/dsh-model-organizer
|
|
42
|
+
dsh plugin --profile <profile> add link:/absolute/path/to/dsh-model-organizer
|
|
38
43
|
```
|
|
39
44
|
|
|
40
|
-
> **兼容性**:已在 DSH `0.1.7`、`0.2.0-rc.1
|
|
45
|
+
> **兼容性**:已在 DSH `0.1.7`、`0.2.0-rc.1`(web)、`0.2.0-rc.2`(桌面端)上核验,`0.1.5` 起都可用。
|
|
41
46
|
>
|
|
42
47
|
> 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(浏览器可能还在用缓存的旧版本)。
|
|
43
48
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
📷 待补充。
|
|
47
|
-
|
|
48
|
-
## 想让「推理等级」出现,需要模型自己声明
|
|
49
|
+
## 想让「推理等级」可用,需要模型自己声明
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
「推理等级」入口常驻菜单根层;只有模型声明了等级,它才可点、才会在按钮上显示当前等级。在模型配置里加上(`llm-pi-ai.providers.<供应商>.models[]`;0.2.x 在 profile 的 `cordis.patch.yml`,0.1.x 在 `settings.yaml`):
|
|
51
52
|
|
|
52
53
|
```yaml
|
|
53
|
-
#
|
|
54
|
+
# llm-pi-ai.providers.<供应商>.models[](0.2.x 在 profile 的 cordis.patch.yml;0.1.x 在 settings.yaml)
|
|
54
55
|
- id: <你的模型 ID>
|
|
55
56
|
reasoningEfforts: # 键=菜单里显示的等级,值=选中时发给模型的 API 参数;只有 off 可以留空
|
|
56
57
|
off:
|
|
@@ -59,8 +60,8 @@ dsh plugin --profile web add link:/absolute/path/to/dsh-model-organizer
|
|
|
59
60
|
|
|
60
61
|
## 已知限制
|
|
61
62
|
|
|
62
|
-
- 排序浮窗只列「设置 →
|
|
63
|
-
- 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在
|
|
63
|
+
- 排序浮窗只列「设置 → 模型」里自己添加的供应商;**DeepSeek 官方供应商不能排序**,它们在设置页卡片和输入框菜单里都**固定在顶部**,照常出现、可选
|
|
64
|
+
- 供应商顺序、浮窗位置只存在当前浏览器;模型顺序存在 DSH 的用户配置里,同一份 DSH 的所有设备共用——两台机器各装一份 DSH 则互不相通
|
|
64
65
|
- 不修改 DSH 本体,只通过官方提供的扩展点接入
|
|
65
66
|
|
|
66
67
|
## 许可
|
|
@@ -69,4 +70,4 @@ MIT
|
|
|
69
70
|
|
|
70
71
|
---
|
|
71
72
|
|
|
72
|
-
想自己改这个插件、或者想了解它内部怎么实现的,见 [
|
|
73
|
+
想自己改这个插件、或者想了解它内部怎么实现的,见 [DEVELOPMENT.md](./DEVELOPMENT.md)。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# dsh-model-organizer bundle patch: one host row over the web profile.
|
|
1
|
+
# @nimoxie/dsh-model-organizer bundle patch: one host row over the web profile.
|
|
2
2
|
# The row name resolves from the profile's own node_modules where
|
|
3
3
|
# `dsh plugin --profile web add <path>` installed this package.
|
|
4
4
|
- insert:
|
|
5
5
|
- id: model-organizer
|
|
6
|
-
name: dsh-model-organizer
|
|
6
|
+
name: "@nimoxie/dsh-model-organizer"
|
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({
|
|
2
|
-
id: "dsh-model-organizer",
|
|
2
|
+
id: "@nimoxie/dsh-model-organizer",
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
const module = { exports: {} };
|
|
5
5
|
const exports = module.exports;
|
|
@@ -42,6 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
"composer.searchClear": "清除搜索",
|
|
43
43
|
"composer.searchEmpty": "没有匹配的模型。",
|
|
44
44
|
"composer.selectFailed": "选择失败:{msg}",
|
|
45
|
+
"composer.effortNone": "未配置",
|
|
45
46
|
"composer.none": "未选择模型",
|
|
46
47
|
"composer.empty": "暂无可用模型",
|
|
47
48
|
"composer.locked": "当前不可切换模型",
|
|
@@ -51,7 +52,6 @@ window.__ModuleLoader__.load({
|
|
|
51
52
|
"composer.effort": "推理等级",
|
|
52
53
|
"composer.effortDefault": "Default",
|
|
53
54
|
"composer.effortEmpty": "当前模型未提供推理等级。",
|
|
54
|
-
"composer.effortBack": "模型",
|
|
55
55
|
"composer.refreshing": "正在刷新模型列表…",
|
|
56
56
|
"order.title": "模型顺序",
|
|
57
57
|
"order.hint": "拖动 ⠿ 调整,松手即保存",
|
|
@@ -72,6 +72,7 @@ window.__ModuleLoader__.load({
|
|
|
72
72
|
"composer.searchClear": "Clear search",
|
|
73
73
|
"composer.searchEmpty": "No matching models.",
|
|
74
74
|
"composer.selectFailed": "Selection failed: {msg}",
|
|
75
|
+
"composer.effortNone": "Not configured",
|
|
75
76
|
"composer.none": "No model",
|
|
76
77
|
"composer.empty": "No models available",
|
|
77
78
|
"composer.locked": "Model switching unavailable",
|
|
@@ -81,7 +82,6 @@ window.__ModuleLoader__.load({
|
|
|
81
82
|
"composer.effort": "Effort",
|
|
82
83
|
"composer.effortDefault": "Default",
|
|
83
84
|
"composer.effortEmpty": "This model provides no reasoning effort levels.",
|
|
84
|
-
"composer.effortBack": "Model",
|
|
85
85
|
"composer.refreshing": "Refreshing model list…",
|
|
86
86
|
"order.title": "Model order",
|
|
87
87
|
"order.hint": "Drag ⠿ to reorder, then save",
|
|
@@ -115,7 +115,7 @@ window.__ModuleLoader__.load({
|
|
|
115
115
|
code changes, so a browser can keep running an old build until a hard reload.
|
|
116
116
|
Kept OFF the UI (it is not user-facing): read it as
|
|
117
117
|
`window.__dshModelOrganizerBuild` when a stale build is suspected. */
|
|
118
|
-
const BUILD = "
|
|
118
|
+
const BUILD = "b33";
|
|
119
119
|
|
|
120
120
|
let officialClasses = null;
|
|
121
121
|
let officialClassesSheets = -1;
|
|
@@ -232,6 +232,11 @@ window.__ModuleLoader__.load({
|
|
|
232
232
|
/* (the header's own arrow button is gone — the row toggles on click) */
|
|
233
233
|
|
|
234
234
|
".dsh-mo-model{padding-left:28px;position:relative;}",
|
|
235
|
+
/* Permanent-but-unconfigured effort cell: caption tone (the search
|
|
236
|
+
placeholder's token — dimmed is unreadably faint), not-allowed
|
|
237
|
+
cursor, hover-proof (doubled selectors outrank the shipped rules). */
|
|
238
|
+
".dsh-mo-cell-disabled,.dsh-mo-cell-disabled.dsh-mo-cell-disabled{color:var(--dsw-alias-label-caption);cursor:not-allowed;}",
|
|
239
|
+
".dsh-mo-cell-disabled:hover,.dsh-mo-cell-disabled.dsh-mo-cell-disabled:hover{background:transparent;cursor:not-allowed;}",
|
|
235
240
|
".dsh-mo-model::before{content:'';position:absolute;left:15px;top:50%;width:5px;height:5px;margin-top:-2.5px;border-radius:50%;background:var(--dsw-alias-label-tertiary);opacity:.5;}",
|
|
236
241
|
".dsh-mo-model[aria-checked='true']::before{background:var(--dsw-alias-label-primary);opacity:.9;}"
|
|
237
242
|
];
|
|
@@ -240,6 +245,8 @@ window.__ModuleLoader__.load({
|
|
|
240
245
|
aria-checked, so the drag lists are untouched. */
|
|
241
246
|
if (C !== null) {
|
|
242
247
|
rules.push("." + C.option + "[aria-checked='true'] ." + C.modelName + "{font-weight:600}");
|
|
248
|
+
/* cellValue paints its own color; fold it back to the dimmed cell's. */
|
|
249
|
+
rules.push(".dsh-mo-cell-disabled ." + C.cellValue + "{color:inherit;}");
|
|
243
250
|
}
|
|
244
251
|
style.textContent = rules.join("");
|
|
245
252
|
} catch (e) {}
|
|
@@ -602,7 +609,7 @@ window.__ModuleLoader__.load({
|
|
|
602
609
|
const ids = [];
|
|
603
610
|
const byId = {};
|
|
604
611
|
for (let i = 0; i < groups.length; i++) { ids.push(groups[i].id); byId[groups[i].id] = groups[i]; }
|
|
605
|
-
const ordered = applyProviderOrder(ids, readProviderOrder());
|
|
612
|
+
const ordered = applyProviderOrder(ids, readProviderOrder(), true);
|
|
606
613
|
const out = [];
|
|
607
614
|
for (let i = 0; i < ordered.length; i++) if (byId[ordered[i]] !== undefined) out.push(byId[ordered[i]]);
|
|
608
615
|
return out;
|
|
@@ -815,12 +822,12 @@ window.__ModuleLoader__.load({
|
|
|
815
822
|
.split("{model}").join(label)
|
|
816
823
|
.split("{effort}").join(effortLabel === undefined ? "" : effortLabel);
|
|
817
824
|
|
|
818
|
-
const chevronIcon = icon("
|
|
825
|
+
const chevronIcon = icon("IconChevronDownOutlineRegular", {
|
|
819
826
|
className: C !== null ? cx(C.chevron, open && C.chevronOpen) : undefined,
|
|
820
827
|
style: C === null ? S.caret : undefined
|
|
821
828
|
});
|
|
822
829
|
const triggerChildren = [];
|
|
823
|
-
const triggerGlyph = icon("
|
|
830
|
+
const triggerGlyph = icon("IconDataOutlineRegular", { key: "g", className: C !== null ? C.triggerIcon : undefined, size: 16 });
|
|
824
831
|
if (triggerGlyph !== null) triggerChildren.push(triggerGlyph);
|
|
825
832
|
triggerChildren.push(h("span", { key: "l", className: C !== null ? C.triggerLabel : undefined, style: C === null ? S.chipLabel : undefined }, label));
|
|
826
833
|
if (effortLabel !== undefined) {
|
|
@@ -892,7 +899,7 @@ window.__ModuleLoader__.load({
|
|
|
892
899
|
h("span", { key: "n", className: C !== null ? C.cellLabel : undefined, style: C === null ? undefined : undefined }, group.name || group.id),
|
|
893
900
|
h("span", { key: "v", className: C !== null ? C.cellValue : undefined, style: C === null ? S.groupCount : undefined }, group.models.length + " " + t("composer.count"))
|
|
894
901
|
];
|
|
895
|
-
const groupChevron = icon("
|
|
902
|
+
const groupChevron = icon("IconChevronRightOutlineRegular", {
|
|
896
903
|
key: "i",
|
|
897
904
|
className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
|
|
898
905
|
style: C === null ? S.groupChevron : undefined
|
|
@@ -911,7 +918,7 @@ window.__ModuleLoader__.load({
|
|
|
911
918
|
|
|
912
919
|
const items = !isOpen ? [] : group.models.map(function (model) {
|
|
913
920
|
const selected = current !== null && current.provider === group.id && current.model === model.id;
|
|
914
|
-
const check = selected ? icon("
|
|
921
|
+
const check = selected ? icon("IconCheckOutlineRegular", { key: "k" }) : null;
|
|
915
922
|
return h("button", {
|
|
916
923
|
key: model.id,
|
|
917
924
|
ref: itemRef(),
|
|
@@ -956,23 +963,26 @@ window.__ModuleLoader__.load({
|
|
|
956
963
|
},
|
|
957
964
|
h("span", { key: "l", className: C !== null ? C.cellLabel : undefined }, t("composer.menuModel")),
|
|
958
965
|
h("span", { key: "v", className: C !== null ? C.cellValue : undefined }, label),
|
|
959
|
-
icon("
|
|
966
|
+
icon("IconChevronRightOutlineRegular", { key: "c", className: C !== null ? C.cellChevron : undefined })
|
|
967
|
+
));
|
|
968
|
+
/* The effort entry is PERMANENT (unlike the shipped conditional one):
|
|
969
|
+
models without declared levels render it dimmed, unclickable and
|
|
970
|
+
skipped by the keyboard walk, so the root pair never reflows. */
|
|
971
|
+
const effortEnabled = reasoning !== undefined;
|
|
972
|
+
paneChildren.push(h("button", {
|
|
973
|
+
key: "effort",
|
|
974
|
+
ref: itemRef(),
|
|
975
|
+
type: "button",
|
|
976
|
+
role: "menuitem",
|
|
977
|
+
className: C !== null ? cx(C.cell, !effortEnabled && "dsh-mo-cell-disabled") : undefined,
|
|
978
|
+
style: C === null ? S.fallbackGroupTitle : undefined,
|
|
979
|
+
disabled: !effortEnabled,
|
|
980
|
+
onClick: effortEnabled ? function () { setPane("effort"); } : undefined
|
|
981
|
+
},
|
|
982
|
+
h("span", { key: "l", className: C !== null ? C.cellLabel : undefined }, t("composer.effort")),
|
|
983
|
+
h("span", { key: "v", className: C !== null ? C.cellValue : undefined }, effortEnabled ? effortLabel : t("composer.effortNone")),
|
|
984
|
+
icon("IconChevronRightOutlineRegular", { key: "c", className: C !== null ? cx(C.cellChevron, !effortEnabled && "dsh-mo-cell-disabled") : undefined })
|
|
960
985
|
));
|
|
961
|
-
if (reasoning !== undefined) {
|
|
962
|
-
paneChildren.push(h("button", {
|
|
963
|
-
key: "effort",
|
|
964
|
-
ref: itemRef(),
|
|
965
|
-
type: "button",
|
|
966
|
-
role: "menuitem",
|
|
967
|
-
className: C !== null ? C.cell : undefined,
|
|
968
|
-
style: C === null ? S.fallbackGroupTitle : undefined,
|
|
969
|
-
onClick: function () { setPane("effort"); }
|
|
970
|
-
},
|
|
971
|
-
h("span", { key: "l", className: C !== null ? C.cellLabel : undefined }, t("composer.effort")),
|
|
972
|
-
h("span", { key: "v", className: C !== null ? C.cellValue : undefined }, effortLabel),
|
|
973
|
-
icon("IconChevronRightOutline14", { key: "c", className: C !== null ? C.cellChevron : undefined })
|
|
974
|
-
));
|
|
975
|
-
}
|
|
976
986
|
} else if (pane === "effort") {
|
|
977
987
|
if (effortChoices.length === 0) {
|
|
978
988
|
paneChildren.push(h("div", { key: "ee", className: C !== null ? C.empty : undefined, style: C === null ? S.menuEmpty : undefined }, t("composer.effortEmpty")));
|
|
@@ -983,7 +993,7 @@ window.__ModuleLoader__.load({
|
|
|
983
993
|
style: C === null ? S.fallbackGroups : undefined
|
|
984
994
|
}, effortChoices.map(function (level) {
|
|
985
995
|
const selected = effectiveEffort === level.effort;
|
|
986
|
-
const check = selected ? icon("
|
|
996
|
+
const check = selected ? icon("IconCheckOutlineRegular", { key: "k" }) : null;
|
|
987
997
|
return h("button", {
|
|
988
998
|
key: level.key,
|
|
989
999
|
ref: itemRef(),
|
|
@@ -1221,7 +1231,7 @@ window.__ModuleLoader__.load({
|
|
|
1221
1231
|
if (note === "ok") notes.push(h("span", { key: "ok", style: S.ok }, t("order.saved")));
|
|
1222
1232
|
else if (note !== null) notes.push(h("span", { key: "n", style: S.dirty }, note));
|
|
1223
1233
|
for (let i = 0; i < notes.length; i++) headChildren.push(notes[i]);
|
|
1224
|
-
const headChevron = icon("
|
|
1234
|
+
const headChevron = icon("IconChevronRightOutlineRegular", {
|
|
1225
1235
|
key: "c",
|
|
1226
1236
|
className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
|
|
1227
1237
|
style: C === null ? S.groupChevron : undefined
|
|
@@ -1292,7 +1302,6 @@ window.__ModuleLoader__.load({
|
|
|
1292
1302
|
try { window.localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids)); } catch (e) {}
|
|
1293
1303
|
}
|
|
1294
1304
|
|
|
1295
|
-
const PANEL_STORAGE_KEY = "dsh-model-organizer.panelOpen";
|
|
1296
1305
|
const PANEL_POS_KEY = "dsh-model-organizer.panelPos";
|
|
1297
1306
|
|
|
1298
1307
|
|
|
@@ -1311,30 +1320,33 @@ window.__ModuleLoader__.load({
|
|
|
1311
1320
|
try { window.localStorage.setItem(PANEL_POS_KEY, JSON.stringify(pos)); } catch (e) {}
|
|
1312
1321
|
}
|
|
1313
1322
|
|
|
1314
|
-
/* ALWAYS expanded on mount. A remembered collapse is indistinguishable from
|
|
1315
|
-
"the default is collapsed" the next time the page opens, which is not what
|
|
1316
|
-
this panel is for. */
|
|
1317
|
-
function readPanelOpen() {
|
|
1318
|
-
return true;
|
|
1319
|
-
}
|
|
1320
1323
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1324
|
+
/* Provider ids come from two different worlds: the Settings page states them plainly
|
|
1325
|
+
("amd"), while the composer menu reads the host's model directory, whose ids may be
|
|
1326
|
+
route ids ("llm-pi-ai/amd"). Comparing the last path segment makes the two match. */
|
|
1327
|
+
function providerKey(id) {
|
|
1328
|
+
const text = String(id);
|
|
1329
|
+
const cut = text.lastIndexOf("/");
|
|
1330
|
+
return cut === -1 ? text : text.slice(cut + 1);
|
|
1323
1331
|
}
|
|
1324
1332
|
|
|
1325
|
-
/* Stored order first, then everything else in document order
|
|
1326
|
-
|
|
1333
|
+
/* Stored order first, then everything else in document order — or the other way
|
|
1334
|
+
round when `unknownFirst` is set. The composer menu wants unknownFirst: an id no
|
|
1335
|
+
stored preference mentions is a provider this plugin does not manage (the shipped
|
|
1336
|
+
ones), and those belong on top, exactly like the Settings page pins its unlisted
|
|
1337
|
+
cards. */
|
|
1338
|
+
function applyProviderOrder(ids, preferred, unknownFirst) {
|
|
1327
1339
|
if (preferred.length === 0) return ids;
|
|
1328
1340
|
const rank = {};
|
|
1329
|
-
for (let i = 0; i < preferred.length; i++) rank[preferred[i]] = i;
|
|
1341
|
+
for (let i = 0; i < preferred.length; i++) rank[providerKey(preferred[i])] = i;
|
|
1330
1342
|
const known = [];
|
|
1331
1343
|
const rest = [];
|
|
1332
1344
|
for (let i = 0; i < ids.length; i++) {
|
|
1333
|
-
if (rank[ids[i]] === undefined) rest.push(ids[i]);
|
|
1345
|
+
if (rank[providerKey(ids[i])] === undefined) rest.push(ids[i]);
|
|
1334
1346
|
else known.push(ids[i]);
|
|
1335
1347
|
}
|
|
1336
|
-
known.sort(function (a, b) { return rank[a] - rank[b]; });
|
|
1337
|
-
return known.concat(rest);
|
|
1348
|
+
known.sort(function (a, b) { return rank[providerKey(a)] - rank[providerKey(b)]; });
|
|
1349
|
+
return unknownFirst === true ? rest.concat(known) : known.concat(rest);
|
|
1338
1350
|
}
|
|
1339
1351
|
|
|
1340
1352
|
/* A seat entry that throws is silently abdicated, so "my panel is missing" and
|
|
@@ -1395,9 +1407,11 @@ window.__ModuleLoader__.load({
|
|
|
1395
1407
|
const openState = useState({});
|
|
1396
1408
|
const openMap = openState[0];
|
|
1397
1409
|
const setOpenMap = openState[1];
|
|
1398
|
-
|
|
1410
|
+
/* The panel always mounts expanded: a remembered collapse is
|
|
1411
|
+
indistinguishable from "collapsed by default" on the next page open. */
|
|
1412
|
+
const panelState = useState(true);
|
|
1399
1413
|
const panelOpen = panelState[0];
|
|
1400
|
-
const setPanelOpen =
|
|
1414
|
+
const setPanelOpen = panelState[1];
|
|
1401
1415
|
const posState = useState(readPanelPos);
|
|
1402
1416
|
const panelPos = posState[0];
|
|
1403
1417
|
const setPanelPos = posState[1];
|
|
@@ -1427,18 +1441,23 @@ window.__ModuleLoader__.load({
|
|
|
1427
1441
|
Cards carry no provider identity in the DOM — match through the
|
|
1428
1442
|
displayName text each card renders. */
|
|
1429
1443
|
const seatRef = useRef(null);
|
|
1444
|
+
|
|
1445
|
+
/* The seat may sit inside slot wrappers between it and the section — climb until a
|
|
1446
|
+
container actually holds the card list. */
|
|
1447
|
+
const seatCardList = function () {
|
|
1448
|
+
let el = seatRef.current;
|
|
1449
|
+
for (let hop = 0; hop < 6 && el !== null; hop++) {
|
|
1450
|
+
el = el.parentElement;
|
|
1451
|
+
if (el === null) return null;
|
|
1452
|
+
const list = el.querySelector("ul");
|
|
1453
|
+
if (list !== null) return list;
|
|
1454
|
+
}
|
|
1455
|
+
return null;
|
|
1456
|
+
};
|
|
1457
|
+
|
|
1430
1458
|
const mirrorCards = function () {
|
|
1431
1459
|
try {
|
|
1432
|
-
const
|
|
1433
|
-
if (seat === null) return;
|
|
1434
|
-
/* The seat may sit inside slot wrappers between it and the section —
|
|
1435
|
-
climb until a container actually holds the card list. */
|
|
1436
|
-
let list = null;
|
|
1437
|
-
let el = seat;
|
|
1438
|
-
for (let hop = 0; hop < 6 && el !== null && list === null; hop++) {
|
|
1439
|
-
el = el.parentElement;
|
|
1440
|
-
if (el !== null) list = el.querySelector("ul");
|
|
1441
|
-
}
|
|
1460
|
+
const list = seatCardList();
|
|
1442
1461
|
if (list === null) return;
|
|
1443
1462
|
const byName = {};
|
|
1444
1463
|
for (const id in display) {
|
|
@@ -1451,27 +1470,45 @@ window.__ModuleLoader__.load({
|
|
|
1451
1470
|
if (Object.keys(byName).length === 0) return;
|
|
1452
1471
|
const rank = {};
|
|
1453
1472
|
baseIds.forEach(function (id, index) { rank[id] = index + 1; });
|
|
1454
|
-
|
|
1473
|
+
/* Configured providers take ranks 1..N. Anything else — the shipped DeepSeek
|
|
1474
|
+
card, the add-provider draft — keeps the default order 0, which sorts it
|
|
1475
|
+
ahead of the ranked cards. Clearing the inline value also drops whatever an
|
|
1476
|
+
earlier pass wrote there, so a card that used to be unranked and is ranked
|
|
1477
|
+
now (or the reverse) never keeps a stale position. */
|
|
1455
1478
|
for (let i = 0; i < list.children.length; i++) {
|
|
1456
1479
|
const li = list.children[i];
|
|
1457
1480
|
const nameEl = li.querySelector ? li.querySelector('[class*="rowName"]') : null;
|
|
1458
1481
|
const name = nameEl !== null ? nameEl.textContent : null;
|
|
1459
1482
|
const id = name !== null && byName[name] !== undefined ? byName[name] : null;
|
|
1460
1483
|
if (id !== null && rank[id] !== undefined) li.style.order = String(rank[id]);
|
|
1461
|
-
else li.style.order =
|
|
1484
|
+
else li.style.order = "";
|
|
1462
1485
|
}
|
|
1463
1486
|
} catch (e) {}
|
|
1464
1487
|
};
|
|
1465
1488
|
|
|
1466
1489
|
useEffect(function () {
|
|
1467
1490
|
mirrorCards();
|
|
1491
|
+
/* The card list is rendered asynchronously and React may replace the list node
|
|
1492
|
+
itself, so the observer watches the section wrapper that holds it — two levels
|
|
1493
|
+
above the seat, where the climb above finds the card list. That node exists
|
|
1494
|
+
from the first paint and survives the list being swapped out. Every pass
|
|
1495
|
+
re-resolves the list by climbing, mutations are coalesced to one pass per
|
|
1496
|
+
frame, and a frame plus one delayed pass cover the first paint, where the cards
|
|
1497
|
+
routinely do not exist yet. */
|
|
1498
|
+
const frame = requestAnimationFrame(function () { mirrorCards(); });
|
|
1499
|
+
const timer = setTimeout(function () { mirrorCards(); }, 300);
|
|
1500
|
+
const stop = function () { cancelAnimationFrame(frame); clearTimeout(timer); };
|
|
1468
1501
|
const seat = seatRef.current;
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
const observer = new MutationObserver(function () {
|
|
1473
|
-
|
|
1474
|
-
|
|
1502
|
+
const section = seat !== null && seat.parentElement !== null ? seat.parentElement.parentElement : null;
|
|
1503
|
+
if (section === null || typeof MutationObserver !== "function") return stop;
|
|
1504
|
+
let queued = false;
|
|
1505
|
+
const observer = new MutationObserver(function () {
|
|
1506
|
+
if (queued) return;
|
|
1507
|
+
queued = true;
|
|
1508
|
+
requestAnimationFrame(function () { queued = false; mirrorCards(); });
|
|
1509
|
+
});
|
|
1510
|
+
observer.observe(section, { childList: true, subtree: true });
|
|
1511
|
+
return function () { observer.disconnect(); stop(); };
|
|
1475
1512
|
}, [sourceKey, stored]);
|
|
1476
1513
|
|
|
1477
1514
|
useLayoutEffect(function () {
|
|
@@ -1481,8 +1518,6 @@ window.__ModuleLoader__.load({
|
|
|
1481
1518
|
}, [panelPos, panelOpen]);
|
|
1482
1519
|
|
|
1483
1520
|
const ids = order === null ? baseIds : order;
|
|
1484
|
-
const idsKey = ids.join("|");
|
|
1485
|
-
const dirty = order !== null && order.join("|") !== sourceKey;
|
|
1486
1521
|
|
|
1487
1522
|
/* The shipped provider cards are left exactly as React renders them. Ordering used
|
|
1488
1523
|
to be mirrored onto them with inline `order`; that is gone — this plugin adds a
|
|
@@ -1606,7 +1641,7 @@ window.__ModuleLoader__.load({
|
|
|
1606
1641
|
h("span", { key: "n", className: C !== null ? C.cellLabel : undefined, style: C === null ? S.rowName : undefined }, profile.displayName || id),
|
|
1607
1642
|
h("span", { key: "c", className: C !== null ? C.cellValue : undefined, style: C === null ? S.rowMeta : undefined }, count + " " + t("composer.count"))
|
|
1608
1643
|
];
|
|
1609
|
-
const rowChevron = icon("
|
|
1644
|
+
const rowChevron = icon("IconChevronRightOutlineRegular", {
|
|
1610
1645
|
key: "v",
|
|
1611
1646
|
className: C !== null ? cx(C.cellChevron, "dsh-mo-chev") : "dsh-mo-chev",
|
|
1612
1647
|
style: C === null ? S.groupChevron : undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimoxie/dsh-model-organizer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "DSH 模型整理插件:输入框的模型下拉改为「供应商分组 + 模型二级折叠菜单」(含推理等级),并在模型设置页提供一个可拖拽的「供应商与模型顺序」浮窗。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"lib",
|
|
18
18
|
"cordis.patch.yml",
|
|
19
19
|
"README.md",
|
|
20
|
-
"
|
|
20
|
+
"DEVELOPMENT.md",
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"keywords": [
|