dsh-model-groups 0.3.2 → 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 +78 -79
- package/lib/client.js +63 -86
- package/native/pi-ai.patch.json +12 -0
- package/package.json +3 -2
- 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,127 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DSH Model Groups
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
为 DeepSeek Harness 网页端提供分层模型菜单与多密钥管理。同一供应商共用 Provider ID、API 地址和协议,各配置分别设置名称、密钥与模型列表。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 功能
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
- 按“供应商 → 密钥配置 → 模型”浏览和选择。
|
|
8
|
+
- 支持原地折叠、侧边面板、名称搜索、全部展开与收起。
|
|
9
|
+
- 记住各级折叠状态;搜索时临时展开匹配项。
|
|
10
|
+
- 不同密钥配置可使用相同模型 ID,选择时使用对应配置的密钥。
|
|
11
|
+
- 手动选择配置,不自动轮换密钥或切换到其他配置。
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
示例供应商 A(同一个 Provider ID / API 地址)
|
|
14
|
-
配置 A(独立密钥)
|
|
15
|
-
示例模型
|
|
16
|
-
配置 B(另一份密钥)
|
|
17
|
-
示例模型
|
|
18
|
-
示例供应商 B
|
|
19
|
-
示例模型
|
|
20
|
-
```
|
|
13
|
+
## 环境要求
|
|
21
14
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- 只使用手动选中的配置。密钥缺失、配置移除或多密钥配置关闭时,旧配置选择会失败或从菜单移除,**不会改用其他密钥、轮换或自动故障切换**。
|
|
27
|
-
- 保存前可用输入的 API 密钥获取模型;已保存的配置按其精确标识在宿主查询。不会默默改用供应商原来的密钥。
|
|
28
|
-
- API 密钥使用原生的只写输入与凭据服务,不会回显已保存的明文。留空保留原凭据;替换共享引用的密钥需要明确确认。
|
|
15
|
+
- DeepSeek Harness **0.2.2-rc.1**。
|
|
16
|
+
- Node.js **22 或以上**。
|
|
17
|
+
- Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
|
|
18
|
+
- DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
**本插件包含宿主兼容补丁。** 插件加载时会检查并备份原文件,自动应用五项补丁;不修改现有供应商、密钥或会话配置。版本或文件内容不受支持时会停止应用,不强制覆盖。
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
- 配置名称位于卡片内部的标题栏,移除按钮在标题栏右侧;不再使用挂在边框上的图例标题。长名称与凭据引用允许换行。
|
|
34
|
-
- 支持多密钥的已有自定义供应商先显示「公共设置」,再显示密钥配置;其他供应商保持原有表单顺序与折叠状态。
|
|
35
|
-
- 点击关闭不会立即停用,先显示原生确认弹窗。取消、关闭按钮、Escape 或遮罩关闭只取消确认,不会改变开关或丢弃输入;确认后仍须点击「应用」才改变运行配置。
|
|
36
|
-
- **有尚未保存的 API 密钥输入时禁止确认关闭**,只提供「返回密钥配置」。输入仅为空格也触发保护;凭据保存失败留下的输入同样受保护。先保存成功,或明确清空不需要的输入,再关闭。
|
|
22
|
+
## 安装
|
|
37
23
|
|
|
38
|
-
|
|
24
|
+
在目标设备的 DSH 环境中执行:
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
```bash
|
|
27
|
+
dsh plugin --profile web add dsh-model-groups@0.3.4 --registry=https://registry.npmjs.org/
|
|
28
|
+
```
|
|
41
29
|
|
|
42
|
-
|
|
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。
|
|
43
31
|
|
|
44
|
-
|
|
32
|
+
插件加载并提示“补丁已自动安装”后,保存草稿,**重启原 DSH 引擎并刷新网页**。无需先停止引擎,也不需要外部终端手工打补丁。
|
|
45
33
|
|
|
46
|
-
|
|
34
|
+
如果安装后插件尚未加载,先启动或重启引擎;若此次启动才应用补丁,再重启一次以载入新模块。补丁已在磁盘就绪时,后续加载不会重复写入。
|
|
47
35
|
|
|
48
|
-
|
|
36
|
+
当前正式版为 `0.3.4`,包含已验证的自动补丁安装与侧边面板修复。
|
|
49
37
|
|
|
50
|
-
|
|
38
|
+
### 交给 AI 安装
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
```text
|
|
41
|
+
请按 https://github.com/betsunninda9-lgtm/dsh-model-groups 的安装说明,
|
|
42
|
+
使用 npm 正式版安装到当前 DSH profile。
|
|
43
|
+
我允许插件自动校验、备份并应用宿主补丁;保留现有配置。
|
|
44
|
+
检查加载日志,成功后提醒我重启原引擎并刷新网页;遇到不兼容就停止报告。
|
|
45
|
+
```
|
|
57
46
|
|
|
58
|
-
##
|
|
47
|
+
## 使用
|
|
59
48
|
|
|
60
|
-
|
|
49
|
+
1. 打开 **设置 → 模型**,新增或编辑自定义模型 API。
|
|
50
|
+
2. 填写供应商公共的 Provider ID、API 地址和协议。
|
|
51
|
+
3. 开启 **多密钥配置**,为每份配置填写名称、密钥和模型列表,点击“应用”。
|
|
52
|
+
4. 在会话模型菜单中展开供应商和配置,选择需要的模型。
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
```text
|
|
55
|
+
示例供应商 A
|
|
56
|
+
配置 A
|
|
57
|
+
示例模型
|
|
58
|
+
配置 B
|
|
59
|
+
示例模型
|
|
60
|
+
示例供应商 B
|
|
61
|
+
示例模型
|
|
62
|
+
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
已有单密钥配置与旧会话选择会保留。开启多密钥后,可在模型菜单手动选择新配置;插件不会自动合并已有供应商或迁移会话选择。
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
关闭多密钥配置会保留已保存的配置与凭据,删除配置也不会同时删除凭据。有尚未保存的密钥输入时,需要先保存或清空,才能确认关闭。
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
npm pack dsh-model-groups@0.3.2 --registry=https://registry.npmjs.org
|
|
70
|
-
mkdir dsh-model-groups
|
|
71
|
-
tar -xzf dsh-model-groups-0.3.2.tgz -C dsh-model-groups --strip-components=1
|
|
72
|
-
cd dsh-model-groups
|
|
73
|
-
```
|
|
68
|
+
窄屏侧边面板会根据可用空间调整各级宽度;三级并列时,较长名称可能截断。
|
|
74
69
|
|
|
75
|
-
|
|
70
|
+
## 更新
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
使用对应发布版本的安装命令更新,随后按加载日志提示重启引擎并刷新网页。兼容补丁已经存在且未变化时,不会重复应用。
|
|
73
|
+
|
|
74
|
+
DSH 升级前请确认插件兼容范围。新版本或未知文件会被拒绝,需要等待对应适配,不能强制套用旧补丁。
|
|
75
|
+
|
|
76
|
+
DSHMarket 的收录需经过社区审核。普通插件的市场更新跟随 npm `latest`,不会自动跟随 GitHub 发布包或 npm `beta` 标签。
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
node scripts/native-patches.mjs --dsh-root /实际安装的DSH根目录 --apply
|
|
78
|
+
## 安装诊断
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
没有出现“多密钥配置”开关时,先确认原引擎已在自动补丁后重启,并刷新原网页。仍未恢复,可在已安装插件目录执行:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node scripts/doctor.mjs --dsh-root /opt/dsh --json
|
|
86
84
|
```
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
`/opt/dsh` 请替换为实际 DSH 安装目录。五项文件应为 `patched`。该命令只读检查程序文件,不读取密钥;磁盘状态正常不代表运行中的模块已刷新。
|
|
87
|
+
|
|
88
|
+
自动安装日志位于原引擎日志中。遇到文件不受支持、目录布局不兼容或权限不足时,请提供程序包版本与文件 SHA-256,勿提供密钥。
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
原文件备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`,不会随插件包更新丢失。安装意外中断可能留下 `patch.lock`;确认没有安装进程后再处理,不要并发重试。
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
## 卸载与还原
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
移除插件不会自动还原宿主补丁或删除凭据。完整还原前,请先:
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
1. 将相关会话和子代理切换回普通单密钥模型。
|
|
97
|
+
2. 关闭多密钥配置,备份配置并清理原版 DSH 不支持的 `multiKey`、`keyEntries` 字段,保留仍需使用的凭据。
|
|
98
|
+
3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
先停用或移除插件,避免其再次加载时重新应用补丁:
|
|
99
101
|
|
|
100
102
|
```bash
|
|
101
|
-
node scripts/native-patches.mjs --dsh-root /实际安装的DSH根目录 --revert
|
|
102
103
|
dsh plugin --profile web remove dsh-model-groups
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
随后安排维护窗口,停止原服务,在保留的插件目录执行:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node scripts/native-patches.mjs --dsh-root /opt/dsh --revert
|
|
110
|
+
```
|
|
106
111
|
|
|
107
|
-
|
|
112
|
+
完成后启动原服务并刷新网页。还原脚本按校验过的文件版本恢复代码,不修改会话或凭据。仍引用 `@dsh-key/` 模型的会话在还原后无法继续使用该选择。
|
|
108
113
|
|
|
109
|
-
|
|
114
|
+
## 开发
|
|
110
115
|
|
|
111
116
|
```bash
|
|
112
117
|
npm run build
|
|
113
118
|
npm test
|
|
114
|
-
npm run test:ui
|
|
115
119
|
npm run check:build
|
|
116
|
-
npm run native:check -- --dsh-root /实际安装的DSH根目录
|
|
117
120
|
```
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
- [原生补丁管理器](scripts/native-patches.mjs)提供版本 / 哈希检查、应用与还原;[补丁目录](native/)存放可重放变更。
|
|
121
|
-
- 仓库内的测试覆盖层级、折叠状态、样式生命周期、客户端注册与[选择器交互](test/picker-ui.test.mjs),不读取真实凭据。
|
|
122
|
-
- 公开仓库不包含本机的宿主备份、配置、会话或依赖手机目录的历史测试。适配器与原生设置页的完整集成验收需要单独准备对应版本的 DSH 环境。
|
|
123
|
-
- [补丁安装器回归](test/native-patches.test.mjs)在临时目录实际应用 / 回退,并模拟写入失败、原子替换失败和并发修改;不重装或重启真实服务。
|
|
122
|
+
设置 `DSH_ROOT` 可启用依赖对应 DSH 程序文件的回归测试。源码包含层级、选择器交互、面板布局及补丁安装与恢复测试。
|
|
124
123
|
|
|
125
|
-
|
|
124
|
+
## 许可证
|
|
126
125
|
|
|
127
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-groups",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "DSH 网页模型选择器:供应商与密钥配置分层折叠、多密钥手动选择。支持 DSH 0.2.2-rc.1。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -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) => {
|