dsh-model-groups 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,117 +1,191 @@
1
1
  # DSH Model Groups
2
2
 
3
- 为 DeepSeek Harness 网页端提供分层模型菜单与多密钥管理。同一供应商共用 Provider ID、API 地址和协议,各配置分别设置名称、密钥与模型列表。
3
+ 为 DeepSeek Harness(DSH)提供分层模型选择与多密钥配置管理。
4
4
 
5
- ## 功能
5
+ 插件在 DSH 网页界面中提供“供应商 → 密钥配置 → 模型”三级模型菜单,并支持在同一供应商下维护多份独立的密钥与模型列表。
6
6
 
7
- - 按“供应商 → 密钥配置 → 模型”浏览和选择。
8
- - 支持原地折叠、侧边面板、名称搜索、全部展开与收起。
9
- - 记住各级折叠状态;搜索时临时展开匹配项。
10
- - 不同密钥配置可使用相同模型 ID,选择时使用对应配置的密钥。
11
- - 手动选择配置,不自动轮换密钥或切换到其他配置。
7
+ ## 核心功能
12
8
 
13
- ## 环境要求
9
+ - **三级分层选择**:在会话模型菜单中按“供应商 → 密钥配置 → 模型”浏览和选择模型。
10
+ - **同供应商多密钥管理**:同一供应商共享 Provider ID 与 API 地址,每份密钥配置独立设置名称、API 密钥、模型列表及 API 协议;未指定协议的旧配置继续跟随供应商,不需迁移或重建密钥。
11
+ - **每组独立协议**:支持 OpenAI Chat Completions、OpenAI Responses 与 Anthropic Messages,同一个模型 ID 也可在不同密钥组走不同协议;只按显式选择发送,不按模型名称猜测。
12
+ - **手动选择,不自动切换**:不同密钥配置可使用相同的模型 ID;每次调用严格使用所选配置的密钥,不自动轮换密钥,也不偷偷切换到其他配置或服务。
13
+ - **多种浏览与搜索方式**:支持“原地折叠”与“侧边面板”两种展示模式,支持名称搜索、展开全部与收起全部,并自动记住各级折叠状态(搜索时临时展开匹配项)。
14
+ - **配置项折叠编辑**:在模型设置页中,已保存的密钥配置默认折叠,仅保留配置名称与原密钥输入栏(继续沿用宿主原有的密钥遮罩与存储机制,不直接显示已保存密钥明文);点击“展开详情”可编辑模型列表,新建项与校验未通过项保持展开,收起详情不会丢失未保存草稿。
15
+ - **网页端补丁维护**:在“设置 → 模型分组”页面手动启用、更新或关闭多密钥补丁,并提供“关闭并卸载插件”操作及脱敏诊断复制。
14
16
 
15
- - DeepSeek Harness **0.2.2-rc.1**。
16
- - Node.js **22 或以上**。
17
- - Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
18
- - DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
17
+ ## 支持环境
19
18
 
20
- **本插件包含宿主兼容补丁。** 插件加载时会检查并备份原文件,自动应用五项补丁;不修改现有供应商、密钥或会话配置。版本或文件内容不受支持时会停止应用,不强制覆盖。
19
+ - **DeepSeek Harness**:`0.2.2-rc.1`
20
+ - **Node.js**:`>=22`
21
+ - **适用界面**:DSH 网页界面(不影响非网页端的原生模型菜单)
22
+ - **文件权限**:DSH 安装目录需可写,且目标程序文件需符合受支持版本的原始或已知补丁哈希
21
23
 
22
- ## 安装
24
+ ## 安装与首次启用
23
25
 
24
- 在目标设备的 DSH 环境中执行:
26
+ 插件分为**安装插件包**与**启用多密钥补丁**两步。加载插件本身不会自动修改宿主文件;未启用多密钥补丁时,仍可正常使用分层模型菜单浏览普通单密钥模型。
27
+
28
+ ### 1. 安装插件
29
+
30
+ 在 DSH 运行环境中执行以下命令安装正式版 npm 包:
25
31
 
26
32
  ```bash
27
- dsh plugin --profile web add dsh-model-groups@0.3.4 --registry=https://registry.npmjs.org/
33
+ dsh plugin --profile web add dsh-model-groups
28
34
  ```
29
35
 
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。
36
+ 也可以指定具体版本号安装:
31
37
 
32
- 插件加载并提示“补丁已自动安装”后,保存草稿,**重启原 DSH 引擎并刷新网页**。无需先停止引擎,也不需要外部终端手工打补丁。
38
+ ```bash
39
+ dsh plugin --profile web add dsh-model-groups@0.3.6
40
+ ```
33
41
 
34
- 如果安装后插件尚未加载,先启动或重启引擎;若此次启动才应用补丁,再重启一次以载入新模块。补丁已在磁盘就绪时,后续加载不会重复写入。
42
+ > 备用方式:若当前环境无法访问 npm 源,可改用对应 GitHub Release 发布的 `.tgz` 包地址进行安装。
35
43
 
36
- 当前正式版为 `0.3.4`,包含已验证的自动补丁安装与侧边面板修复。
44
+ 安装完成后,**重启原 DSH 引擎并刷新网页**,使插件加载生效。
37
45
 
38
- ### 交给 AI 安装
46
+ ### 2. 启用多密钥补丁
39
47
 
