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 CHANGED
@@ -1,153 +1,117 @@
1
1
  # DSH Model Groups
2
2
 
3
- 给 DeepSeek Harness 的模型选择菜单增加分层折叠,并在自定义模型 API 中管理多份密钥配置。
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
- **当前版本仅支持 DSH `0.2.2-rc.1` 的 Harness 网页界面,安装脚本需要 Node.js 22 或更新版本。**
13
+ ## 环境要求
13
14
 
14
- 在 dsh-go 手机上使用时,功能出现在对应的 Harness 网页中。dsh-go 原生聊天界面的模型菜单不在本插件的修改范围内。
15
+ - DeepSeek Harness **0.2.2-rc.1**。
16
+ - Node.js **22 或以上**。
17
+ - Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
18
+ - DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
15
19
 
16
- 完整安装包括两部分:**注册客户端插件**,以及**应用五项 DSH 兼容补丁**。只安装 npm 包不会自动应用补丁。补丁用于让模型设置、模型发现和请求发送共同识别每份密钥配置。
20
+ **本插件包含宿主兼容补丁。** 插件加载时会检查并备份原文件,自动应用五项补丁;不修改现有供应商、密钥或会话配置。版本或文件内容不受支持时会停止应用,不强制覆盖。
17
21
 
18
- ## 交给 AI 安装
22
+ ## 安装
19
23
 
20
- 把下面整段复制给能够访问 DSH 安装环境的 AI。这是一项安装任务,不是文档审阅任务。
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
- PLUGIN_DIR="${DSH_HOME:-$HOME/.dsh}/local-plugins/dsh-model-groups-0.3.3"
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
- 成功时会列出五个目标文件。`pristine` 表示尚未应用,`patched` 表示已应用本插件对应的补丁。版本、内容或路径不匹配时,脚本报错并停止;请先解决原因。
34
+ 如果安装后插件尚未加载,先启动或重启引擎;若此次启动才应用补丁,再重启一次以载入新模块。补丁已在磁盘就绪时,后续加载不会重复写入。
76
35
 
77
- ### 3. 注册插件
36
+ 当前正式版为 `0.3.4`,包含已验证的自动补丁安装与侧边面板修复。
78
37
 
79
- 以下以 `web` profile 为例;其他实例请填写其实际 profile 名称:
38
+ ### 交给 AI 安装
80
39
 
81
- ```bash
82
- dsh plugin --profile web add "$PLUGIN_DIR"
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
- ### 4. 应用补丁并重启
49
+ 1. 打开 **设置 → 模型**,新增或编辑自定义模型 API。
50
+ 2. 填写供应商公共的 Provider ID、API 地址和协议。
51
+ 3. 开启 **多密钥配置**,为每份配置填写名称、密钥和模型列表,点击“应用”。
52
+ 4. 在会话模型菜单中展开供应商和配置,选择需要的模型。
88
53
 
89
- 保存正在编辑的内容,停止正在使用的 DSH 服务。在外部终端回到步骤 1 的插件目录,使用步骤 2 确认的 DSH 路径执行:
90
-
91
- ```bash
92
- read -r -p '请输入实际 DSH 安装目录:' DSH_ROOT
93
- node scripts/native-patches.mjs --dsh-root "$DSH_ROOT" --apply
54
+ ```text
55
+ 示例供应商 A
56
+ 配置 A
57
+ 示例模型
58
+ 配置 B
59
+ 示例模型
60
+ 示例供应商 B
61
+ 示例模型
94
62
  ```
95
63
 
96
- 补丁脚本会校验版本与文件哈希,备份原文件后写入变更。已经应用的补丁会跳过;脚本不会启动或停止 DSH。
64
+ 已有单密钥配置与旧会话选择会保留。开启多密钥后,可在模型菜单手动选择新配置;插件不会自动合并已有供应商或迁移会话选择。
97
65
 
98
- 用原来的方式启动 DSH,然后刷新原网页。dsh-go 本机环境中,先准备好外部终端的执行方式,再停止引擎;停止后当前 AI 对话无法继续替你执行命令。
66
+ 关闭多密钥配置会保留已保存的配置与凭据,删除配置也不会同时删除凭据。有尚未保存的密钥输入时,需要先保存或清空,才能确认关闭。
99
67
 
100
- ### 5. 确认安装结果
68
+ 窄屏侧边面板会根据可用空间调整各级宽度;三级并列时,较长名称可能截断。
101
69
 
102
- - 会话中的模型菜单按供应商分组,能够展开和收起。
103
- - 在“设置 → 模型”编辑支持的自定义模型 API,可以看到“多密钥配置”开关。
104
- - 开启并保存多份配置后,模型菜单出现对应的配置层级。
70
+ ## 更新
105
71
 
106
- 只有客户端分组出现、设置页没有新开关时,请检查五项补丁是否全部应用,以及原 DSH 服务是否已重启。
72
+ 使用对应发布版本的安装命令更新,随后按加载日志提示重启引擎并刷新网页。兼容补丁已经存在且未变化时,不会重复应用。
107
73
 
