dsh-model-groups 0.3.3 → 0.3.4
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/README.md +71 -105
- package/lib/client.js +63 -86
- package/native/pi-ai.patch.json +12 -0
- package/package.json +2 -1
- package/scripts/doctor.mjs +39 -0
- package/scripts/native-patches.mjs +8 -1
- package/scripts/setup.mjs +40 -0
- package/src/auto-patch.js +45 -0
- package/src/client.js +16 -0
- package/src/groups.css +20 -0
- package/src/index.js +29 -4
- package/src/model-select.js +46 -85
package/README.md
CHANGED
|
@@ -1,153 +1,117 @@
|
|
|
1
1
|
# DSH Model Groups
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
为 DeepSeek Harness 网页端提供分层模型菜单与多密钥管理。同一供应商共用 Provider ID、API 地址和协议,各配置分别设置名称、密钥与模型列表。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- 同一供应商的多份配置可以使用相同的模型 ID,选择时分别使用对应密钥。
|
|
7
|
-
- 支持名称搜索、全部展开、全部收起,以及折叠状态记忆。
|
|
8
|
-
- 模型和密钥由用户手动选择,不自动轮换或切换配置。
|
|
5
|
+
## 功能
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
- 按“供应商 → 密钥配置 → 模型”浏览和选择。
|
|
8
|
+
- 支持原地折叠、侧边面板、名称搜索、全部展开与收起。
|
|
9
|
+
- 记住各级折叠状态;搜索时临时展开匹配项。
|
|
10
|
+
- 不同密钥配置可使用相同模型 ID,选择时使用对应配置的密钥。
|
|
11
|
+
- 手动选择配置,不自动轮换密钥或切换到其他配置。
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
## 环境要求
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
- DeepSeek Harness **0.2.2-rc.1**。
|
|
16
|
+
- Node.js **22 或以上**。
|
|
17
|
+
- Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
|
|
18
|
+
- DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
**本插件包含宿主兼容补丁。** 插件加载时会检查并备份原文件,自动应用五项补丁;不修改现有供应商、密钥或会话配置。版本或文件内容不受支持时会停止应用,不强制覆盖。
|
|
17
21
|
|
|
18
|
-
##
|
|
22
|
+
## 安装
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```text
|
|
23
|
-
请帮我安装 dsh-model-groups,并完成安装后的验证。
|
|
24
|
-
|
|
25
|
-
官方说明:
|
|
26
|
-
https://github.com/betsunninda9-lgtm/dsh-model-groups
|
|
27
|
-
|
|
28
|
-
请实际执行安装,不要只评价文档或给出命令建议:
|
|
29
|
-
1. 确认当前使用的 DSH 主机、profile 和界面。该插件仅支持
|
|
30
|
-
DSH 0.2.2-rc.1 的 Harness 网页界面;版本不符就停止并说明。
|
|
31
|
-
2. 阅读仓库当前 README,使用 npm 官方源下载当前版本,
|
|
32
|
-
解压到长期保留的插件目录。不要修改供应商、密钥或模型配置。
|
|
33
|
-
3. 找到正在使用的 DSH 实际安装目录,核对包名与版本。
|
|
34
|
-
不要把插件目录当成 DSH 目录,也不要猜测安装路径。
|
|
35
|
-
4. 在解压后的插件目录执行 native-patches.mjs 的 --check,
|
|
36
|
-
显式传入 --dsh-root。遇到哈希或版本不符就停止,不强制覆盖。
|
|
37
|
-
5. 用 dsh plugin --profile <当前profile> add <插件绝对路径>
|
|
38
|
-
注册插件。只有当前实例使用 web profile 时才填写 web。
|
|
39
|
-
6. 在合适的停机窗口应用兼容补丁;如果当前对话依赖这个引擎,
|
|
40
|
-
先完成可在线执行的步骤,再告诉我如何停止、应用补丁和重启。
|
|
41
|
-
不要直接中断正在进行的对话。
|
|
42
|
-
7. 重启原来使用的 DSH 服务并刷新原网页,核对模型菜单分组和
|
|
43
|
-
设置页“多密钥配置”开关。不要启动另一个服务代替原服务。
|
|
44
|
-
8. 报告安装版本、插件目录、补丁检查结果,以及是否仍需我重启。
|
|
45
|
-
不要把下载成功或插件注册成功当成整项安装已完成。
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## 手动安装
|
|
49
|
-
|
|
50
|
-
以下命令适用于 Bash 环境。先确认 DSH 版本,再执行。
|
|
51
|
-
|
|
52
|
-
### 1. 下载并解压
|
|
24
|
+
在目标设备的 DSH 环境中执行:
|
|
53
25
|
|
|
54
26
|
```bash
|
|
55
|
-
|
|
56
|
-
mkdir -p "$PLUGIN_DIR"
|
|
57
|
-
cd "$PLUGIN_DIR"
|
|
58
|
-
npm pack dsh-model-groups@0.3.3 --registry=https://registry.npmjs.org/
|
|
59
|
-
tar -xzf dsh-model-groups-0.3.3.tgz --strip-components=1
|
|
27
|
+
dsh plugin --profile web add dsh-model-groups@0.3.4 --registry=https://registry.npmjs.org/
|
|
60
28
|
```
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
### 2. 确认 DSH 目录并检查补丁
|
|
65
|
-
|
|
66
|
-
`DSH_ROOT` 是正在使用的 DSH 安装目录:该目录本身或其 `node_modules/@deepseek-ai/dsh` 下的包应为 `@deepseek-ai/dsh@0.2.2-rc.1`,且该目录下包含补丁要修改的 `node_modules/@deepseek-ai/` 文件。它不是插件目录、profile 目录或项目工作目录。
|
|
30
|
+
安装包:[npm](https://www.npmjs.com/package/dsh-model-groups) · [GitHub 发布页](https://github.com/betsunninda9-lgtm/dsh-model-groups/releases/tag/v0.3.4)。`web` 为 dsh-go 本机常用 profile,其他环境请使用实际 profile。
|
|
67
31
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
read -r -p '请输入实际 DSH 安装目录:' DSH_ROOT
|
|
72
|
-
node scripts/native-patches.mjs --dsh-root "$DSH_ROOT" --check
|
|
73
|
-
```
|
|
32
|
+
插件加载并提示“补丁已自动安装”后,保存草稿,**重启原 DSH 引擎并刷新网页**。无需先停止引擎,也不需要外部终端手工打补丁。
|
|
74
33
|
|
|
75
|
-
|
|
34
|
+
如果安装后插件尚未加载,先启动或重启引擎;若此次启动才应用补丁,再重启一次以载入新模块。补丁已在磁盘就绪时,后续加载不会重复写入。
|
|
76
35
|
|
|
77
|
-
|
|
36
|
+
当前正式版为 `0.3.4`,包含已验证的自动补丁安装与侧边面板修复。
|
|
78
37
|
|
|
79
|
-
|
|
38
|
+
### 交给 AI 安装
|
|
80
39
|
|
|
81
|
-
```
|
|
82
|
-
dsh
|
|
40
|
+
```text
|
|
41
|
+
请按 https://github.com/betsunninda9-lgtm/dsh-model-groups 的安装说明,
|
|
42
|
+
使用 npm 正式版安装到当前 DSH profile。
|
|
43
|
+
我允许插件自动校验、备份并应用宿主补丁;保留现有配置。
|
|
44
|
+
检查加载日志,成功后提醒我重启原引擎并刷新网页;遇到不兼容就停止报告。
|
|
83
45
|
```
|
|
84
46
|
|
|
85
|
-
|
|
47
|
+
## 使用
|
|
86
48
|
|
|
87
|
-
|
|
49
|
+
1. 打开 **设置 → 模型**,新增或编辑自定义模型 API。
|
|
50
|
+
2. 填写供应商公共的 Provider ID、API 地址和协议。
|
|
51
|
+
3. 开启 **多密钥配置**,为每份配置填写名称、密钥和模型列表,点击“应用”。
|
|
52
|
+
4. 在会话模型菜单中展开供应商和配置,选择需要的模型。
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
54
|
+
```text
|
|
55
|
+
示例供应商 A
|
|
56
|
+
配置 A
|
|
57
|
+
示例模型
|
|
58
|
+
配置 B
|
|
59
|
+
示例模型
|
|
60
|
+
示例供应商 B
|
|
61
|
+
示例模型
|
|
94
62
|
```
|
|
95
63
|
|
|
96
|
-
|
|
64
|
+
已有单密钥配置与旧会话选择会保留。开启多密钥后,可在模型菜单手动选择新配置;插件不会自动合并已有供应商或迁移会话选择。
|
|
97
65
|
|
|
98
|
-
|
|
66
|
+
关闭多密钥配置会保留已保存的配置与凭据,删除配置也不会同时删除凭据。有尚未保存的密钥输入时,需要先保存或清空,才能确认关闭。
|
|
99
67
|
|
|
100
|
-
|
|
68
|
+
窄屏侧边面板会根据可用空间调整各级宽度;三级并列时,较长名称可能截断。
|
|
101
69
|
|
|
102
|
-
|
|
103
|
-
- 在“设置 → 模型”编辑支持的自定义模型 API,可以看到“多密钥配置”开关。
|
|
104
|
-
- 开启并保存多份配置后,模型菜单出现对应的配置层级。
|
|
70
|
+
## 更新
|
|
105
71
|
|
|
106
|
-
|
|
72
|
+
使用对应发布版本的安装命令更新,随后按加载日志提示重启引擎并刷新网页。兼容补丁已经存在且未变化时,不会重复应用。
|
|
107
73
|
|
|
108
|
-
|
|
74
|
+
DSH 升级前请确认插件兼容范围。新版本或未知文件会被拒绝,需要等待对应适配,不能强制套用旧补丁。
|
|
109
75
|
|
|
110
|
-
|
|
76
|
+
DSHMarket 的收录需经过社区审核。普通插件的市场更新跟随 npm `latest`,不会自动跟随 GitHub 发布包或 npm `beta` 标签。
|
|
111
77
|
|
|
112
|
-
|
|
78
|
+
## 安装诊断
|
|
113
79
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
配置 B
|
|
119
|
-
示例模型
|
|
120
|
-
示例供应商 B
|
|
121
|
-
示例模型
|
|
80
|
+
没有出现“多密钥配置”开关时,先确认原引擎已在自动补丁后重启,并刷新原网页。仍未恢复,可在已安装插件目录执行:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node scripts/doctor.mjs --dsh-root /opt/dsh --json
|
|
122
84
|
```
|
|
123
85
|
|
|
124
|
-
|
|
86
|
+
`/opt/dsh` 请替换为实际 DSH 安装目录。五项文件应为 `patched`。该命令只读检查程序文件,不读取密钥;磁盘状态正常不代表运行中的模块已刷新。
|
|
125
87
|
|
|
126
|
-
|
|
88
|
+
自动安装日志位于原引擎日志中。遇到文件不受支持、目录布局不兼容或权限不足时,请提供程序包版本与文件 SHA-256,勿提供密钥。
|
|
127
89
|
|
|
128
|
-
|
|
90
|
+
原文件备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`,不会随插件包更新丢失。安装意外中断可能留下 `patch.lock`;确认没有安装进程后再处理,不要并发重试。
|
|
129
91
|
|
|
130
|
-
##
|
|
92
|
+
## 卸载与还原
|
|
131
93
|
|
|
132
|
-
|
|
94
|
+
移除插件不会自动还原宿主补丁或删除凭据。完整还原前,请先:
|
|
133
95
|
|
|
134
|
-
|
|
96
|
+
1. 将相关会话和子代理切换回普通单密钥模型。
|
|
97
|
+
2. 关闭多密钥配置,备份配置并清理原版 DSH 不支持的 `multiKey`、`keyEntries` 字段,保留仍需使用的凭据。
|
|
98
|
+
3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
|
|
135
99
|
|
|
136
|
-
|
|
137
|
-
2. 关闭各供应商的多密钥配置并应用。备份 DSH 配置后,清理原版 DSH 不支持的 `multiKey`、`keyEntries` 配置字段;保留仍需使用的单密钥配置和凭据。
|
|
138
|
-
3. 停止 DSH,在保留的插件目录中执行下面命令,再启动原服务。
|
|
100
|
+
先停用或移除插件,避免其再次加载时重新应用补丁:
|
|
139
101
|
|
|
140
102
|
```bash
|
|
141
|
-
read -r -p '请输入实际 DSH 安装目录:' DSH_ROOT
|
|
142
|
-
node scripts/native-patches.mjs --dsh-root "$DSH_ROOT" --revert
|
|
143
103
|
dsh plugin --profile web remove dsh-model-groups
|
|
144
104
|
```
|
|
145
105
|
|
|
146
|
-
|
|
106
|
+
随后安排维护窗口,停止原服务,在保留的插件目录执行:
|
|
147
107
|
|
|
148
|
-
|
|
108
|
+
```bash
|
|
109
|
+
node scripts/native-patches.mjs --dsh-root /opt/dsh --revert
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
完成后启动原服务并刷新网页。还原脚本按校验过的文件版本恢复代码,不修改会话或凭据。仍引用 `@dsh-key/` 模型的会话在还原后无法继续使用该选择。
|
|
149
113
|
|
|
150
|
-
|
|
114
|
+
## 开发
|
|
151
115
|
|
|
152
116
|
```bash
|
|
153
117
|
npm run build
|
|
@@ -155,6 +119,8 @@ npm test
|
|
|
155
119
|
npm run check:build
|
|
156
120
|
```
|
|
157
121
|
|
|
158
|
-
|
|
122
|
+
设置 `DSH_ROOT` 可启用依赖对应 DSH 程序文件的回归测试。源码包含层级、选择器交互、面板布局及补丁安装与恢复测试。
|
|
123
|
+
|
|
124
|
+
## 许可证
|
|
159
125
|
|
|
160
|
-
|
|
126
|
+
[MIT](LICENSE)。包含改编自 DeepSeek Harness 的代码,保留原版权声明。
|
package/lib/client.js
CHANGED
|
@@ -294,6 +294,29 @@ const MEASURE_STYLE = {
|
|
|
294
294
|
* store/verbs) + the standard locale seat.
|
|
295
295
|
* @returns the trigger and, while open, the two-level menu.
|
|
296
296
|
*/
|
|
297
|
+
// Lay out the entire cascade together: positioning a child alone can put it
|
|
298
|
+
// over an ancestor when neither side has room for another full-size card.
|
|
299
|
+
function layoutModelFlyouts(rect, viewportWidth, viewportHeight, count) {
|
|
300
|
+
const margin = Math.min(12, Math.max(0, viewportWidth / 8));
|
|
301
|
+
const available = Math.max(1, viewportWidth - margin * 2);
|
|
302
|
+
const preferredWidth = rect.width > 0 ? rect.width : 280;
|
|
303
|
+
const width = Math.min(preferredWidth, available / count);
|
|
304
|
+
const height = Math.min(rect.height > 0 ? rect.height : 360, Math.max(1, viewportHeight - margin * 2));
|
|
305
|
+
const leftRoom = rect.left - margin;
|
|
306
|
+
const rightRoom = viewportWidth - margin - (rect.left + preferredWidth);
|
|
307
|
+
const direction = leftRoom >= width * (count - 1) ? -1
|
|
308
|
+
: rightRoom >= width * (count - 1) ? 1 : leftRoom >= rightRoom ? -1 : 1;
|
|
309
|
+
const desiredLeft = direction < 0 ? rect.left - width * (count - 1) : rect.left;
|
|
310
|
+
const start = Math.max(margin, Math.min(desiredLeft, viewportWidth - margin - width * count));
|
|
311
|
+
const top = Math.max(margin, Math.min(rect.top, viewportHeight - margin - height));
|
|
312
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
313
|
+
left: start + width * (direction < 0 ? count - 1 - index : index),
|
|
314
|
+
top, width, height,
|
|
315
|
+
// Override the native menu's intrinsic minimum and maximum at narrow widths.
|
|
316
|
+
minWidth: 0, maxWidth: width, maxHeight: height,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
297
320
|
function ModelSelect({ locked, available, directory, load, select, t, renderSlot, providersForm }) {
|
|
298
321
|
const preferences = react.useSyncExternalStore(groupPreferences.subscribe, groupPreferences.getSnapshot);
|
|
299
322
|
const configuration = react.useSyncExternalStore(listener => providersForm?.subscribe(listener) ?? (() => {}), () => providersForm?.getSnapshot() ?? EMPTY_PROVIDERS_SNAPSHOT);
|
|
@@ -332,75 +355,18 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
332
355
|
}
|
|
333
356
|
};
|
|
334
357
|
|
|
335
|
-
// 完全读取并匹配官方原版主卡片尺寸(宽、高严格保持与主卡片完全一致)
|
|
336
358
|
const triggerFlyout = (entry, anchorEl) => {
|
|
337
359
|
clearFlyoutTimer();
|
|
338
360
|
if (activeFlyout?.key === entry.key) return;
|
|
339
361
|
setActiveSubFlyout(null);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
const margin = 12;
|
|
343
|
-
const winW = window.innerWidth;
|
|
344
|
-
const winH = window.innerHeight;
|
|
345
|
-
// 直接读取官方主卡片的实际渲染宽度与高度
|
|
346
|
-
const panelW = menuRect.width > 0 ? menuRect.width : 280;
|
|
347
|
-
const panelH = menuRect.height > 0 ? menuRect.height : 360;
|
|
348
|
-
|
|
349
|
-
// 紧靠主卡片左侧,0px 缝隙严丝合缝
|
|
350
|
-
let left = menuRect.left - panelW;
|
|
351
|
-
if (left < margin) {
|
|
352
|
-
if (winW - menuRect.right >= panelW + margin) {
|
|
353
|
-
left = menuRect.right;
|
|
354
|
-
} else {
|
|
355
|
-
left = Math.max(margin, Math.min(menuRect.left, winW - panelW - margin));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 顶部与官方主卡片绝对平齐,高度与官方主卡片 1:1 等高
|
|
360
|
-
let top = menuRect.top;
|
|
361
|
-
setActiveFlyout({
|
|
362
|
-
key: entry.key,
|
|
363
|
-
entry,
|
|
364
|
-
pos: {
|
|
365
|
-
left: Math.round(left),
|
|
366
|
-
top: Math.round(top),
|
|
367
|
-
width: Math.round(panelW),
|
|
368
|
-
height: Math.round(panelH)
|
|
369
|
-
}
|
|
370
|
-
});
|
|
362
|
+
const rect = activeFlyout?.anchorRect ?? (menuRef.current ?? anchorEl).getBoundingClientRect();
|
|
363
|
+
setActiveFlyout({ key: entry.key, entry, anchorRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height } });
|
|
371
364
|
};
|
|
372
365
|
|
|
373
|
-
const triggerSubFlyout = (childGroup
|
|
366
|
+
const triggerSubFlyout = (childGroup) => {
|
|
374
367
|
clearFlyoutTimer();
|
|
375
368
|
if (activeSubFlyout?.key === childGroup.key) return;
|
|
376
|
-
|
|
377
|
-
const flyoutRect = flyoutEl ? flyoutEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
|
|
378
|
-
const margin = 12;
|
|
379
|
-
const winW = window.innerWidth;
|
|
380
|
-
const winH = window.innerHeight;
|
|
381
|
-
const panelW = flyoutRect.width > 0 ? flyoutRect.width : 280;
|
|
382
|
-
const panelH = flyoutRect.height > 0 ? flyoutRect.height : 360;
|
|
383
|
-
|
|
384
|
-
let left = flyoutRect.left - panelW;
|
|
385
|
-
if (left < margin) {
|
|
386
|
-
if (winW - flyoutRect.right >= panelW + margin) {
|
|
387
|
-
left = flyoutRect.right;
|
|
388
|
-
} else {
|
|
389
|
-
left = Math.max(margin, Math.min(flyoutRect.left, winW - panelW - margin));
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
let top = flyoutRect.top;
|
|
394
|
-
setActiveSubFlyout({
|
|
395
|
-
key: childGroup.key,
|
|
396
|
-
childGroup,
|
|
397
|
-
pos: {
|
|
398
|
-
left: Math.round(left),
|
|
399
|
-
top: Math.round(top),
|
|
400
|
-
width: Math.round(panelW),
|
|
401
|
-
height: Math.round(panelH)
|
|
402
|
-
}
|
|
403
|
-
});
|
|
369
|
+
setActiveSubFlyout({ key: childGroup.key, childGroup });
|
|
404
370
|
};
|
|
405
371
|
|
|
406
372
|
const scheduleCloseFlyout = (delay = 240) => {
|
|
@@ -412,11 +378,14 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
412
378
|
};
|
|
413
379
|
|
|
414
380
|
const scheduleCloseSubFlyout = (delay = 240) => {
|
|
415
|
-
|
|
381
|
+
// Leaving the key panel must not let an older parent-leave timer
|
|
382
|
+
// destroy both the provider and key panels.
|
|
383
|
+
clearFlyoutTimer();
|
|
416
384
|
subFlyoutTimerRef.current = setTimeout(() => {
|
|
417
385
|
setActiveSubFlyout(null);
|
|
418
386
|
}, delay);
|
|
419
387
|
};
|
|
388
|
+
(0, react.useEffect)(() => () => clearFlyoutTimer(), []);
|
|
420
389
|
const [toast, setToast] = (0, react.useState)(null);
|
|
421
390
|
const toastSeq = (0, react.useRef)(0);
|
|
422
391
|
const rootRef = (0, react.useRef)(null);
|
|
@@ -590,6 +559,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
590
559
|
state,
|
|
591
560
|
query
|
|
592
561
|
]);
|
|
562
|
+
const cascade = open && pane === "model" && menuMode === "panel" && !searching && activeFlyout
|
|
563
|
+
? layoutModelFlyouts({ ...activeFlyout.anchorRect, ...(menuPos ?? {}) }, window.innerWidth, window.innerHeight, activeSubFlyout ? 3 : 2)
|
|
564
|
+
: null;
|
|
593
565
|
if (!available) return null;
|
|
594
566
|
const show = () => {
|
|
595
567
|
setSelectionFocus(false);
|
|
@@ -853,7 +825,7 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
853
825
|
title: searching ? text('搜索时自动展开,清空搜索后恢复', 'Expanded during search; clearing restores your groups') : entry.id || entry.name,
|
|
854
826
|
onClick: (e) => {
|
|
855
827
|
if (isFlyoutMode) {
|
|
856
|
-
if (isFlyoutActive) setActiveFlyout(null);
|
|
828
|
+
if (isFlyoutActive) { clearFlyoutTimer(); setActiveFlyout(null); setActiveSubFlyout(null); }
|
|
857
829
|
else triggerFlyout(entry, e.currentTarget);
|
|
858
830
|
} else {
|
|
859
831
|
setHighlightedIndex(null);
|
|
@@ -936,8 +908,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
936
908
|
open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
937
909
|
ref: menuRef,
|
|
938
910
|
id: `${id}-menu`,
|
|
939
|
-
className: ModelSelect_module_css_default.menu,
|
|
940
|
-
|
|
911
|
+
className: clsx(ModelSelect_module_css_default.menu, cascade && "dsh-mg-cascade-panel"),
|
|
912
|
+
"data-flyout-level": cascade ? "0" : undefined,
|
|
913
|
+
style: cascade?.[0] ?? menuPos ?? MEASURE_STYLE,
|
|
941
914
|
role: pane === "model" ? "group" : "menu",
|
|
942
915
|
"aria-label": t("menu.aria"),
|
|
943
916
|
"aria-busy": state.status === "loading" || busy,
|
|
@@ -1126,18 +1099,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
1126
1099
|
}, level.key))] })
|
|
1127
1100
|
]
|
|
1128
1101
|
}), document.body),
|
|
1129
|
-
|
|
1102
|
+
cascade !== null && activeFlyout !== null && (0, react_dom.createPortal)(
|
|
1130
1103
|
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
1131
1104
|
ref: flyoutRef,
|
|
1132
|
-
className: ModelSelect_module_css_default.menu,
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
top: activeFlyout.pos.top,
|
|
1136
|
-
width: activeFlyout.pos.width,
|
|
1137
|
-
height: activeFlyout.pos.height,
|
|
1138
|
-
position: "fixed",
|
|
1139
|
-
margin: 0
|
|
1140
|
-
},
|
|
1105
|
+
className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
|
|
1106
|
+
"data-flyout-level": "1",
|
|
1107
|
+
style: { ...cascade[1], position: "fixed", margin: 0 },
|
|
1141
1108
|
onMouseEnter: clearFlyoutTimer,
|
|
1142
1109
|
onMouseLeave: () => scheduleCloseFlyout(200),
|
|
1143
1110
|
onMouseDown: (e) => {
|
|
@@ -1176,18 +1143,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
1176
1143
|
}),
|
|
1177
1144
|
document.body
|
|
1178
1145
|
),
|
|
1179
|
-
|
|
1146
|
+
cascade !== null && activeSubFlyout !== null && (0, react_dom.createPortal)(
|
|
1180
1147
|
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
1181
1148
|
ref: subFlyoutRef,
|
|
1182
|
-
className: ModelSelect_module_css_default.menu,
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
top: activeSubFlyout.pos.top,
|
|
1186
|
-
width: activeSubFlyout.pos.width,
|
|
1187
|
-
height: activeSubFlyout.pos.height,
|
|
1188
|
-
position: "fixed",
|
|
1189
|
-
margin: 0
|
|
1190
|
-
},
|
|
1149
|
+
className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
|
|
1150
|
+
"data-flyout-level": "2",
|
|
1151
|
+
style: { ...cascade[2], position: "fixed", margin: 0 },
|
|
1191
1152
|
onMouseEnter: clearFlyoutTimer,
|
|
1192
1153
|
onMouseLeave: () => scheduleCloseSubFlyout(200),
|
|
1193
1154
|
onMouseDown: (e) => {
|
|
@@ -1286,7 +1247,7 @@ class ModelSeatBoundary extends react.Component {
|
|
|
1286
1247
|
}
|
|
1287
1248
|
|
|
1288
1249
|
|
|
1289
|
-
const MODEL_GROUPS_CSS = "/* DSH Model Groups: 100% native token-compatible styling */\n\n.dsh-mg-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 4px;\n flex-shrink: 0;\n margin: 2px 0 4px;\n padding: 0 4px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-summary {\n flex: 1;\n min-width: 0;\n padding: 0 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dsh-mg-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n}\n\n.dsh-mg-toolbar button {\n flex: none;\n height: 22px;\n padding: 0 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n line-height: 22px;\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-toolbar button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-toolbar button:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.dsh-mg-groupStart {\n display: block;\n height: 0;\n pointer-events: none;\n}\n\n.dsh-mg-groupHeading {\n border-radius: var(--dsw-radius-md);\n margin: 1px 0;\n}\n\n.dsh-mg-majorHeading {\n position: static;\n}\n\n.dsh-mg-minorHeading {\n position: relative;\n}\n\n.dsh-mg-majorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-minorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-source {\n display: block;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n.dsh-mg-triggerSource {\n color: var(--dsw-alias-label-caption);\n font-size: 13px;\n}\n\n.dsh-mg-groupHeading button {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 32px;\n padding: 4px 7px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n transition: background .12s;\n}\n\n.dsh-mg-groupHeading button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-groupHeading button:disabled {\n cursor: default;\n}\n\n.dsh-mg-groupHeading button:focus-visible,\n.dsh-mg-toolbar button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n}\n\n.dsh-mg-groupArrow {\n flex: none;\n width: 14px;\n height: 14px;\n color: var(--dsw-alias-label-caption);\n transition: transform .12s;\n}\n\n.dsh-mg-groupArrowOpen {\n transform: rotate(90deg);\n}\n\n.dsh-mg-groupName {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-groupCount {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-weight: 400;\n line-height: 16px;\n}\n\n.dsh-mg-currentBadge {\n flex: none;\n color: var(--dsw-alias-state-business-primary);\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n}\n\n.dsh-mg-groups {\n min-height: 0;\n}\n\n.dsh-mg-groups [hidden] {\n display: none !important;\n}\n\n/* ── 飞出卡片:与官方原版菜单 100% 结构一致,剔除 header 伪分层与白色叠加 ── */\n.dsh-mg-flyout-subgroup-list {\n display: flex;\n flex-direction: column;\n}\n\n.dsh-mg-subgroup-item {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-md);\n width: auto;\n min-width: 100%;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n background: 0 0;\n border: none;\n outline: none;\n align-items: center;\n gap: 6px;\n padding: 5px 7px;\n display: flex;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n transition: background .12s;\n}\n\n.dsh-mg-subgroup-item:hover,\n.dsh-mg-subgroup-item[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-subgroup-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-subgroup-count {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-subgroup-arrow {\n flex: none;\n color: var(--dsw-alias-menu-icon, var(--dsw-alias-label-caption));\n}\n\n.dsh-mg-active-heading {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ── 底部模式切换:原生轻量化,无突兀高饱和蓝块 ── */\n.dsh-mg-modebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 6px 2px;\n margin-top: 3px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-mode-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-mode-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-mode-btn[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-active, rgba(255, 255, 255, 0.1));\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n@media (pointer: coarse) {\n .dsh-mg-groupHeading button { min-height: 36px; }\n .dsh-mg-toolbar button { height: 26px; }\n .dsh-mg-mode-btn { min-height: 24px; padding: 2px 8px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dsh-mg-groupArrow { transition: none; }\n}\n\n";
|
|
1250
|
+
const MODEL_GROUPS_CSS = "/* DSH Model Groups: 100% native token-compatible styling */\n\n.dsh-mg-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 4px;\n flex-shrink: 0;\n margin: 2px 0 4px;\n padding: 0 4px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-summary {\n flex: 1;\n min-width: 0;\n padding: 0 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dsh-mg-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n}\n\n.dsh-mg-toolbar button {\n flex: none;\n height: 22px;\n padding: 0 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n line-height: 22px;\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-toolbar button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-toolbar button:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.dsh-mg-groupStart {\n display: block;\n height: 0;\n pointer-events: none;\n}\n\n.dsh-mg-groupHeading {\n border-radius: var(--dsw-radius-md);\n margin: 1px 0;\n}\n\n.dsh-mg-majorHeading {\n position: static;\n}\n\n.dsh-mg-minorHeading {\n position: relative;\n}\n\n.dsh-mg-majorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-minorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-source {\n display: block;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n.dsh-mg-triggerSource {\n color: var(--dsw-alias-label-caption);\n font-size: 13px;\n}\n\n.dsh-mg-groupHeading button {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 32px;\n padding: 4px 7px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n transition: background .12s;\n}\n\n.dsh-mg-groupHeading button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-groupHeading button:disabled {\n cursor: default;\n}\n\n.dsh-mg-groupHeading button:focus-visible,\n.dsh-mg-toolbar button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n}\n\n.dsh-mg-groupArrow {\n flex: none;\n width: 14px;\n height: 14px;\n color: var(--dsw-alias-label-caption);\n transition: transform .12s;\n}\n\n.dsh-mg-groupArrowOpen {\n transform: rotate(90deg);\n}\n\n.dsh-mg-groupName {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-groupCount {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-weight: 400;\n line-height: 16px;\n}\n\n.dsh-mg-currentBadge {\n flex: none;\n color: var(--dsw-alias-state-business-primary);\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n}\n\n.dsh-mg-groups {\n min-height: 0;\n}\n\n.dsh-mg-groups [hidden] {\n display: none !important;\n}\n\n/* ── 飞出卡片:与官方原版菜单 100% 结构一致,剔除 header 伪分层与白色叠加 ── */\n.dsh-mg-flyout-subgroup-list {\n display: flex;\n flex-direction: column;\n}\n\n.dsh-mg-subgroup-item {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-md);\n width: auto;\n min-width: 100%;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n background: 0 0;\n border: none;\n outline: none;\n align-items: center;\n gap: 6px;\n padding: 5px 7px;\n display: flex;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n transition: background .12s;\n}\n\n.dsh-mg-subgroup-item:hover,\n.dsh-mg-subgroup-item[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-subgroup-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-subgroup-count {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-subgroup-arrow {\n flex: none;\n color: var(--dsw-alias-menu-icon, var(--dsw-alias-label-caption));\n}\n\n.dsh-mg-active-heading {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ── 底部模式切换:原生轻量化,无突兀高饱和蓝块 ── */\n.dsh-mg-modebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 6px 2px;\n margin-top: 3px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-mode-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-mode-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-mode-btn[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-active, rgba(255, 255, 255, 0.1));\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n@media (pointer: coarse) {\n .dsh-mg-groupHeading button { min-height: 36px; }\n .dsh-mg-toolbar button { height: 26px; }\n .dsh-mg-mode-btn { min-height: 24px; padding: 2px 8px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dsh-mg-groupArrow { transition: none; }\n}\n\n\n/* Cascade columns share the viewport rather than covering their ancestors. */\n.dsh-mg-cascade-panel {\n box-sizing: border-box;\n min-width: 0;\n}\n.dsh-mg-cascade-panel button,\n.dsh-mg-cascade-panel [role=\"menu\"],\n.dsh-mg-cascade-panel .dsh-mg-modebar,\n.dsh-mg-cascade-panel .dsh-mg-toolbar {\n min-width: 0;\n}\n.dsh-mg-cascade-panel .dsh-mg-toolbar,\n.dsh-mg-cascade-panel .dsh-mg-modebar,\n.dsh-mg-cascade-panel .dsh-mg-toolbar-actions {\n flex-wrap: wrap;\n}\n.dsh-mg-cascade-panel .dsh-mg-summary {\n white-space: normal;\n}\n";
|
|
1290
1251
|
function installModelGroupsStyles() {
|
|
1291
1252
|
const tagId = 'dsh-model-groups/groups.css';
|
|
1292
1253
|
if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;
|
|
@@ -1305,6 +1266,22 @@ function apply(ctx) {
|
|
|
1305
1266
|
installModelSelectStyles();
|
|
1306
1267
|
installModelGroupsStyles();
|
|
1307
1268
|
const providersForm = ctx.configForms?.get('llm-pi-ai');
|
|
1269
|
+
let statusDisposed = false;
|
|
1270
|
+
let statusNotice;
|
|
1271
|
+
if (typeof fetch === 'function') Promise.resolve().then(() => fetch('/dsh-model-groups/status', { credentials: 'same-origin', cache: 'no-store' }))
|
|
1272
|
+
.then(response => { if (!response.ok) throw new Error('status unavailable'); return response.json(); })
|
|
1273
|
+
.then(status => {
|
|
1274
|
+
if (statusDisposed || (!status.restartRequired && !status.failed)) return;
|
|
1275
|
+
statusNotice = document.createElement('div');
|
|
1276
|
+
statusNotice.dataset.plugin = 'dsh-model-groups';
|
|
1277
|
+
statusNotice.setAttribute('role', 'alertdialog');
|
|
1278
|
+
statusNotice.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#222);display:grid;place-items:center;padding:24px;box-sizing:border-box;';
|
|
1279
|
+
statusNotice.textContent = status.failed
|
|
1280
|
+
? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。当前网页暂不可用于编辑模型配置。'
|
|
1281
|
+
: '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。当前网页暂停操作,避免新旧模块混用。';
|
|
1282
|
+
document.body.appendChild(statusNotice);
|
|
1283
|
+
}).catch(() => {});
|
|
1284
|
+
ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });
|
|
1308
1285
|
const onStorage = event => {
|
|
1309
1286
|
if (event.key === STORAGE_KEY || event.key === null) groupPreferences.sync(event.key === null ? null : event.newValue);
|
|
1310
1287
|
};
|
package/native/pi-ai.patch.json
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
"target": "node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js",
|
|
5
5
|
"baseSha256": "070c839e2c00e2d5f7c5d82307ca3d84dbb2323e46979daf265908751bbe8ae8",
|
|
6
6
|
"resultSha256": "a2b3bcda38015026ed58074cef0d508bbd2d7cb1423c00331651fcbd1a9f6b55",
|
|
7
|
+
"variants": [
|
|
8
|
+
{
|
|
9
|
+
"source": "0.2.2-rc.1 with tool lookup optimization and credential cancellation",
|
|
10
|
+
"baseSha256": "0a99a2fc903a4285044b09114b3bd20ee1e612f39aa381e4a71b10d48ee9e9f7",
|
|
11
|
+
"resultSha256": "c87f965d9fcc5bbcd386e98ea3b292b05ec7a55d80df1266e18234031b80990e"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"source": "npm official 0.2.2-rc.1",
|
|
15
|
+
"baseSha256": "1a3ae58f9b4493e15459cc5000ec80f21b5d3f4a9dea6ffb6bc28e5b8817ae8a",
|
|
16
|
+
"resultSha256": "b9416207cc7c27aff40cb2b877c9176e53809eb2f6dc741828c2ed6f6bdf541b"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
7
19
|
"replacements": [
|
|
8
20
|
{
|
|
9
21
|
"before": "\t...modelFields\n});\n/** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */\nconst modelOverride = z.object(modelFields);\nconst profile = z.object({\n\tapiKeyEnv: z.string().role(\"credential-ref\"),\n\tdisplayName: z.string(),\n\tapi: z.union(supportedProtocols()),\n\tbaseURL: z.string(),\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-groups",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "DSH 网页模型选择器:供应商与密钥配置分层折叠、多密钥手动选择。支持 DSH 0.2.2-rc.1。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"test": "node --test test/*.test.mjs",
|
|
62
62
|
"test:ui": "node --test test/picker-ui.test.mjs",
|
|
63
63
|
"native:check": "node scripts/native-patches.mjs --check",
|
|
64
|
+
"doctor": "node scripts/doctor.mjs",
|
|
64
65
|
"prepack": "npm run build && npm run check:build"
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { planPatches } from './native-patches.mjs';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
if (args.includes('--help')) {
|
|
7
|
+
console.log('Usage: node scripts/doctor.mjs --dsh-root <DSH installation directory> [--json]\nRead-only installation check. Does not read credentials or change DSH.');
|
|
8
|
+
} else {
|
|
9
|
+
try {
|
|
10
|
+
const i = args.indexOf('--dsh-root');
|
|
11
|
+
if (i < 0 || !args[i + 1] || args.some((arg, index) => index !== i + 1 && !['--dsh-root', '--json'].includes(arg))) {
|
|
12
|
+
throw new Error('请用 --dsh-root 指定正在使用的 DSH 安装目录。');
|
|
13
|
+
}
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
|
|
15
|
+
const plans = planPatches(args[i + 1], 'check');
|
|
16
|
+
const missing = plans.filter(plan => plan.state !== 'patched');
|
|
17
|
+
const report = {
|
|
18
|
+
plugin: pkg.name,
|
|
19
|
+
version: pkg.version,
|
|
20
|
+
supportedDsh: pkg.engines.dsh,
|
|
21
|
+
diskPatchesReady: missing.length === 0,
|
|
22
|
+
runtimeVerified: false,
|
|
23
|
+
files: plans.map(plan => ({ target: plan.artifact.target, state: plan.state })),
|
|
24
|
+
next: missing.length > 0
|
|
25
|
+
? '多密钥安装尚未完成:需要按 README 应用尚未安装的兼容补丁,再重启原 DSH 服务并刷新网页。'
|
|
26
|
+
: '磁盘补丁齐全。请确认原 DSH 服务已在应用补丁后重启,并刷新网页;本检查不能确认运行中的模块或浏览器缓存。',
|
|
27
|
+
};
|
|
28
|
+
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
29
|
+
else {
|
|
30
|
+
console.log(`${report.plugin} ${report.version} · DSH ${report.supportedDsh}`);
|
|
31
|
+
for (const item of report.files) console.log(`${item.state}: ${item.target}`);
|
|
32
|
+
console.log(report.next);
|
|
33
|
+
}
|
|
34
|
+
if (missing.length) process.exitCode = 2;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`无法确认多密钥安装状态:${error.message}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -32,7 +32,7 @@ export function planPatches(dshRoot, mode = 'check', artifactDirectory = path.jo
|
|
|
32
32
|
if (dsh.name !== '@deepseek-ai/dsh' || dsh.version !== '0.2.2-rc.1') throw new Error('This patch supports only @deepseek-ai/dsh 0.2.2-rc.1');
|
|
33
33
|
const seen = new Set();
|
|
34
34
|
return ARTIFACTS.map(name => {
|
|
35
|
-
|
|
35
|
+
let artifact = JSON.parse(fs.readFileSync(path.join(artifactDirectory, name), 'utf8'));
|
|
36
36
|
if (artifact.version !== '0.2.2-rc.1' || !/^@deepseek-ai\/dsh-[a-z-]+$/.test(artifact.packageName)) throw new Error('Unsupported patch package/version');
|
|
37
37
|
if (!Array.isArray(artifact.replacements) || artifact.replacements.length === 0 || !/^[0-9a-f]{64}$/.test(artifact.baseSha256) || !/^[0-9a-f]{64}$/.test(artifact.resultSha256)) throw new Error('Invalid patch manifest');
|
|
38
38
|
const prefix = `node_modules/${artifact.packageName}/`;
|
|
@@ -44,6 +44,13 @@ export function planPatches(dshRoot, mode = 'check', artifactDirectory = path.jo
|
|
|
44
44
|
if (pkg.name !== artifact.packageName || pkg.version !== artifact.version) throw new Error('Installed native package version mismatch');
|
|
45
45
|
const source = fs.readFileSync(file, 'utf8');
|
|
46
46
|
const hash = sha256(source);
|
|
47
|
+
if (artifact.variants !== undefined) {
|
|
48
|
+
if (!Array.isArray(artifact.variants) || artifact.variants.some(variant => !/^[0-9a-f]{64}$/.test(variant.baseSha256) || !/^[0-9a-f]{64}$/.test(variant.resultSha256))) throw new Error('Invalid patch variants');
|
|
49
|
+
const candidates = [artifact, ...artifact.variants];
|
|
50
|
+
const matched = candidates.filter(variant => hash === variant.baseSha256 || hash === variant.resultSha256);
|
|
51
|
+
if (matched.length > 1) throw new Error('Ambiguous patch variant');
|
|
52
|
+
if (matched.length === 1) artifact = { ...artifact, baseSha256: matched[0].baseSha256, resultSha256: matched[0].resultSha256 };
|
|
53
|
+
}
|
|
47
54
|
const state = hash === artifact.baseSha256 ? 'pristine' : hash === artifact.resultSha256 ? 'patched' : 'modified';
|
|
48
55
|
if (state === 'modified') throw new Error(`Refusing unrecognized content: ${artifact.target}`);
|
|
49
56
|
// A no-op still verifies both directions to detect a broken manifest.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { planPatches, applyPlan } from './native-patches.mjs';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
try {
|
|
8
|
+
if (args.includes('--help')) {
|
|
9
|
+
console.log('Usage: node scripts/setup.mjs --dsh-root <directory> --profile <profile> [--apply --service-stopped]\nDefault: read-only compatibility check. Apply registers the plugin and patches DSH. Stop the original DSH service first; restart it afterwards.');
|
|
10
|
+
} else {
|
|
11
|
+
const values = {};
|
|
12
|
+
let apply = false, stopped = false;
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (arg === '--apply') apply = true;
|
|
16
|
+
else if (arg === '--service-stopped') stopped = true;
|
|
17
|
+
else if (['--dsh-root', '--profile'].includes(arg) && args[i + 1] && !args[i + 1].startsWith('--')) values[arg] = args[++i];
|
|
18
|
+
else throw new Error(`未知或不完整的参数:${arg}`);
|
|
19
|
+
}
|
|
20
|
+
const root = values['--dsh-root'];
|
|
21
|
+
const profile = values['--profile'];
|
|
22
|
+
if (!root || !profile || !/^[A-Za-z0-9_-]+$/.test(profile)) throw new Error('请提供 --dsh-root 和有效的 --profile。');
|
|
23
|
+
if (apply && !stopped) throw new Error('应用前请停止原 DSH 服务,再提供 --apply --service-stopped。该参数由你确认停机,不会自动检测或停止服务。');
|
|
24
|
+
const plans = planPatches(root, 'check');
|
|
25
|
+
for (const plan of plans) console.log(`${plan.state}: ${plan.artifact.target}`);
|
|
26
|
+
if (!apply) {
|
|
27
|
+
console.log('兼容检查通过,尚未注册插件或应用补丁。停止原服务后,用相同命令加 --apply --service-stopped 完成安装。');
|
|
28
|
+
} else {
|
|
29
|
+
const pluginRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
30
|
+
const registered = spawnSync('dsh', ['plugin', '--profile', profile, 'add', path.resolve(pluginRoot)], { stdio: 'inherit' });
|
|
31
|
+
if (registered.error || registered.status !== 0) throw new Error('插件注册失败,未应用补丁。请检查上方安装输出。');
|
|
32
|
+
const count = applyPlan(planPatches(root, 'apply'));
|
|
33
|
+
if (!planPatches(root, 'check').every(plan => plan.state === 'patched')) throw new Error('补丁复查未通过。插件可能已注册,请检查磁盘状态后再启动服务。');
|
|
34
|
+
console.log(`插件已注册,${count} 个宿主文件已更新,五项磁盘补丁全部就绪。请启动原 DSH 服务并刷新原网页;当前尚未验证运行中的功能。`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`安装停止:${error.message}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { planPatches, applyPlan, sha256 } from '../scripts/native-patches.mjs';
|
|
7
|
+
|
|
8
|
+
export function findDshRoot(entry = process.argv[1]) {
|
|
9
|
+
if (!entry) throw new Error('无法确定 DSH 进程入口。');
|
|
10
|
+
const require = createRequire(fs.realpathSync(entry));
|
|
11
|
+
const pkg = require.resolve('@deepseek-ai/dsh-llm-pi-ai/package.json');
|
|
12
|
+
const resolved = fs.realpathSync(pkg);
|
|
13
|
+
let dir = path.dirname(resolved);
|
|
14
|
+
while (true) {
|
|
15
|
+
const candidate = path.join(dir, 'node_modules/@deepseek-ai/dsh-llm-pi-ai/package.json');
|
|
16
|
+
if (fs.existsSync(candidate) && fs.realpathSync(candidate) === resolved) {
|
|
17
|
+
// Full patch planning validates the DSH package/version and every target.
|
|
18
|
+
planPatches(dir, 'check');
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
const parent = path.dirname(dir);
|
|
22
|
+
if (parent === dir) break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
throw new Error('未找到与当前 DSH 进程一致且可安全修改的安装目录。');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function autoPatch(root, home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')) {
|
|
29
|
+
const plans = planPatches(root, 'apply');
|
|
30
|
+
const pending = plans.some(plan => plan.source !== plan.result);
|
|
31
|
+
if (!pending) return { state: 'disk-ready', changed: 0, runtimeVerified: false };
|
|
32
|
+
const storage = path.join(home, 'dsh-model-groups', sha256(fs.realpathSync(root)).slice(0, 16));
|
|
33
|
+
fs.mkdirSync(storage, { recursive: true, mode: 0o700 });
|
|
34
|
+
const lock = path.join(storage, 'patch.lock');
|
|
35
|
+
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
36
|
+
catch { throw new Error('补丁安装已被锁定;可能有另一进程正在安装。确认没有安装任务后再处理锁目录,勿并发重试。'); }
|
|
37
|
+
try {
|
|
38
|
+
const changed = applyPlan(planPatches(root, 'apply'), path.join(storage, 'backups'));
|
|
39
|
+
if (!planPatches(root, 'check').every(plan => plan.state === 'patched')) throw new Error('自动补丁复查失败。');
|
|
40
|
+
return { state: changed ? 'restart-required' : 'disk-ready', changed, runtimeVerified: false };
|
|
41
|
+
} finally {
|
|
42
|
+
if (path.dirname(lock) !== storage || fs.realpathSync(lock) !== lock || !fs.lstatSync(lock).isDirectory()) throw new Error('补丁锁路径异常。');
|
|
43
|
+
fs.rmdirSync(lock);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/client.js
CHANGED
|
@@ -6,6 +6,22 @@ export function apply(ctx) {
|
|
|
6
6
|
installModelSelectStyles();
|
|
7
7
|
installModelGroupsStyles();
|
|
8
8
|
const providersForm = ctx.configForms?.get('llm-pi-ai');
|
|
9
|
+
let statusDisposed = false;
|
|
10
|
+
let statusNotice;
|
|
11
|
+
if (typeof fetch === 'function') Promise.resolve().then(() => fetch('/dsh-model-groups/status', { credentials: 'same-origin', cache: 'no-store' }))
|
|
12
|
+
.then(response => { if (!response.ok) throw new Error('status unavailable'); return response.json(); })
|
|
13
|
+
.then(status => {
|
|
14
|
+
if (statusDisposed || (!status.restartRequired && !status.failed)) return;
|
|
15
|
+
statusNotice = document.createElement('div');
|
|
16
|
+
statusNotice.dataset.plugin = 'dsh-model-groups';
|
|
17
|
+
statusNotice.setAttribute('role', 'alertdialog');
|
|
18
|
+
statusNotice.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#222);display:grid;place-items:center;padding:24px;box-sizing:border-box;';
|
|
19
|
+
statusNotice.textContent = status.failed
|
|
20
|
+
? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。当前网页暂不可用于编辑模型配置。'
|
|
21
|
+
: '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。当前网页暂停操作,避免新旧模块混用。';
|
|
22
|
+
document.body.appendChild(statusNotice);
|
|
23
|
+
}).catch(() => {});
|
|
24
|
+
ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });
|
|
9
25
|
const onStorage = event => {
|
|
10
26
|
if (event.key === STORAGE_KEY || event.key === null) groupPreferences.sync(event.key === null ? null : event.newValue);
|
|
11
27
|
};
|
package/src/groups.css
CHANGED
|
@@ -278,3 +278,23 @@
|
|
|
278
278
|
.dsh-mg-groupArrow { transition: none; }
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
|
|
282
|
+
/* Cascade columns share the viewport rather than covering their ancestors. */
|
|
283
|
+
.dsh-mg-cascade-panel {
|
|
284
|
+
box-sizing: border-box;
|
|
285
|
+
min-width: 0;
|
|
286
|
+
}
|
|
287
|
+
.dsh-mg-cascade-panel button,
|
|
288
|
+
.dsh-mg-cascade-panel [role="menu"],
|
|
289
|
+
.dsh-mg-cascade-panel .dsh-mg-modebar,
|
|
290
|
+
.dsh-mg-cascade-panel .dsh-mg-toolbar {
|
|
291
|
+
min-width: 0;
|
|
292
|
+
}
|
|
293
|
+
.dsh-mg-cascade-panel .dsh-mg-toolbar,
|
|
294
|
+
.dsh-mg-cascade-panel .dsh-mg-modebar,
|
|
295
|
+
.dsh-mg-cascade-panel .dsh-mg-toolbar-actions {
|
|
296
|
+
flex-wrap: wrap;
|
|
297
|
+
}
|
|
298
|
+
.dsh-mg-cascade-panel .dsh-mg-summary {
|
|
299
|
+
white-space: normal;
|
|
300
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
import { findDshRoot, autoPatch } from './auto-patch.js';
|
|
2
|
+
|
|
3
|
+
// Explicit patch distribution: installation/loading validates and updates the
|
|
4
|
+
// known DSH disk files. It never forces module reload, restart or config changes.
|
|
5
|
+
export function apply(ctx) {
|
|
6
|
+
const key = Symbol.for('dsh-model-groups.patch-state');
|
|
7
|
+
const state = globalThis[key] ??= { restartRequired: false, error: null };
|
|
8
|
+
ctx.inject(['webserver'], child => {
|
|
9
|
+
child.effect(() => child.webserver.register({
|
|
10
|
+
kind: 'exact', path: '/dsh-model-groups/status',
|
|
11
|
+
handler(req, res) {
|
|
12
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
13
|
+
res.end(JSON.stringify({ restartRequired: state.restartRequired, failed: state.error !== null }));
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const result = autoPatch(findDshRoot());
|
|
19
|
+
if (result.state === 'restart-required') {
|
|
20
|
+
state.restartRequired = true;
|
|
21
|
+
ctx.logger.warn(`多密钥兼容补丁已自动安装(${result.changed} 项)。请保存草稿,在 App 重启原引擎并刷新原网页;当前进程尚未验证生效。`);
|
|
22
|
+
} else {
|
|
23
|
+
ctx.logger.info('多密钥磁盘补丁已就绪。若本次进程尚未在打补丁后重启,请重启原引擎并刷新网页。');
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
state.error = error.message;
|
|
27
|
+
ctx.logger.error(`多密钥自动安装未完成:${error.message}。未强制覆盖未知文件,请运行安装诊断;不要把模型分组出现视为多密钥已就绪。`);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/model-select.js
CHANGED
|
@@ -111,6 +111,29 @@ const MEASURE_STYLE = {
|
|
|
111
111
|
* store/verbs) + the standard locale seat.
|
|
112
112
|
* @returns the trigger and, while open, the two-level menu.
|
|
113
113
|
*/
|
|
114
|
+
// Lay out the entire cascade together: positioning a child alone can put it
|
|
115
|
+
// over an ancestor when neither side has room for another full-size card.
|
|
116
|
+
function layoutModelFlyouts(rect, viewportWidth, viewportHeight, count) {
|
|
117
|
+
const margin = Math.min(12, Math.max(0, viewportWidth / 8));
|
|
118
|
+
const available = Math.max(1, viewportWidth - margin * 2);
|
|
119
|
+
const preferredWidth = rect.width > 0 ? rect.width : 280;
|
|
120
|
+
const width = Math.min(preferredWidth, available / count);
|
|
121
|
+
const height = Math.min(rect.height > 0 ? rect.height : 360, Math.max(1, viewportHeight - margin * 2));
|
|
122
|
+
const leftRoom = rect.left - margin;
|
|
123
|
+
const rightRoom = viewportWidth - margin - (rect.left + preferredWidth);
|
|
124
|
+
const direction = leftRoom >= width * (count - 1) ? -1
|
|
125
|
+
: rightRoom >= width * (count - 1) ? 1 : leftRoom >= rightRoom ? -1 : 1;
|
|
126
|
+
const desiredLeft = direction < 0 ? rect.left - width * (count - 1) : rect.left;
|
|
127
|
+
const start = Math.max(margin, Math.min(desiredLeft, viewportWidth - margin - width * count));
|
|
128
|
+
const top = Math.max(margin, Math.min(rect.top, viewportHeight - margin - height));
|
|
129
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
130
|
+
left: start + width * (direction < 0 ? count - 1 - index : index),
|
|
131
|
+
top, width, height,
|
|
132
|
+
// Override the native menu's intrinsic minimum and maximum at narrow widths.
|
|
133
|
+
minWidth: 0, maxWidth: width, maxHeight: height,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
function ModelSelect({ locked, available, directory, load, select, t, renderSlot, providersForm }) {
|
|
115
138
|
const preferences = react.useSyncExternalStore(groupPreferences.subscribe, groupPreferences.getSnapshot);
|
|
116
139
|
const configuration = react.useSyncExternalStore(listener => providersForm?.subscribe(listener) ?? (() => {}), () => providersForm?.getSnapshot() ?? EMPTY_PROVIDERS_SNAPSHOT);
|
|
@@ -149,75 +172,18 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
149
172
|
}
|
|
150
173
|
};
|
|
151
174
|
|
|
152
|
-
// 完全读取并匹配官方原版主卡片尺寸(宽、高严格保持与主卡片完全一致)
|
|
153
175
|
const triggerFlyout = (entry, anchorEl) => {
|
|
154
176
|
clearFlyoutTimer();
|
|
155
177
|
if (activeFlyout?.key === entry.key) return;
|
|
156
178
|
setActiveSubFlyout(null);
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const margin = 12;
|
|
160
|
-
const winW = window.innerWidth;
|
|
161
|
-
const winH = window.innerHeight;
|
|
162
|
-
// 直接读取官方主卡片的实际渲染宽度与高度
|
|
163
|
-
const panelW = menuRect.width > 0 ? menuRect.width : 280;
|
|
164
|
-
const panelH = menuRect.height > 0 ? menuRect.height : 360;
|
|
165
|
-
|
|
166
|
-
// 紧靠主卡片左侧,0px 缝隙严丝合缝
|
|
167
|
-
let left = menuRect.left - panelW;
|
|
168
|
-
if (left < margin) {
|
|
169
|
-
if (winW - menuRect.right >= panelW + margin) {
|
|
170
|
-
left = menuRect.right;
|
|
171
|
-
} else {
|
|
172
|
-
left = Math.max(margin, Math.min(menuRect.left, winW - panelW - margin));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 顶部与官方主卡片绝对平齐,高度与官方主卡片 1:1 等高
|
|
177
|
-
let top = menuRect.top;
|
|
178
|
-
setActiveFlyout({
|
|
179
|
-
key: entry.key,
|
|
180
|
-
entry,
|
|
181
|
-
pos: {
|
|
182
|
-
left: Math.round(left),
|
|
183
|
-
top: Math.round(top),
|
|
184
|
-
width: Math.round(panelW),
|
|
185
|
-
height: Math.round(panelH)
|
|
186
|
-
}
|
|
187
|
-
});
|
|
179
|
+
const rect = activeFlyout?.anchorRect ?? (menuRef.current ?? anchorEl).getBoundingClientRect();
|
|
180
|
+
setActiveFlyout({ key: entry.key, entry, anchorRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height } });
|
|
188
181
|
};
|
|
189
182
|
|
|
190
|
-
const triggerSubFlyout = (childGroup
|
|
183
|
+
const triggerSubFlyout = (childGroup) => {
|
|
191
184
|
clearFlyoutTimer();
|
|
192
185
|
if (activeSubFlyout?.key === childGroup.key) return;
|
|
193
|
-
|
|
194
|
-
const flyoutRect = flyoutEl ? flyoutEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
|
|
195
|
-
const margin = 12;
|
|
196
|
-
const winW = window.innerWidth;
|
|
197
|
-
const winH = window.innerHeight;
|
|
198
|
-
const panelW = flyoutRect.width > 0 ? flyoutRect.width : 280;
|
|
199
|
-
const panelH = flyoutRect.height > 0 ? flyoutRect.height : 360;
|
|
200
|
-
|
|
201
|
-
let left = flyoutRect.left - panelW;
|
|
202
|
-
if (left < margin) {
|
|
203
|
-
if (winW - flyoutRect.right >= panelW + margin) {
|
|
204
|
-
left = flyoutRect.right;
|
|
205
|
-
} else {
|
|
206
|
-
left = Math.max(margin, Math.min(flyoutRect.left, winW - panelW - margin));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let top = flyoutRect.top;
|
|
211
|
-
setActiveSubFlyout({
|
|
212
|
-
key: childGroup.key,
|
|
213
|
-
childGroup,
|
|
214
|
-
pos: {
|
|
215
|
-
left: Math.round(left),
|
|
216
|
-
top: Math.round(top),
|
|
217
|
-
width: Math.round(panelW),
|
|
218
|
-
height: Math.round(panelH)
|
|
219
|
-
}
|
|
220
|
-
});
|
|
186
|
+
setActiveSubFlyout({ key: childGroup.key, childGroup });
|
|
221
187
|
};
|
|
222
188
|
|
|
223
189
|
const scheduleCloseFlyout = (delay = 240) => {
|
|
@@ -229,11 +195,14 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
229
195
|
};
|
|
230
196
|
|
|
231
197
|
const scheduleCloseSubFlyout = (delay = 240) => {
|
|
232
|
-
|
|
198
|
+
// Leaving the key panel must not let an older parent-leave timer
|
|
199
|
+
// destroy both the provider and key panels.
|
|
200
|
+
clearFlyoutTimer();
|
|
233
201
|
subFlyoutTimerRef.current = setTimeout(() => {
|
|
234
202
|
setActiveSubFlyout(null);
|
|
235
203
|
}, delay);
|
|
236
204
|
};
|
|
205
|
+
(0, react.useEffect)(() => () => clearFlyoutTimer(), []);
|
|
237
206
|
const [toast, setToast] = (0, react.useState)(null);
|
|
238
207
|
const toastSeq = (0, react.useRef)(0);
|
|
239
208
|
const rootRef = (0, react.useRef)(null);
|
|
@@ -407,6 +376,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
407
376
|
state,
|
|
408
377
|
query
|
|
409
378
|
]);
|
|
379
|
+
const cascade = open && pane === "model" && menuMode === "panel" && !searching && activeFlyout
|
|
380
|
+
? layoutModelFlyouts({ ...activeFlyout.anchorRect, ...(menuPos ?? {}) }, window.innerWidth, window.innerHeight, activeSubFlyout ? 3 : 2)
|
|
381
|
+
: null;
|
|
410
382
|
if (!available) return null;
|
|
411
383
|
const show = () => {
|
|
412
384
|
setSelectionFocus(false);
|
|
@@ -670,7 +642,7 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
670
642
|
title: searching ? text('搜索时自动展开,清空搜索后恢复', 'Expanded during search; clearing restores your groups') : entry.id || entry.name,
|
|
671
643
|
onClick: (e) => {
|
|
672
644
|
if (isFlyoutMode) {
|
|
673
|
-
if (isFlyoutActive) setActiveFlyout(null);
|
|
645
|
+
if (isFlyoutActive) { clearFlyoutTimer(); setActiveFlyout(null); setActiveSubFlyout(null); }
|
|
674
646
|
else triggerFlyout(entry, e.currentTarget);
|
|
675
647
|
} else {
|
|
676
648
|
setHighlightedIndex(null);
|
|
@@ -753,8 +725,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
753
725
|
open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
754
726
|
ref: menuRef,
|
|
755
727
|
id: `${id}-menu`,
|
|
756
|
-
className: ModelSelect_module_css_default.menu,
|
|
757
|
-
|
|
728
|
+
className: clsx(ModelSelect_module_css_default.menu, cascade && "dsh-mg-cascade-panel"),
|
|
729
|
+
"data-flyout-level": cascade ? "0" : undefined,
|
|
730
|
+
style: cascade?.[0] ?? menuPos ?? MEASURE_STYLE,
|
|
758
731
|
role: pane === "model" ? "group" : "menu",
|
|
759
732
|
"aria-label": t("menu.aria"),
|
|
760
733
|
"aria-busy": state.status === "loading" || busy,
|
|
@@ -943,18 +916,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
943
916
|
}, level.key))] })
|
|
944
917
|
]
|
|
945
918
|
}), document.body),
|
|
946
|
-
|
|
919
|
+
cascade !== null && activeFlyout !== null && (0, react_dom.createPortal)(
|
|
947
920
|
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
948
921
|
ref: flyoutRef,
|
|
949
|
-
className: ModelSelect_module_css_default.menu,
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
top: activeFlyout.pos.top,
|
|
953
|
-
width: activeFlyout.pos.width,
|
|
954
|
-
height: activeFlyout.pos.height,
|
|
955
|
-
position: "fixed",
|
|
956
|
-
margin: 0
|
|
957
|
-
},
|
|
922
|
+
className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
|
|
923
|
+
"data-flyout-level": "1",
|
|
924
|
+
style: { ...cascade[1], position: "fixed", margin: 0 },
|
|
958
925
|
onMouseEnter: clearFlyoutTimer,
|
|
959
926
|
onMouseLeave: () => scheduleCloseFlyout(200),
|
|
960
927
|
onMouseDown: (e) => {
|
|
@@ -993,18 +960,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
|
|
|
993
960
|
}),
|
|
994
961
|
document.body
|
|
995
962
|
),
|
|
996
|
-
|
|
963
|
+
cascade !== null && activeSubFlyout !== null && (0, react_dom.createPortal)(
|
|
997
964
|
react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
998
965
|
ref: subFlyoutRef,
|
|
999
|
-
className: ModelSelect_module_css_default.menu,
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
top: activeSubFlyout.pos.top,
|
|
1003
|
-
width: activeSubFlyout.pos.width,
|
|
1004
|
-
height: activeSubFlyout.pos.height,
|
|
1005
|
-
position: "fixed",
|
|
1006
|
-
margin: 0
|
|
1007
|
-
},
|
|
966
|
+
className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
|
|
967
|
+
"data-flyout-level": "2",
|
|
968
|
+
style: { ...cascade[2], position: "fixed", margin: 0 },
|
|
1008
969
|
onMouseEnter: clearFlyoutTimer,
|
|
1009
970
|
onMouseLeave: () => scheduleCloseSubFlyout(200),
|
|
1010
971
|
onMouseDown: (e) => {
|