40
- ```text
41
- 请按 https://github.com/betsunninda9-lgtm/dsh-model-groups 的安装说明,
42
- 使用 npm 正式版安装到当前 DSH profile。
43
- 我允许插件自动校验、备份并应用宿主补丁;保留现有配置。
44
- 检查加载日志,成功后提醒我重启原引擎并刷新网页;遇到不兼容就停止报告。
45
- ```
48
+ 如需使用同一供应商下的多密钥配置功能,请在网页界面完成启用:
49
+
50
+ 1. 打开 **设置 → 模型分组**。
51
+ 2. 查看“当前状态”卡片,确认无冲突或活动任务限制。
52
+ 3. 在“多密钥操作”区域点击 **启用多密钥**,阅读提示后点击 **确认执行**。
53
+ 4. 页面提示已实际修改宿主文件后,**重启原 DSH 引擎并刷新网页**。
54
+
55
+ > **说明**:启用操作会校验并备份受支持的 5 个宿主文件(备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`),再写入多密钥补丁。若遇到未知版本或已被其他修改覆盖的文件,插件会拒绝写入,不会强制覆盖。
56
+
57
+ ## 日常使用
58
+
59
+ ### 1. 配置多密钥与模型
46
60
 
47
- ## 使用
61
+ 1. 打开 **设置 → 模型**,新增或编辑自定义模型供应商。
62
+ 2. 在 **公共设置** 中填写共享的显示名称、API 地址与协议。
63
+ 3. 开启 **多密钥配置** 开关。
64
+ 4. 点击 **添加密钥配置**(首次开启会自动生成一项),为每份配置填写 **配置名称**、**API 密钥** 并配置模型列表:
65
+ - 已保存的密钥配置默认折叠;如需修改配置名称、增删模型或移除该配置,点击 **展开详情**,完成后可点击 **收起详情**。
66
+ - 在 **分组 API 协议** 中选择“跟随供应商”或该中转站支持的协议。供应商地址仍为共享地址;不自动按模型名切换协议。协议选项、模型获取与请求发送均按当前密钥组生效。
67
+ - 已配置密钥的输入栏留空即可保留原密钥;输入新密钥会替换对应凭据引用。
68
+ 5. 点击底部操作栏的 **应用**(或新建时的保存按钮)提交修改。保存成功后,已确认无误的密钥配置会自动折叠。
48
69
 
49
- 1. 打开 **设置 → 模型**,新增或编辑自定义模型 API。
50
- 2. 填写供应商公共的 Provider ID、API 地址和协议。
51
- 3. 开启 **多密钥配置**,为每份配置填写名称、密钥和模型列表,点击“应用”。
52
- 4. 在会话模型菜单中展开供应商和配置,选择需要的模型。
70
+ 如需在单个供应商内关闭 **多密钥配置** 开关:
71
+ - 若当前有已输入但尚未保存的 API 密钥,需先点击“应用”保存或清空输入,才能关闭开关。
72
+ - 关闭开关并应用后,该供应商恢复使用原有的单密钥配置;已保存的多密钥配置项和密钥凭据不会被删除。
73
+
74
+ ### 2. 在会话中选择模型
75
+
76
+ 打开对话输入框旁的模型菜单,按层级展开并选择目标模型:
53
77
 
54
78
  ```text
55
79
  示例供应商 A
56
- 配置 A
57
- 示例模型
58
- 配置 B
59
- 示例模型
80
+ ├─ 密钥配置 A
81
+ │ └─ deepseek-chat
82
+ └─ 密钥配置 B
83
+ └─ deepseek-chat
60
84
  示例供应商 B
61
- 示例模型
85
+ └─ gpt-4o
62
86
  ```
63
87
 
64
- 已有单密钥配置与旧会话选择会保留。开启多密钥后,可在模型菜单手动选择新配置;插件不会自动合并已有供应商或迁移会话选择。
88
+ - **展示模式切换**:在菜单底部可切换 **侧边面板**(多级悬浮面板,窄屏下自动调整各级宽度,名称过长时截断显示)与 **原地折叠**(单列表内树状展开)。
89
+ - **快捷操作**:菜单顶部工具栏支持搜索模型或配置名称,以及一键 **展开全部** / **收起全部**。
90
+ - **旧会话兼容**:开启多密钥前已选中原单密钥模型的会话会保留原选择,并在对应供应商下显示为“原单密钥(兼容旧会话)”;插件不会自动合并已有供应商或自动迁移旧会话的模型选择。
65
91
 
66
- 关闭多密钥配置会保留已保存的配置与凭据,删除配置也不会同时删除凭据。有尚未保存的密钥输入时,需要先保存或清空,才能确认关闭。
92
+ ## 更新
67
93
 
68
- 窄屏侧边面板会根据可用空间调整各级宽度;三级并列时,较长名称可能截断。
94
+ 1. 执行安装命令更新插件包:
95
+ ```bash
96
+ dsh plugin --profile web add dsh-model-groups@latest
97
+ ```
98
+ 2. 更新插件包后,先 **重启原 DSH 引擎并刷新网页**。
99
+ 3. 打开 **设置 → 模型分组** 查看补丁状态:
100
+ - 若多密钥操作按钮显示为 **更新多密钥补丁**(提示“多密钥补丁有更新可用”),请点击该按钮并 **确认执行**,完成后再次 **重启原 DSH 引擎并刷新网页**。
101
+ - 若状态显示“多密钥已启用(磁盘状态)”且无更新提示,说明当前宿主补丁已是最新兼容版本,无需重复重打补丁。
69
102
 
70
- ## 更新
103
+ ### 很老的版本能直接更新吗?
71
104
 
72
- 使用对应发布版本的安装命令更新,随后按加载日志提示重启引擎并刷新网页。兼容补丁已经存在且未变化时,不会重复应用。
105
+ 可以直接更新**已识别的历史补丁**,不必逐版安装、先关闭多密钥或先卸载。正式版保留历史补丁的精确逆向配方:先验证旧文件可还原为它自己的原版,再生成该构建对应的最新补丁;5 个文件全部通过后才备份并写入。不按插件版本号猜测文件内容,也不会拿另一设备的构建覆盖当前构建。
73
106
 
74
- DSH 升级前请确认插件兼容范围。新版本或未知文件会被拒绝,需要等待对应适配,不能强制套用旧补丁。
107
+ | 当前情况 | 最新版处理 |
108
+ | --- | --- |
109
+ | 未打补丁的受支持原版 | 手动确认“启用多密钥” |
110
+ | 已识别的旧补丁,包括旧自动应用版本 | 直接提示“更新多密钥补丁”,不重建配置、密钥 ID 或会话 |
111
+ | 补丁已是本构建最新版 | 只检查,不重复写入 |
112
+ | 仅识别旧补丁、缺少验证过的新版配方 | 整批阻止更新,保留已支持的检查与还原能力;反馈诊断,不建议先还原 |
113
+ | 未知哈希、缺文件、不同宿主版本或不安全路径 | 整批停止,不覆盖;详情显示固定文件标识、版本及程序哈希 |
114
+ | 网页与插件后端版本不一致 | 禁止维护,先重启原引擎并刷新网页,再重新确认 |
75
115
 
76
- DSHMarket 的收录需经过社区审核。普通插件的市场更新跟随 npm `latest`,不会自动跟随 GitHub 发布包或 npm `beta` 标签。
116
+ 旧版中有自动应用补丁的版本(也包括 `0.3.5-beta.1`~`beta.3`);最新版加载插件**不会自动改宿主**。即使安装器热重载了插件,或页面显示磁盘文件齐全,也不能证明旧进程已加载新宿主模块,仍应按上述顺序重启原引擎和刷新原网页。
77
117
 
78
- ## 安装诊断
118
+ 从 GitHub 测试分支安装的用户,请在**原 profile** 执行上面的 npm `@latest` 命令切回正式渠道,再按本节复查;分支来源不会因正式发版或删除测试分支自动迁移。若实际使用的 profile 不是 `web`,请使用原 profile 名,不另建一份插件。
79
119
 
80
- 没有出现“多密钥配置”开关时,先确认原引擎已在自动补丁后重启,并刷新原网页。仍未恢复,可在已安装插件目录执行:
120
+ 若更新后仍有冲突:先复制脱敏诊断,保留现场;**不要先卸载、还原补丁、删除配置或重建密钥**。需要适配时,只提供详情标记的程序文件,不提供凭据、配置或聊天记录。旧配置未填写分组协议时继续跟随供应商;显式切换协议也不保证上游一定返回缓存命中,缓存统计以实际 API usage 为准。
81
121
 
82
- ```bash
83
- node scripts/doctor.mjs --dsh-root /opt/dsh --json
84
- ```
122
+ > **升级 DSH 主程序前请注意**:本插件补丁仅适配受支持的 DSH 版本(当前为 `0.2.2-rc.1`)。升级 DSH 前请先确认插件已完成对应版本适配,或在理解“关闭与卸载后的影响”并处理多密钥引用后,再关闭补丁;不要为解决插件更新冲突而盲目还原。
123
+
124
+ ## 关闭与卸载
125
+
126
+ **请勿直接从普通插件列表或命令行卸载仍启用补丁的插件。** 普通卸载只会移除插件包本身,不会还原已修改的宿主文件。
85
127
 
86
- `/opt/dsh` 请替换为实际 DSH 安装目录。五项文件应为 `patched`。该命令只读检查程序文件,不读取密钥;磁盘状态正常不代表运行中的模块已刷新。
128
+ ### 推荐方式:通过设置页关闭或卸载
87
129
 
88
- 自动安装日志位于原引擎日志中。遇到文件不受支持、目录布局不兼容或权限不足时,请提供程序包版本与文件 SHA-256,勿提供密钥。
130
+ 维护前请先停止正在运行的对话或任务,并保存其他设置页的草稿:
89
131
 
90
- 原文件备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`,不会随插件包更新丢失。安装意外中断可能留下 `patch.lock`;确认没有安装进程后再处理,不要并发重试。
132
+ 1. 打开 **设置 → 模型分组**。
133
+ 2. 根据需要选择操作并点击 **确认执行**:
134
+ - **仅关闭多密钥(保留插件)**:点击 **关闭多密钥**。宿主文件将恢复为原版,插件本身保留,可继续作为普通模型的分组选择器使用。
135
+ - **还原补丁并卸载插件**:点击 **关闭并卸载插件**。系统会先校验并恢复宿主原版文件,验证成功后再移除插件包,并清理本插件在当前浏览器的两个显示偏好项(`dsh:model-groups:v1` 与 `dsh:model-groups:mode`)。
136
+ 3. 操作完成并提示需要重启后,**重启原 DSH 引擎并刷新网页**。
91
137
 
92
- ## 卸载与还原
138
+ ### 关闭与卸载后的影响说明
93
139
 
94
- 移除插件不会自动还原宿主补丁或删除凭据。完整还原前,请先:
140
+ - **不删除用户数据**:关闭多密钥或卸载插件不会删除已保存的 API 密钥、供应商配置、聊天历史或文件备份。
141
+ - **不自动切换或清理配置**:插件不会自动将使用多密钥模型的会话、默认模型或子代理配置切换回普通模型,也不会自动清理配置文件中的 `multiKey` 与 `keyEntries` 字段。
142
+ - **后续使用限制**:还原宿主文件后,原版 DSH 不支持识别 `@dsh-key/` 多密钥模型路由及 `multiKey` / `keyEntries` 配置字段。仍引用多密钥模型的旧会话在手动改选普通模型前将无法直接继续请求;残留的多密钥配置字段也可能影响原版引擎的配置校验或重启。建议在关闭或卸载前,先将相关会话切换回普通单密钥模型,并在需要时关闭对应供应商的多密钥开关或清理相关配置字段。
95
143
 
96
- 1. 将相关会话和子代理切换回普通单密钥模型。
97
- 2. 关闭多密钥配置,备份配置并清理原版 DSH 不支持的 `multiKey`、`keyEntries` 字段,保留仍需使用的凭据。
98
- 3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
144
+ ### 命令行备用还原方式
99
145
 
100
- 先停用或移除插件,避免其再次加载时重新应用补丁:
146
+ 若已通过命令行移除插件或无法进入网页设置页,可在停止原 DSH 服务后,使用保留的插件目录执行脚本恢复宿主文件:
101
147
 
102
148
  ```bash
103
- dsh plugin --profile web remove dsh-model-groups
149
+ node scripts/native-patches.mjs --dsh-root /opt/dsh --revert
104
150
  ```
105
151
 
106
- 随后安排维护窗口,停止原服务,在保留的插件目录执行:
152
+ 请将 `/opt/dsh` 替换为实际的 DSH 安装目录。完成后重新启动原 DSH 服务并刷新网页。
153
+
154
+ ## 常见问题
155
+
156
+ ### 1. 为什么在“设置 → 模型”里看不到“多密钥配置”开关?
157
+
158
+ - 请先到 **设置 → 模型分组** 确认是否已点击 **启用多密钥**。安装插件后默认不会自动打补丁。
159
+ - 若已点击“启用多密钥”,请确认操作完成后已经 **重启原 DSH 引擎** 并 **刷新网页**。
160
+ - “多密钥配置”仅适用于自定义或已声明的 `pi-ai` 供应商编辑卡片,仅输入凭据的简化卡片不显示该开关。
161
+
162
+ ### 2. “刷新检查”、“刷新网页”和“重启引擎”有什么区别?
163
+
164
+ - **刷新检查**(位于“设置 → 模型分组”):仅只读检查磁盘上的宿主文件状态与依赖提醒,不会修改任何文件,也不意味着需要重启引擎。
165
+ - **刷新网页**(浏览器刷新):重新加载前端网页资源,使前端界面与当前引擎状态同步。
166
+ - **重启原 DSH 引擎**:重启运行中的 DSH 后端服务进程。更新插件包或遇到网页/后端版本不一致时,应先重启并刷新以加载同一版维护逻辑;执行“启用多密钥”、“更新多密钥补丁”、“关闭多密钥”或“关闭并卸载插件”并实际修改宿主文件后,还必须再次重启以加载新宿主模块。
167
+
168
+ ### 3. 为什么页面显示“多密钥已启用(磁盘状态)”,但提示运行时状态未知?
169
+
170
+ 磁盘校验只能确认安装目录中的文件已写入补丁,无法直接证明当前运行中的 Node.js 进程是在打补丁前还是打补丁后启动的。只要在修改补丁后重启过一次原引擎并刷新了网页,即可正常使用。
171
+
172
+ ### 4. 如何进行只读安装诊断或反馈问题?
173
+
174
+ 可在网页 **设置 → 模型分组** 底部展开 **检查详情与脱敏诊断**,点击 **复制脱敏诊断**。仅包含固定状态、错误码、插件/宿主版本及程序文件 SHA-256,不含文件路径、程序正文、密钥、配置或聊天记录。
175
+
176
+ 也可在插件目录运行只读命令行诊断:
107
177
 
108
178
  ```bash
109
- node scripts/native-patches.mjs --dsh-root /opt/dsh --revert
179
+ node scripts/doctor.mjs --dsh-root /opt/dsh --json
110
180
  ```
111
181
 
112
- 完成后启动原服务并刷新网页。还原脚本按校验过的文件版本恢复代码,不修改会话或凭据。仍引用 `@dsh-key/` 模型的会话在还原后无法继续使用该选择。
182
+ (请将 `/opt/dsh` 替换为实际 DSH 安装目录。)
183
+
184
+ 若因前次维护异常中断留下锁目录 `DSH_HOME/dsh-model-groups/<安装目录指纹>/patch.lock`,请先确认没有任何正在运行的补丁维护进程后再人工清理,切勿并发重试。
185
+
186
+ ## 开发与许可证
113
187
 
114
- ## 开发
188
+ 常用开发与检查命令:
115
189
 
116
190
  ```bash
117
191
  npm run build
@@ -119,8 +193,16 @@ npm test
119
193
  npm run check:build
120
194
  ```
121
195
 
122
- 设置 `DSH_ROOT` 可启用依赖对应 DSH 程序文件的回归测试。源码包含层级、选择器交互、面板布局及补丁安装与恢复测试。
196
+ 设置 `DSH_ROOT` 可启用依赖对应 DSH 程序文件的只读回归测试;可用 `PI_AI_SOURCES` 传入其他已知原始/旧补丁程序的路径(按系统路径分隔符分开),覆盖三种构建。第三种另有从真实完整文件核验的最小 delta 夹具,重建后仍检查完整 SHA-256;不包含用户配置或凭据。真实安装/更新/还原集成测试仅在临时副本中写入,配置和凭据使用合成哨兵。源码包含层级、选择器交互、协议探测、删除确认、面板布局、补丁安装与恢复,以及真实 pi-ai 登录/令牌刷新取消行为的离线测试。
197
+
198
+ 宿主设置页属于 `@deepseek-ai/dsh-client-ui-settings-models`,不是插件自己的 `lib/client.js`。`npm run build` 仅构建插件,不能替代生成宿主补丁。修改生成器后用明确的未打补丁宿主源码生成/校验(源文件只读,哈希必须匹配):
199
+
200
+ ```bash
201
+ node scripts/settings-models-collapse.mjs --source /path/to/pristine-client.js
202
+ node scripts/settings-models-collapse.mjs --source /path/to/pristine-client.js --check
203
+ DSH_ROOT=/opt/dsh npm test
204
+ ```
123
205
 
124
- ## 许可证
206
+ 生成器不会隐式读取或修改已安装宿主。分发包包含预先验证的 `native/*.patch.json`,维护页显式确认后才应用。测试夹具与开发生成器不参与运行时补丁应用。
125
207
 
126
- [MIT](LICENSE)。包含改编自 DeepSeek Harness 的代码,保留原版权声明。
208
+ 本项目基于 [MIT 许可证](LICENSE) 开源。部分模型选择器代码改编自 DeepSeek Harness,保留原始版权声明。
package/lib/client.js CHANGED
@@ -1258,6 +1258,292 @@ function installModelGroupsStyles() {
1258
1258
  document.head.appendChild(style);
1259
1259
  }
1260
1260
 
1261
+
1262
+ const SETTINGS_PAGE_CSS = ".mg-maintenance{box-sizing:border-box;display:flex;flex-direction:column;gap:12px;max-width:720px;min-width:0;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:1.5}\n.mg-maintenance h2{margin:0;font-size:18px;font-weight:600;line-height:26px}\n.mg-maintenance h3{margin:0;font-size:14px;font-weight:600;line-height:22px}\n.mg-maintenance p{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}\n.mg-maintenance-card{display:flex;flex-direction:column;gap:12px;min-width:0;border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl,12px);background:var(--dsw-alias-settings-card-fill);padding:16px}\n.mg-maintenance-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}\n.mg-maintenance ul{padding-left:20px;margin:0;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.mg-maintenance-actions{display:flex;flex-wrap:wrap;gap:10px;align-items:center}\n.mg-maintenance button{align-self:flex-start;font:inherit;font-weight:500;line-height:20px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-tsp-secondary);border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-sm,6px);padding:8px 14px;min-height:40px;cursor:pointer;white-space:normal}\n.mg-maintenance .mg-maintenance-primary:not(:disabled){border-color:var(--dsw-alias-state-business-primary);font-weight:600}\n.mg-maintenance .mg-maintenance-secondary{font-size:12px}\n@media(hover:hover){.mg-maintenance button:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}}\n.mg-maintenance button:disabled{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-tsp-secondary);cursor:not-allowed;opacity:1}\n.mg-maintenance .mg-maintenance-danger:not(:disabled),.mg-maintenance .mg-maintenance-error{color:var(--dsw-alias-state-error-primary)}\n.mg-maintenance .mg-maintenance-help{color:var(--dsw-alias-label-secondary)}\n.mg-maintenance-dialog{display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-state-business-primary);border-radius:var(--dsw-radius-sm,6px);padding:16px}\n.mg-maintenance-warning{padding:10px;border-left:3px solid var(--dsw-alias-state-business-primary)}\n.mg-maintenance summary{cursor:pointer;font-weight:500;min-height:28px}\n.mg-maintenance details[open]>*+*{margin-top:12px}\n.mg-maintenance :focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}\n@media(max-width:480px){.mg-maintenance-card,.mg-maintenance-dialog{padding:12px}.mg-maintenance button{max-width:100%}}\n";
1263
+ function installSettingsPageStyles() {
1264
+ const tagId = "dsh-model-groups/settings-page.css";
1265
+ if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;
1266
+ const style = document.createElement('style');
1267
+ style.dataset.plugin = 'dsh-model-groups';
1268
+ style.dataset.pluginCss = tagId;
1269
+ style.textContent = SETTINGS_PAGE_CSS;
1270
+ document.head.appendChild(style);
1271
+ }
1272
+
1273
+ const MODEL_GROUPS_CLIENT_VERSION = "0.3.6";
1274
+ // Public program metadata only. Never include paths, source text, configuration,
1275
+ // credentials, environment variables, or arbitrary exception messages.
1276
+ const PATCH_IDS = ['pi-ai.patch.json', 'settings-models.patch.json', 'llm-typert.host.patch.json', 'llm-typert.remote-client.patch.json', 'api-remotes.patch.json'];
1277
+ const COMPATIBILITY_CODES = ['ready', 'update_available', 'not_applied', 'unsupported_patch_build', 'host_version', 'package_version', 'unrecognized_content', 'target_unavailable', 'patch_manifest'];
1278
+ const publicVersion = value => typeof value === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,40})?$/.test(value) ? value : null;
1279
+ const publicHash = value => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value) ? value : null;
1280
+ function compatibilitySnapshot(value) {
1281
+ if (!value || value.schema !== 1 || !Array.isArray(value.files) || value.files.length > PATCH_IDS.length) return null;
1282
+ return { schema: 1, pluginVersion: publicVersion(value.pluginVersion), hostVersion: publicVersion(value.hostVersion), supportedDsh: publicVersion(value.supportedDsh),
1283
+ files: value.files.filter(item => item && PATCH_IDS.includes(item.id)).map(item => ({ id: item.id,
1284
+ state: ['patched', 'pristine', 'conflict'].includes(item.state) ? item.state : 'conflict',
1285
+ code: COMPATIBILITY_CODES.includes(item.code) ? item.code : 'patch_manifest',
1286
+ installedVersion: publicVersion(item.installedVersion), observedSha256: publicHash(item.observedSha256),
1287
+ baseSha256: publicHash(item.baseSha256), targetSha256: publicHash(item.targetSha256) })) };
1288
+ }
1289
+ function compatibilityGuidance(snapshot) {
1290
+ const codes = snapshot?.files.map(item => item.code) ?? [];
1291
+ if (codes.includes('host_version') || codes.includes('package_version')) return '宿主程序版本不在本版支持范围。先核对插件与 DSH 的兼容版本;不要强行降级、覆盖文件或删除配置。';
1292
+ if (codes.includes('unrecognized_content') || codes.includes('target_unavailable') || codes.includes('patch_manifest') || codes.includes('unsupported_patch_build')) return '已停止整批更新,未覆盖未知文件。先在原 profile 更新最新版插件,重启原引擎并刷新网页后复查;仍不支持时,复制脱敏诊断反馈。不要先卸载、还原补丁或删除配置;如需进一步适配,只提供被标记的程序文件,不提供密钥、配置或聊天记录。';
1293
+ if (codes.includes('update_available')) return '已识别历史补丁,可直接更新到最新版,不需要逐版安装或先关闭多密钥。请保存草稿、停止任务,确认更新后重启原引擎并刷新网页。';
1294
+ return '';
1295
+ }
1296
+
1297
+ const MAINTENANCE_CLIENT_PROTOCOL = 2;
1298
+ const MAINTENANCE_CONFIRMATION = 'CONFIRM PATCH OPERATION';
1299
+ const MAINTENANCE_ENDPOINT = '/dsh-model-groups/maintenance';
1300
+ const MAINTENANCE_KEYS = ['dsh:model-groups:v1', 'dsh:model-groups:mode'];
1301
+ const ACTION_CAPABILITIES = { apply: 'canApply', 'restore-only': 'canRestoreOnly', 'restore-and-uninstall': 'canRestore' };
1302
+
1303
+ // Never project arbitrary server messages, patch paths, or tokens into the DOM.
1304
+ function maintenanceSnapshot(value) {
1305
+ const phases = ['idle', 'applied', 'restored', 'removing', 'removed', 'failed'];
1306
+ if (!value || !phases.includes(value.phase) || typeof value.restartRequired !== 'boolean'
1307
+ || !Object.values(ACTION_CAPABILITIES).every(key => typeof value[key] === 'boolean')
1308
+ || !(value.token == null || (typeof value.token === 'string' && value.token.length > 0))
1309
+ || (Object.values(ACTION_CAPABILITIES).some(key => value[key]) && !(typeof value.token === 'string' && value.token.length > 0))
1310
+ || !Array.isArray(value.patches) || !Array.isArray(value.blockers)
1311
+ || !(value.warnings == null || (Array.isArray(value.warnings) && value.warnings.every(warning => warning && typeof warning.code === 'string'
1312
+ && (warning.count == null || (Number.isSafeInteger(warning.count) && warning.count > 0)))))
1313
+ || !value.patches.every(patch => patch && ['patched', 'pristine', 'conflict'].includes(patch.state)
1314
+ && (patch.updateAvailable == null || typeof patch.updateAvailable === 'boolean')
1315
+ && (patch.applySupported == null || typeof patch.applySupported === 'boolean'))
1316
+ || !value.blockers.every(blocker => blocker && typeof blocker.code === 'string' && typeof blocker.message === 'string'
1317
+ && (blocker.count == null || (Number.isSafeInteger(blocker.count) && blocker.count > 0)))) {
1318
+ throw new Error('Invalid maintenance snapshot');
1319
+ }
1320
+ return { phase: value.phase, maintenanceProtocol: value.maintenanceProtocol === MAINTENANCE_CLIENT_PROTOCOL ? MAINTENANCE_CLIENT_PROTOCOL : null,
1321
+ pluginVersion: publicVersion(value.pluginVersion), compatibility: compatibilitySnapshot(value.compatibility), restartRequired: value.restartRequired, canApply: value.canApply,
1322
+ canRestoreOnly: value.canRestoreOnly, canRestore: value.canRestore,
1323
+ token: value.token ?? null, patches: value.patches.map(patch => ({ state: patch.state, updateAvailable: patch.updateAvailable === true, applySupported: patch.applySupported !== false })),
1324
+ blockers: value.blockers.map(blocker => ({ code: blocker.code, count: blocker.count ?? null })),
1325
+ warnings: (value.warnings ?? []).map(warning => ({ code: warning.code, count: warning.count ?? null })) };
1326
+ }
1327
+ function maintenanceCanAction(snapshot, action) {
1328
+ const capability = Object.hasOwn(ACTION_CAPABILITIES, action) ? ACTION_CAPABILITIES[action] : null;
1329
+ return !!capability && !!snapshot && ['idle', 'applied', 'restored', 'failed'].includes(snapshot.phase) && snapshot[capability]
1330
+ && typeof snapshot.token === 'string' && snapshot.token.length > 0
1331
+ && !snapshot.patches.some(patch => patch.state === 'conflict')
1332
+ && (action !== 'apply' || !snapshot.patches.some(patch => patch.applySupported === false));
1333
+ }
1334
+ function maintenanceCanRestore(snapshot) { return maintenanceCanAction(snapshot, 'restore-and-uninstall'); }
1335
+ function maintenanceBlockerText(blocker) {
1336
+ const labels = {
1337
+ active_sessions: '存在正在运行的会话,请先停止运行中的任务。',
1338
+ active_turns: '存在正在运行的回合,请先等待结束或停止。',
1339
+ patch_conflict: '补丁状态冲突,不会强制覆盖。请复制脱敏诊断;不要先卸载、还原或删除配置。',
1340
+ client_version_mismatch: '网页与插件后端版本不一致,已禁止维护。请重启原引擎并刷新网页后重新检查。',
1341
+ unsupported_patch_build: '此宿主构建尚无经过验证的新版协议补丁,已阻止不完整更新;仍可关闭或卸载,不会改动密钥。',
1342
+ backup_missing: '缺少可验证的原版备份,无法安全恢复。',
1343
+ restart_required: '需要重启原引擎后重新检查。',
1344
+ inspection_failed: '状态检查失败,请使用安装诊断排查。',
1345
+ config_inspection_failed: '配置检查失败,无法验证模型与密钥依赖;请使用安装诊断排查。',
1346
+ session_inspection_failed: '会话检查失败,无法验证活动回合与模型引用;请使用安装诊断排查。',
1347
+ session_service_missing: '缺少会话查询或持久化服务,无法验证历史会话的模型依赖;请使用安装诊断排查。',
1348
+ process_inspection_failed: '进程检查失败,无法排除其他引擎使用补丁;请使用安装诊断排查。',
1349
+ profile_inspection_failed: '配置档检查失败,无法排除跨配置档依赖;请使用安装诊断排查。',
1350
+ patch_inspection_failed: '补丁文件检查失败,无法验证磁盘状态;不会修改未知文件。',
1351
+ not_removable: '当前插件状态不允许安全卸载。',
1352
+ not_applicable: '当前状态不允许安全应用补丁。',
1353
+ manager_unavailable: '插件管理服务不可用,请稍后重试。',
1354
+ host_unknown: '无法验证宿主版本,不会修改未知文件。',
1355
+ state_changed: '状态已发生变化,请刷新检查后重新打开操作并确认。',
1356
+ apply_failed: '补丁应用未完成,请刷新检查,不要视为成功。',
1357
+ restore_failed: '原版恢复未完成,请刷新检查,不要视为成功。',
1358
+ remove_failed: '卸载未完成,请刷新检查并使用安装诊断排查。',
1359
+ remove_unverified: '无法验证卸载结果,请刷新检查,不要视为成功。',
1360
+ verify_failed: '安全验证失败,不会强制覆盖文件。',
1361
+ busy: '维护或其他任务正在运行,请结束后刷新。',
1362
+ confirmation_required: '安全确认已失效,请刷新检查后重新打开操作并确认。',
1363
+ maintenance_failed: '维护失败,请刷新检查并使用安装诊断排查。',
1364
+ inspection_unknown: '无法确认运行状态,请先停止任务并使用安装诊断排查。',
1365
+ profile_unknown: '无法确认当前配置档,不会执行维护。',
1366
+ cross_profile_bundle: '其他配置档也在使用此插件,请先处理跨配置档依赖。',
1367
+ alias_dependency: '仍有会话或配置引用多密钥模型,请先切换为普通模型再检查;本预览不会自动转换。',
1368
+ key_config_dependency: '仍有多密钥配置依赖:请先禁用 multiKey 并清理 keyEntries 配置;已保存的密钥会保留。本预览不会自动转换。',
1369
+ expression_unknown: '存在无法安全判断的配置表达式,请先人工检查配置。',
1370
+ other_process: '其他引擎进程可能正在使用补丁,请先停止相关进程。',
1371
+ };
1372
+ return (labels[blocker.code.toLowerCase()] || '安全检查未通过,请先处理运行中的任务或使用安装诊断排查。')
1373
+ + (blocker.count ? `(${blocker.count} 项)` : '');
1374
+ }
1375
+
1376
+ // Copy only allowlisted codes and enumerated state: unknown codes may contain secrets or paths.
1377
+ function maintenanceDiagnostic(state) {
1378
+ const snapshot = state.snapshot;
1379
+ const knownCodes = new Set(['active_sessions', 'active_turns', 'patch_conflict', 'client_version_mismatch', 'unsupported_patch_build', 'backup_missing', 'restart_required',
1380
+ 'inspection_failed', 'config_inspection_failed', 'session_inspection_failed', 'session_service_missing',
1381
+ 'process_inspection_failed', 'profile_inspection_failed', 'patch_inspection_failed', 'default_inspection_failed',
1382
+ 'agent_inspection_failed', 'activity_unverified', 'not_removable', 'not_applicable', 'manager_unavailable',
1383
+ 'host_unknown', 'state_changed', 'apply_failed', 'restore_failed', 'remove_failed', 'remove_unverified',
1384
+ 'verify_failed', 'busy', 'confirmation_required', 'maintenance_failed', 'inspection_unknown', 'profile_unknown',
1385
+ 'cross_profile_bundle', 'alias_dependency', 'key_config_dependency', 'expression_unknown', 'other_process']);
1386
+ const diagnosticItem = item => {
1387
+ const code = item.code.toLowerCase();
1388
+ return { code: knownCodes.has(code) ? code : 'unknown', count: item.count };
1389
+ };
1390
+ return JSON.stringify({
1391
+ status: snapshot?.phase ?? 'unknown', busy: !!state.busy, resultUnverified: !!state.error,
1392
+ restartRequired: snapshot?.restartRequired ?? null,
1393
+ maintenanceProtocol: snapshot?.maintenanceProtocol ?? null, pluginVersion: publicVersion(snapshot?.pluginVersion),
1394
+ compatibility: compatibilitySnapshot(snapshot?.compatibility),
1395
+ capabilities: snapshot ? { apply: snapshot.canApply, close: snapshot.canRestoreOnly, uninstall: snapshot.canRestore } : null,
1396
+ patches: snapshot?.patches.map(patch => patch.state) ?? [],
1397
+ blockers: snapshot?.blockers.map(diagnosticItem) ?? [],
1398
+ warnings: snapshot?.warnings.map(diagnosticItem) ?? [],
1399
+ }, null, 2);
1400
+ }
1401
+ function maintenanceWarningText(warning) {
1402
+ const labels = {
1403
+ alias_dependency: '旧多密钥模型选择可能影响后续使用或重启;本页不会自动切换服务或模型。',
1404
+ key_config_dependency: '旧多密钥配置可能影响后续使用或重启;关闭会保留密钥和历史,不自动转换配置。',
1405
+ expression_unknown: '部分配置表达式无法完整检查,请留意关闭后的使用和重启情况。',
1406
+ };
1407
+ const code = warning.code.toLowerCase();
1408
+ const inspection = ['config_inspection_failed', 'session_inspection_failed', 'session_service_missing', 'process_inspection_failed', 'profile_inspection_failed', 'inspection_unknown', 'default_inspection_failed', 'agent_inspection_failed', 'activity_unverified'];
1409
+ return (labels[code] || (inspection.includes(code) ? '检查不完整,无法确认全部依赖;这只是提醒,不会额外禁用操作。' : '部分使用情况未能验证,不等于安全;这只是提醒,不会额外禁用操作。'))
1410
+ + (warning.count ? `(${warning.count} 项)` : '');
1411
+ }
1412
+
1413
+ function createMaintenanceController({ request = (...args) => fetch(...args), storage = () => globalThis.localStorage,
1414
+ clientVersion = typeof MODEL_GROUPS_CLIENT_VERSION === 'string' ? MODEL_GROUPS_CLIENT_VERSION : null } = {}) {
1415
+ let state = { snapshot: null, busy: false, error: '', cleanupWarning: false, patchedWarning: false };
1416
+ let disposed = false;
1417
+ let generation = 0;
1418
+ const listeners = new Set();
1419
+ const update = next => { if (disposed) return; state = { ...state, ...next }; for (const listener of listeners) listener(); };
1420
+ async function readSnapshot(options) {
1421
+ const response = await request(MAINTENANCE_ENDPOINT, { credentials: 'same-origin', cache: 'no-store', ...options });
1422
+ if (!response.ok) throw new Error('Maintenance HTTP error');
1423
+ const envelope = await response.json();
1424
+ if (envelope?.ok !== true) throw new Error('Maintenance refused');
1425
+ const snapshot = maintenanceSnapshot(envelope.value);
1426
+ if (clientVersion !== null && (snapshot.maintenanceProtocol !== MAINTENANCE_CLIENT_PROTOCOL || snapshot.pluginVersion !== clientVersion)) {
1427
+ return { ...snapshot, versionMismatch: true, canApply: false, canRestoreOnly: false, canRestore: false, token: null,
1428
+ blockers: [...snapshot.blockers, { code: 'CLIENT_VERSION_MISMATCH', count: null }] };
1429
+ }
1430
+ return snapshot;
1431
+ }
1432
+ function accept(snapshot) {
1433
+ let cleanupWarning = false;
1434
+ if (snapshot.phase === 'removed' && !snapshot.versionMismatch) {
1435
+ for (const key of MAINTENANCE_KEYS) {
1436
+ try { storage()?.removeItem(key); } catch { cleanupWarning = true; }
1437
+ }
1438
+ }
1439
+ update({ snapshot, cleanupWarning, patchedWarning: snapshot.patches.some(patch => patch.state === 'patched'),
1440
+ error: snapshot.versionMismatch ? '网页与插件后端版本不一致,已停止维护。请重启原引擎并刷新网页后复查。' : snapshot.phase === 'failed' ? '维护操作失败,未确认完成。请刷新检查,不要强制覆盖。' : '' });
1441
+ }
1442
+ async function run(action = null) {
1443
+ if (disposed || state.busy || (action && !maintenanceCanAction(state.snapshot, action))) return;
1444
+ const sequence = ++generation;
1445
+ const token = state.snapshot?.token;
1446
+ const current = () => !disposed && sequence === generation;
1447
+ update({ busy: true, error: '' });
1448
+ try {
1449
+ // Recheck immediately before using the confirmed token; a changed state needs new consent.
1450
+ let snapshot = await readSnapshot({ method: 'GET' });
1451
+ if (!current()) return;
1452
+ if (action) {
1453
+ if (snapshot.token !== token || !maintenanceCanAction(snapshot, action)) {
1454
+ accept(snapshot);
1455
+ update({ error: '状态已发生变化或操作被阻止,未执行操作。请检查当前状态并重新打开操作并确认。' });
1456
+ return;
1457
+ }
1458
+ update({ snapshot: { ...snapshot, canApply: false, canRestoreOnly: false, canRestore: false, token: null } });
1459
+ snapshot = await readSnapshot({ method: 'POST', headers: { 'Content-Type': 'application/json' },
1460
+ body: JSON.stringify({ action, token, confirmation: MAINTENANCE_CONFIRMATION, ...(clientVersion === null ? {} : { maintenanceProtocol: MAINTENANCE_CLIENT_PROTOCOL, clientVersion }) }) });
1461
+ if (!current()) return;
1462
+ }
1463
+ accept(snapshot);
1464
+ } catch {
1465
+ if (current()) update({ snapshot: null,
1466
+ error: action ? '未能确认维护结果:连接中断、请求被拒绝或响应无效不代表成功。请刷新检查。' : '状态检查失败,请确认连接后刷新重试。' });
1467
+ } finally { if (current()) update({ busy: false }); }
1468
+ }
1469
+ const operate = (action, checked) => checked === true && Object.hasOwn(ACTION_CAPABILITIES, action) ? run(action) : Promise.resolve();
1470
+ return {
1471
+ getSnapshot: () => state,
1472
+ subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); },
1473
+ activate: () => { disposed = false; generation++; update({ busy: false }); },
1474
+ refresh: () => run(),
1475
+ operate,
1476
+ apply: checked => operate('apply', checked),
1477
+ restoreOnly: checked => operate('restore-only', checked),
1478
+ restore: checked => operate('restore-and-uninstall', checked),
1479
+ dispose: () => { disposed = true; generation++; listeners.clear(); },
1480
+ };
1481
+ }
1482
+
1483
+ function ModelGroupsSettingsPage() {
1484
+ const h = react.createElement;
1485
+ const controller = react.useMemo(() => createMaintenanceController(), []);
1486
+ const state = react.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
1487
+ const [pending, setPending] = react.useState(null);
1488
+ const [copyStatus, setCopyStatus] = react.useState('');
1489
+ const confirmationId = react.useId();
1490
+ react.useEffect(() => { controller.activate(); controller.refresh(); return () => controller.dispose(); }, [controller]);
1491
+ const snapshot = state.snapshot;
1492
+ const phaseLabels = { idle: '检查完成,可选择操作', applied: '多密钥已应用(磁盘状态)', restored: '多密钥已关闭,插件仍保留(磁盘状态)', removing: '正在卸载,尚未确认完成', removed: '已确认关闭多密钥并卸载插件', failed: '维护失败,未确认完成' };
1493
+ const updatesAvailable = snapshot?.patches.some(patch => patch.updateAvailable) ?? false;
1494
+ const statusLabel = snapshot?.versionMismatch ? '网页与维护后端版本不一致,请先重启并刷新' : snapshot?.phase === 'idle'
1495
+ ? snapshot.patches.some(patch => patch.state === 'conflict') ? '磁盘文件校验冲突,已停止整批维护'
1496
+ : snapshot.patches.some(patch => patch.applySupported === false) ? '旧补丁已识别,但此构建不支持新版协议补丁'
1497
+ : updatesAvailable ? '旧多密钥补丁已应用,新版更新待确认'
1498
+ : snapshot.patches.length && snapshot.patches.every(patch => patch.state === 'patched') ? '多密钥已启用(磁盘状态)'
1499
+ : snapshot.patches.length && snapshot.patches.every(patch => patch.state === 'pristine') ? '多密钥已关闭(磁盘状态)' : '磁盘状态待处理,请查看操作限制'
1500
+ : phaseLabels[snapshot?.phase];
1501
+ const patchLabels = { patched: '已应用', pristine: '原版', conflict: '冲突(不会强制覆盖)' };
1502
+ const actions = [['apply', updatesAvailable ? '更新多密钥补丁' : '启用多密钥'], ['restore-only', '关闭多密钥'], ['restore-and-uninstall', '关闭并卸载插件']];
1503
+ const anyAllowed = actions.some(([action]) => maintenanceCanAction(snapshot, action));
1504
+ const refresh = () => { setPending(null); controller.refresh(); };
1505
+ const confirm = () => { const action = pending; setPending(null); if (action) controller.operate(action, true); };
1506
+ const copy = async () => {
1507
+ try { await globalThis.navigator.clipboard.writeText(maintenanceDiagnostic(state)); setCopyStatus('已复制脱敏诊断'); }
1508
+ catch { setCopyStatus('无法复制,请检查浏览器剪贴板权限。'); }
1509
+ };
1510
+ return h('section', { className: 'mg-maintenance', 'aria-label': '模型分组设置', 'aria-busy': state.busy },
1511
+ h('h2', null, '模型分组 · 多密钥管理'),
1512
+ h('p', null, '选择操作,再确认一次即可。不会调用 AI,不会显示或删除 API 密钥。'),
1513
+ snapshot?.pluginVersion && h('p', { className: 'mg-maintenance-help' }, `插件后端 ${snapshot.pluginVersion} · 宿主 ${snapshot.compatibility?.hostVersion ?? '版本未确认'}`),
1514
+ h('section', { className: 'mg-maintenance-card', 'aria-label': '当前状态' },
1515
+ h('div', { className: 'mg-maintenance-row' }, h('h3', null, '当前状态'), h('button', { type: 'button', disabled: state.busy, onClick: refresh }, state.busy ? '处理中…' : '刷新检查')),
1516
+ h('p', { role: 'status', 'aria-live': 'polite', className: 'mg-maintenance-help' }, state.busy ? '正在检查或执行维护,请勿关闭或重复操作。' : snapshot ? statusLabel : '尚无可验证状态,请刷新检查。'),
1517
+ state.error && h('p', { role: 'alert', className: 'mg-maintenance-error' }, state.error),
1518
+ state.patchedWarning && h('p', { role: 'alert', className: 'mg-maintenance-error' }, '警告:从普通插件列表卸载只移除插件,仍会留下宿主补丁,可能造成升级、文件冲突及旧多密钥模型依赖问题。请使用这里的“关闭并卸载插件”。'),
1519
+ snapshot?.restartRequired && h('p', { role: 'status' }, '本进程已实际修改宿主文件,需要重启原引擎并刷新;页面刷新或插件重载不等于进程重启。'),
1520
+ compatibilityGuidance(snapshot?.compatibility) && h('p', { role: 'status', className: 'mg-maintenance-help' }, compatibilityGuidance(snapshot.compatibility)),
1521
+ updatesAvailable && h('p', { role: 'status' }, '多密钥补丁有更新可用;确认后将备份、校验并更新已识别的旧补丁。'),
1522
+ snapshot?.phase === 'removed' && !snapshot.versionMismatch && h('p', { role: 'status' }, '卸载已确认:仅清理本插件的两个浏览器显示偏好,密钥、历史和备份均保留。'),
1523
+ snapshot?.warnings.length > 0 && h('p', { className: 'mg-maintenance-warning', role: 'status' }, '部分使用情况未验证,关闭前请阅读提示;检查提醒不会额外禁用操作。'),
1524
+ snapshot?.blockers.length > 0 && h('div', { role: 'alert' }, h('strong', null, '操作限制(各按钮以安全权限为准)'), h('ul', null, ...snapshot.blockers.map((blocker, index) => h('li', { key: index }, maintenanceBlockerText(blocker))))),
1525
+ state.cleanupWarning && h('p', { role: 'status' }, '卸载已确认,但浏览器阻止清理本插件的显示偏好;其他数据未受影响。')),
1526
+ h('section', { className: 'mg-maintenance-card', 'aria-label': '多密钥操作' },
1527
+ h('h3', null, '多密钥操作'),
1528
+ h('div', { className: 'mg-maintenance-actions' }, ...actions.map(([action, label]) => h('button', { key: action, type: 'button', className: action === 'restore-and-uninstall' ? 'mg-maintenance-secondary mg-maintenance-danger' : 'mg-maintenance-primary', disabled: state.busy || !!pending || !maintenanceCanAction(snapshot, action), onClick: () => setPending(action) }, label))),
1529
+ !anyAllowed && h('p', null, '请刷新检查或处理上方操作限制。检查提醒本身不会禁用按钮。'),
1530
+ pending && h('div', { role: 'dialog', 'aria-labelledby': confirmationId, 'aria-describedby': `${confirmationId}-help`, className: 'mg-maintenance-dialog' },
1531
+ h('h3', { id: confirmationId }, `确认${actions.find(([action]) => action === pending)[1]}?`),
1532
+ h('p', { id: `${confirmationId}-help` }, pending === 'apply' ? '将备份、校验并修改宿主文件以启用多密钥。不会自动换服务或模型。请保存草稿、停止任务,完成后重启原引擎。' : '将校验并恢复宿主原版,关闭多密钥。密钥、配置和历史保留,不自动换服务或模型;旧多密钥选择和配置可能影响后续使用或重启。请保存草稿、停止任务,完成后重启原引擎。'),
1533
+ h('p', null, '请先停止所有任务。部分使用情况未能检查不等于安全;检查提醒不会替代您的判断。'),
1534
+ pending === 'restore-and-uninstall' && h('p', null, '确认原版恢复成功后才卸载插件;只有确认卸载完成才清理本插件的两个浏览器显示偏好。连接中断不代表成功。'),
1535
+ h('div', { className: 'mg-maintenance-actions' }, h('button', { type: 'button', onClick: () => setPending(null) }, '取消'), h('button', { type: 'button', disabled: state.busy || !maintenanceCanAction(snapshot, pending), onClick: confirm }, '确认执行')))),
1536
+ h('details', { className: 'mg-maintenance-card' },
1537
+ h('summary', null, '检查详情与脱敏诊断'),
1538
+ h('p', null, '这里只验证磁盘和安全权限,运行时是否已切换未知。每次确认后会重新检查;状态变化需重新确认。未知文件不会强制覆盖,检查不完整仅提醒;操作仍以各自安全权限为准。'),
1539
+ snapshot?.warnings.length > 0 && h('ul', null, ...snapshot.warnings.map((warning, index) => h('li', { key: index }, maintenanceWarningText(warning)))),
1540
+ snapshot && h('ul', null, ...snapshot.patches.map((patch, index) => h('li', { key: index }, `宿主文件 ${index + 1}:${patchLabels[patch.state]}${patch.applySupported === false ? '(此构建不支持新版补丁)' : patch.updateAvailable ? '(更新可用)' : ''}`))),
1541
+ snapshot?.compatibility && h('ul', null, ...snapshot.compatibility.files.map(item => h('li', { key: item.id }, `${item.id} · ${item.installedVersion ?? '版本未知'} · ${item.code} · SHA-256 ${item.observedSha256 ?? '不可读取'}`))),
1542
+ h('p', null, '诊断仅含程序版本、固定文件标识与程序 SHA-256,不含文件路径、配置正文、密钥或聊天。升级失败请先保留现场,不要用另一构建的文件覆盖。'),
1543
+ h('button', { type: 'button', onClick: copy }, '复制脱敏诊断'),
1544
+ h('p', { role: 'status' }, copyStatus)));
1545
+ }
1546
+
1261
1547
  const groupPreferences = createGroupPreferences();