108
- ## 使用方法
74
+ DSH 升级前请确认插件兼容范围。新版本或未知文件会被拒绝,需要等待对应适配,不能强制套用旧补丁。
109
75
 
110
- 在“设置 → 模型”新增或编辑自定义模型 API,填写公共的 Provider ID、API 地址和协议。开启“多密钥配置”,为每份配置填写名称、密钥和模型列表,点击“应用”保存。
76
+ DSHMarket 的收录需经过社区审核。普通插件的市场更新跟随 npm `latest`,不会自动跟随 GitHub 发布包或 npm `beta` 标签。
111
77
 
112
- 例如,两个配置都提供同一模型时,菜单中会分别显示:
78
+ ## 安装诊断
113
79
 
114
- ```text
115
- 示例供应商 A
116
- 配置 A
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
- 插件不会自动合并已有供应商,也不会替用户迁移密钥。API 地址不同的服务应分别配置。
90
+ 原文件备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`,不会随插件包更新丢失。安装意外中断可能留下 `patch.lock`;确认没有安装进程后再处理,不要并发重试。
129
91
 
130
- ## 升级与卸载
92
+ ## 卸载与还原
131
93
 
132
- **DSH 升级后,请先确认插件支持的新版本,不能把本版补丁强行应用到其他 DSH 版本。**
94
+ 移除插件不会自动还原宿主补丁或删除凭据。完整还原前,请先:
133
95
 
134
- 仅停用或移除客户端插件不会还原 DSH 兼容补丁。完整卸载前:
96
+ 1. 将相关会话和子代理切换回普通单密钥模型。
97
+ 2. 关闭多密钥配置,备份配置并清理原版 DSH 不支持的 `multiKey`、`keyEntries` 字段,保留仍需使用的凭据。
98
+ 3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
135
99
 
136
- 1. 在相关会话和子代理配置中切换回普通单密钥模型,确认不再使用带 `@dsh-key/` 前缀的选择。
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
- 其他 profile 请替换 `web`。还原脚本只恢复代码,不会替用户修改会话、配置或凭据;仍引用多密钥模型的会话在还原后将无法继续使用该选择。
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
- 从 GitHub 获取源码后,运行:
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
- 需要检查真实 DSH 目标文件时,可设置 `DSH_ROOT` 后运行测试。测试覆盖层级、折叠状态、选择器交互和补丁安装器;真实网页中的安装验收仍需按上文完成。
122
+ 设置 `DSH_ROOT` 可启用依赖对应 DSH 程序文件的回归测试。源码包含层级、选择器交互、面板布局及补丁安装与恢复测试。
123
+
124
+ ## 许可证
159
125
 
160
- 本项目采用 MIT 许可证。客户端选择器包含改编自 DeepSeek Harness 的代码,保留原版权声明。
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 menuEl = menuRef.current;
341
- const menuRect = menuEl ? menuEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
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, anchorEl) => {
366
+ const triggerSubFlyout = (childGroup) => {
374
367
  clearFlyoutTimer();
375
368
  if (activeSubFlyout?.key === childGroup.key) return;
376
- const flyoutEl = flyoutRef.current;
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
- if (subFlyoutTimerRef.current !== null) clearTimeout(subFlyoutTimerRef.current);
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
- style: menuPos ?? MEASURE_STYLE,
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
- open && pane === "model" && menuMode === "panel" && activeFlyout !== null && (0, react_dom.createPortal)(
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
- style: {
1134
- left: activeFlyout.pos.left,
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
- open && pane === "model" && menuMode === "panel" && activeSubFlyout !== null && (0, react_dom.createPortal)(
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
- style: {
1184
- left: activeSubFlyout.pos.left,
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
  };
@@ -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",
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
- const artifact = JSON.parse(fs.readFileSync(path.join(artifactDirectory, name), 'utf8'));
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
- // Client-only picker. Native multi-key support is installed explicitly via
2
- // scripts/native-patches.mjs, never by mutating runtime code during apply.
3
- // This plugin owns no provider credentials or independent grouping settings.
4
- export function apply() {}
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
+ }
@@ -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 menuEl = menuRef.current;
158
- const menuRect = menuEl ? menuEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
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, anchorEl) => {
183
+ const triggerSubFlyout = (childGroup) => {
191
184
  clearFlyoutTimer();
192
185
  if (activeSubFlyout?.key === childGroup.key) return;
193
- const flyoutEl = flyoutRef.current;
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
- if (subFlyoutTimerRef.current !== null) clearTimeout(subFlyoutTimerRef.current);
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
- style: menuPos ?? MEASURE_STYLE,
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
- open && pane === "model" && menuMode === "panel" && activeFlyout !== null && (0, react_dom.createPortal)(
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
- style: {
951
- left: activeFlyout.pos.left,
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
- open && pane === "model" && menuMode === "panel" && activeSubFlyout !== null && (0, react_dom.createPortal)(
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
- style: {
1001
- left: activeSubFlyout.pos.left,
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) => {