1262
1548
  const EMPTY_PROVIDERS_SNAPSHOT = Object.freeze({ status: 'unavailable', value: {} });
1263
1549
 
@@ -1265,6 +1551,10 @@ const inject = ['slots', 'modelDirectories', 'sessions', 'locale', 'remote', 're
1265
1551
  function apply(ctx) {
1266
1552
  installModelSelectStyles();
1267
1553
  installModelGroupsStyles();
1554
+ installSettingsPageStyles();
1555
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1556
+ name: 'settings.section', id: 'dsh-model-groups', order: 35, label: '模型分组',
1557
+ }, ModelGroupsSettingsPage));
1268
1558
  const providersForm = ctx.configForms?.get('llm-pi-ai');
1269
1559
  let statusDisposed = false;
1270
1560
  let statusNotice;
@@ -1274,11 +1564,11 @@ function apply(ctx) {
1274
1564
  if (statusDisposed || (!status.restartRequired && !status.failed)) return;
1275
1565
  statusNotice = document.createElement('div');
1276
1566
  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;';
1567
+ statusNotice.setAttribute('role', 'status');
1568
+ statusNotice.style.cssText = 'position:fixed;bottom:16px;right:16px;max-width:min(360px,80vw);z-index:1000;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#222);padding:12px;border:1px solid currentColor;border-radius:8px;box-sizing:border-box;pointer-events:none;';
1279
1569
  statusNotice.textContent = status.failed
1280
- ? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。当前网页暂不可用于编辑模型配置。'
1281
- : '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。当前网页暂停操作,避免新旧模块混用。';
1570
+ ? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。请暂勿编辑模型配置;可到「设置 → 模型分组」检查并安全恢复。'
1571
+ : '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。请暂勿编辑模型配置,避免新旧模块混用;维护入口仍可使用。';
1282
1572
  document.body.appendChild(statusNotice);
1283
1573
  }).catch(() => {});
1284
1574
  ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });