dsh-model-groups 0.3.5 → 0.3.7
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 +48 -84
- package/docs/images/add-key-configuration.webp +0 -0
- package/docs/images/collapsed-key-configurations.webp +0 -0
- package/docs/images/key-configuration.webp +0 -0
- package/docs/images/multi-key-management.webp +0 -0
- package/docs/images/provider-multi-key-toggle.webp +0 -0
- package/docs/maintenance.md +47 -0
- package/lib/client.js +75 -27
- package/native/pi-ai.patch.json +507 -4
- package/native/settings-models.patch.json +226 -7
- package/package.json +5 -2
- package/scripts/build.mjs +2 -1
- package/scripts/doctor.mjs +22 -23
- package/scripts/native-patches.mjs +65 -5
- package/src/auto-patch.js +5 -3
- package/src/client.js +2 -2
- package/src/compatibility.js +22 -0
- package/src/index.js +2 -6
- package/src/maintenance-state.js +28 -13
- package/src/maintenance.js +35 -11
- package/src/settings-page.js +23 -12
package/README.md
CHANGED
|
@@ -1,125 +1,89 @@
|
|
|
1
|
-
# DSH Model Groups
|
|
1
|
+
# DSH Model Groups · 模型分组
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**在 DeepSeek Harness(DSH)网页端,按供应商、密钥配置和模型逐级选择。** 如果同一家供应商有多把 API 密钥,可以为每把密钥分别设置模型列表和 API 协议。即使模型名称一样,也能选中你要用的那份配置。插件不会自动换密钥或切换供应商。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- 按“供应商 → 密钥配置 → 模型”浏览和选择。
|
|
8
|
-
- 支持原地折叠、侧边面板、名称搜索、全部展开与收起。
|
|
9
|
-
- 记住各级折叠状态;搜索时临时展开匹配项。
|
|
10
|
-
- 不同密钥配置可使用相同模型 ID,选择时使用对应配置的密钥。
|
|
11
|
-
- 手动选择配置,不自动轮换密钥或切换到其他配置。
|
|
12
|
-
- 模型设置中已保存的密钥项默认折叠,名称和原密钥栏仍可见;展开编辑模型,新建和错误项保持展开,折叠不丢失草稿。
|
|
13
|
-
- 实际修改补丁才提示重启;普通检查不要求重启,关闭状态标记不再被误当成待重启。
|
|
14
|
-
|
|
15
|
-
## 环境要求
|
|
16
|
-
|
|
17
|
-
- DeepSeek Harness **0.2.2-rc.1**。
|
|
18
|
-
- Node.js **22 或以上**。
|
|
19
|
-
- Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
|
|
20
|
-
- DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
|
|
21
|
-
|
|
22
|
-
**本版本不会自动打补丁。** 请在网页设置 → 模型分组明确选择安装。安装会校验、备份并修改五个宿主文件,之后须重启引擎。普通卸载插件不会撤销宿主修改。未知版本/文件不强制覆盖。
|
|
5
|
+
支持 **DSH `0.2.2-rc.1`**、**Node.js 22 或更新版本**。本插件适用于 DSH 网页端,不替换原生 App 的模型菜单。
|
|
23
6
|
|
|
24
7
|
## 安装
|
|
25
8
|
|
|
26
|
-
|
|
9
|
+
在运行 DSH 的环境里执行(如果你使用的 DSH 配置不是 `web`,把命令中的 `web` 换成你的配置名):
|
|
27
10
|
|
|
28
11
|
```bash
|
|
29
|
-
dsh plugin --profile web add
|
|
12
|
+
dsh plugin --profile web add dsh-model-groups@latest --registry=https://registry.npmjs.org/
|
|
30
13
|
```
|
|
31
14
|
|
|
32
|
-
|
|
15
|
+
如果 npm 尚未同步到你需要的版本,可以把 `dsh-model-groups@latest` 换成 [GitHub 0.3.7 正式版安装包](https://github.com/betsunninda9-lgtm/dsh-model-groups/releases/download/v0.3.7/dsh-model-groups-0.3.7.tgz) 的下载地址。安装后,**重启正在使用的 DSH 引擎,再刷新网页**。只想给现有模型分组?装好就可以用,不需要启用多密钥。
|
|
33
16
|
|
|
34
|
-
##
|
|
17
|
+
## 在对话中选模型
|
|
35
18
|
|
|
36
|
-
|
|
37
|
-
2. 填写供应商公共的 Provider ID、API 地址和协议。
|
|
38
|
-
3. 开启 **多密钥配置**,为每份配置填写名称、密钥和模型列表,点击“应用”。
|
|
39
|
-
4. 在会话模型菜单中展开供应商和配置,选择需要的模型。
|
|
19
|
+
打开输入框旁的模型菜单,展开供应商,选择模型。启用了多密钥的供应商会多一级 **密钥配置**。你可以搜索模型或配置名称、展开或收起全部分组,也可以在菜单底部切换 **侧边面板** 或 **原地折叠**。清空搜索后会恢复原来的展开状态。
|
|
40
20
|
|
|
41
|
-
|
|
42
|
-
示例供应商 A
|
|
43
|
-
配置 A
|
|
44
|
-
示例模型
|
|
45
|
-
配置 B
|
|
46
|
-
示例模型
|
|
47
|
-
示例供应商 B
|
|
48
|
-
示例模型
|
|
49
|
-
```
|
|
21
|
+
开启多密钥前就已选中普通模型的会话会保留原选择,菜单中标为 **原单密钥(兼容旧会话)**。
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
关闭多密钥配置会保留已保存的配置与凭据,删除配置也不会同时删除凭据。有尚未保存的密钥输入时,需要先保存或清空,才能确认关闭。
|
|
54
|
-
|
|
55
|
-
窄屏侧边面板会根据可用空间调整各级宽度;三级并列时,较长名称可能截断。
|
|
56
|
-
|
|
57
|
-
## 更新
|
|
23
|
+
## 为同一家供应商添加多把密钥
|
|
58
24
|
|
|
59
|
-
|
|
25
|
+
### 启用多密钥
|
|
60
26
|
|
|
61
|
-
|
|
27
|
+
1. 在网页打开 **设置 → 模型分组**,点击 **启用多密钥 → 确认执行**。
|
|
28
|
+
2. 等页面提示已更新 DSH 文件后,**重启 DSH 引擎,再刷新网页**。只刷新网页不够。
|
|
62
29
|
|
|
63
|
-
|
|
30
|
+
这一步会检查并备份 DSH 程序文件,再安装补丁。安装目录必须可写;如果文件版本无法识别,插件会停止,不会强行覆盖。
|
|
64
31
|
|
|
65
|
-
|
|
32
|
+
### 添加密钥配置
|
|
66
33
|
|
|
67
|
-
|
|
34
|
+
1. 打开 **设置 → 模型**,新增或编辑一个支持多密钥的供应商。在 **公共设置** 中填写共享的 API 地址和默认协议。
|
|
35
|
+
2. 打开 **多密钥配置**,为第一项填写配置名称、API 密钥和模型列表;有更多密钥就点击 **添加密钥配置**。
|
|
36
|
+
3. 如果某把密钥需要不同的 API 协议,在 **分组 API 协议** 中选择;不确定时保留 **跟随供应商**,并以供应商说明为准。可选协议包括 OpenAI Chat Completions、OpenAI Responses 和 Anthropic Messages。
|
|
37
|
+
4. 点击 **应用**(新建时点击 **创建提供商**)。已保存的配置默认收起;要修改模型列表,点击 **展开详情**。编辑时将已保存的密钥输入框留空,原密钥会保留。
|
|
68
38
|
|
|
69
|
-
|
|
70
|
-
node scripts/doctor.mjs --dsh-root /opt/dsh --json
|
|
71
|
-
```
|
|
39
|
+

|
|
72
40
|
|
|
73
|
-
|
|
41
|
+
*展开一项密钥配置后,可以设置该项的 API 协议和模型列表。截图中的内部密钥引用已遮盖。*
|
|
74
42
|
|
|
75
|
-
|
|
43
|
+
<details>
|
|
44
|
+
<summary>查看开关位置、折叠效果和添加按钮</summary>
|
|
76
45
|
|
|
77
|
-
|
|
46
|
+
**找到开关**:新建供应商时,选择 **自定义模型 API**,在 **API 协议** 下方打开 **多密钥配置**。
|
|
78
47
|
|
|
79
|
-
|
|
48
|
+

|
|
80
49
|
|
|
81
|
-
|
|
50
|
+
**查看多份配置**:点击 **展开详情** 编辑对应配置,不必逐个展开其他配置。
|
|
82
51
|
|
|
83
|
-
|
|
52
|
+

|
|
84
53
|
|
|
85
|
-
|
|
54
|
+
**继续添加**:在配置列表下方点击 **添加密钥配置**。每份密钥配置至少需要一个模型。
|
|
86
55
|
|
|
87
|
-
|
|
56
|
+

|
|
88
57
|
|
|
89
|
-
|
|
58
|
+
</details>
|
|
90
59
|
|
|
91
|
-
|
|
60
|
+
如果只想让某个供应商恢复使用单密钥,先保存或清空尚未保存的密钥输入,再关掉该供应商的 **多密钥配置** 并点击 **应用**。已保存的多密钥配置不会被删除。
|
|
92
61
|
|
|
93
|
-
|
|
62
|
+
## 更新
|
|
94
63
|
|
|
95
|
-
1.
|
|
96
|
-
2.
|
|
97
|
-
3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
|
|
64
|
+
1. 在原 DSH 配置中用目标正式版本重新执行安装命令,**不需要先卸载**。重启 DSH 引擎并刷新网页。
|
|
65
|
+
2. 打开 **设置 → 模型分组**。如果出现 **更新多密钥补丁**,点击并确认;更新完成后,再重启引擎并刷新网页。没有更新提示就不用重复操作。
|
|
98
66
|
|
|
99
|
-
|
|
67
|
+
已识别且受支持的旧补丁可以直接更新,不用逐版安装。若提示文件冲突或不支持更新,请先复制诊断反馈;不要先卸载、还原补丁或删除配置。升级 **DSH 本身**前,请先确认目标 DSH 版本是否受插件支持。
|
|
100
68
|
|
|
101
|
-
|
|
102
|
-
dsh plugin --profile web remove dsh-model-groups
|
|
103
|
-
```
|
|
69
|
+
## 关闭或卸载
|
|
104
70
|
|
|
105
|
-
|
|
71
|
+
**如果已启用多密钥,不要直接从插件列表或命令行卸载。** 那样只会移除插件,不会还原 DSH 文件。
|
|
106
72
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
73
|
+
1. 保存设置草稿、停止运行中的任务,并把仍使用多密钥模型的会话、默认模型及子代理切回普通模型。建议先在各供应商设置里关闭 **多密钥配置** 并应用。
|
|
74
|
+
2. 打开 **设置 → 模型分组**,选择 **关闭多密钥**(保留模型分组插件),或 **关闭并卸载插件**(先还原 DSH 文件,再卸载),然后确认。
|
|
75
|
+
3. 页面提示需要重启时,**重启 DSH 引擎并刷新网页**。
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+

|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
*「设置 → 模型分组」在多密钥已启用时的界面(0.3.6)。按钮会随当前状态变化。*
|
|
114
80
|
|
|
115
|
-
|
|
116
|
-
npm run build
|
|
117
|
-
npm test
|
|
118
|
-
npm run check:build
|
|
119
|
-
```
|
|
81
|
+
关闭或卸载不会删除 API 密钥、供应商配置、聊天历史或备份,也不会替你切换模型。还原后,旧会话里的多密钥模型可能无法继续使用;原版 DSH 也可能无法读取残留的多密钥设置。遇到这种情况请先反馈诊断,不要直接删除密钥或整个配置。
|
|
120
82
|
|
|
121
|
-
|
|
83
|
+
## 遇到问题
|
|
122
84
|
|
|
123
|
-
|
|
85
|
+
- **找不到多密钥开关**:先确认已在 **设置 → 模型分组** 启用,并已重启引擎、刷新网页。只有支持多密钥的供应商编辑页会显示此开关,只能填写凭据的简化页面不会显示。
|
|
86
|
+
- **不确定是否已经生效**:设置页的 **刷新检查** 只检查文件,不会修改文件或重启引擎。若刚启用、更新或关闭多密钥,请重启引擎再刷新网页。
|
|
87
|
+
- **无法启用、更新或卸载**:打开 **设置 → 模型分组 → 检查详情与脱敏诊断**,点击 **复制脱敏诊断**,附在 [GitHub Issue](https://github.com/betsunninda9-lgtm/dsh-model-groups/issues) 中。该诊断不包含 API 密钥、文件路径、配置正文或聊天记录。
|
|
124
88
|
|
|
125
|
-
[MIT](LICENSE)
|
|
89
|
+
开发、测试和命令行恢复说明见 [维护指南](docs/maintenance.md)。项目基于 [MIT 许可证](LICENSE) 开源。部分模型选择器代码改编自 DeepSeek Harness,保留原始版权声明。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 开发与维护指南
|
|
2
|
+
|
|
3
|
+
日常安装和使用见 [README](../README.md)。
|
|
4
|
+
|
|
5
|
+
## 本地验证
|
|
6
|
+
|
|
7
|
+
需要 Node.js 22 或更新版本。修改网页端源码后,重新生成并提交客户端产物:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run build
|
|
11
|
+
npm test
|
|
12
|
+
npm run check:build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`npm run check:build` 只比对产物,不会覆盖它。历史版本升级测试读取 Git 历史,建议使用完整克隆。部分集成测试需要指定受支持的 DSH 安装目录;未指定时会跳过相应场景。需要覆盖其他已知构建时,可通过 `PI_AI_SOURCES` 传入只包含程序代码的文件,多个路径用系统路径分隔符隔开。集成测试在临时副本中写入,不要提供用户配置或凭据。
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
DSH_ROOT=/path/to/dsh npm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 两类网页代码
|
|
22
|
+
|
|
23
|
+
- 插件自己的网页界面位于 `src/`,由 `npm run build` 生成 `lib/client.js`。
|
|
24
|
+
- DSH 的「设置 → 模型」由宿主提供,发布包中的 `native/*.patch.json` 为已验证的程序文件补丁,不随客户端构建重新生成。若修改模型设置页补丁生成器,必须提供哈希匹配的原始程序文件,并验证相关构建的启用、升级及还原,不要直接编辑分发补丁或用其他构建的文件替换。
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node scripts/settings-models-collapse.mjs --source /path/to/pristine-client.js
|
|
28
|
+
node scripts/settings-models-collapse.mjs --source /path/to/pristine-client.js --check
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 诊断与恢复
|
|
32
|
+
|
|
33
|
+
只读诊断(请替换实际安装路径):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node scripts/doctor.mjs --dsh-root /path/to/dsh --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
遇到文件校验冲突或无法更新时,先保留现场并收集脱敏诊断。不要强制覆盖程序文件、删除配置或重建密钥。若此前从测试分支安装,可在原 DSH 配置中使用 README 的正式版本命令直接替换插件包,再按更新流程检查补丁。
|
|
40
|
+
|
|
41
|
+
若插件已误卸载或无法打开网页设置,在处理多密钥模型引用后,先停止正在使用的 DSH 引擎,再从保留的插件目录执行:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node scripts/native-patches.mjs --dsh-root /path/to/dsh --revert
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
确认恢复完成后,重新启动同一个 DSH 引擎并刷新网页。备份存放在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`。若因异常中断遗留同目录下的 `patch.lock`,请先确认没有补丁维护进程正在运行,切勿并发重试。原版 DSH 可能无法识别 `@dsh-key/` 路由以及 `multiKey`、`keyEntries` 等设置;恢复前请按 README 处理相关模型引用和配置,避免影响启动。
|
package/lib/client.js
CHANGED
|
@@ -1270,6 +1270,31 @@ function installSettingsPageStyles() {
|
|
|
1270
1270
|
document.head.appendChild(style);
|
|
1271
1271
|
}
|
|
1272
1272
|
|
|
1273
|
+
const MODEL_GROUPS_CLIENT_VERSION = "0.3.7";
|
|
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;
|
|
1273
1298
|
const MAINTENANCE_CONFIRMATION = 'CONFIRM PATCH OPERATION';
|
|
1274
1299
|
const MAINTENANCE_ENDPOINT = '/dsh-model-groups/maintenance';
|
|
1275
1300
|
const MAINTENANCE_KEYS = ['dsh:model-groups:v1', 'dsh:model-groups:mode'];
|
|
@@ -1286,14 +1311,16 @@ function maintenanceSnapshot(value) {
|
|
|
1286
1311
|
|| !(value.warnings == null || (Array.isArray(value.warnings) && value.warnings.every(warning => warning && typeof warning.code === 'string'
|
|
1287
1312
|
&& (warning.count == null || (Number.isSafeInteger(warning.count) && warning.count > 0)))))
|
|
1288
1313
|
|| !value.patches.every(patch => patch && ['patched', 'pristine', 'conflict'].includes(patch.state)
|
|
1289
|
-
&& (patch.updateAvailable == null || typeof patch.updateAvailable === 'boolean')
|
|
1314
|
+
&& (patch.updateAvailable == null || typeof patch.updateAvailable === 'boolean')
|
|
1315
|
+
&& (patch.applySupported == null || typeof patch.applySupported === 'boolean'))
|
|
1290
1316
|
|| !value.blockers.every(blocker => blocker && typeof blocker.code === 'string' && typeof blocker.message === 'string'
|
|
1291
1317
|
&& (blocker.count == null || (Number.isSafeInteger(blocker.count) && blocker.count > 0)))) {
|
|
1292
1318
|
throw new Error('Invalid maintenance snapshot');
|
|
1293
1319
|
}
|
|
1294
|
-
return { phase: value.phase,
|
|
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,
|
|
1295
1322
|
canRestoreOnly: value.canRestoreOnly, canRestore: value.canRestore,
|
|
1296
|
-
token: value.token ?? null, patches: value.patches.map(patch => ({ state: patch.state, updateAvailable: patch.updateAvailable === true })),
|
|
1323
|
+
token: value.token ?? null, patches: value.patches.map(patch => ({ state: patch.state, updateAvailable: patch.updateAvailable === true, applySupported: patch.applySupported !== false })),
|
|
1297
1324
|
blockers: value.blockers.map(blocker => ({ code: blocker.code, count: blocker.count ?? null })),
|
|
1298
1325
|
warnings: (value.warnings ?? []).map(warning => ({ code: warning.code, count: warning.count ?? null })) };
|
|
1299
1326
|
}
|
|
@@ -1301,14 +1328,17 @@ function maintenanceCanAction(snapshot, action) {
|
|
|
1301
1328
|
const capability = Object.hasOwn(ACTION_CAPABILITIES, action) ? ACTION_CAPABILITIES[action] : null;
|
|
1302
1329
|
return !!capability && !!snapshot && ['idle', 'applied', 'restored', 'failed'].includes(snapshot.phase) && snapshot[capability]
|
|
1303
1330
|
&& typeof snapshot.token === 'string' && snapshot.token.length > 0
|
|
1304
|
-
&& !snapshot.patches.some(patch => patch.state === 'conflict')
|
|
1331
|
+
&& !snapshot.patches.some(patch => patch.state === 'conflict')
|
|
1332
|
+
&& (action !== 'apply' || !snapshot.patches.some(patch => patch.applySupported === false));
|
|
1305
1333
|
}
|
|
1306
1334
|
function maintenanceCanRestore(snapshot) { return maintenanceCanAction(snapshot, 'restore-and-uninstall'); }
|
|
1307
1335
|
function maintenanceBlockerText(blocker) {
|
|
1308
1336
|
const labels = {
|
|
1309
1337
|
active_sessions: '存在正在运行的会话,请先停止运行中的任务。',
|
|
1310
1338
|
active_turns: '存在正在运行的回合,请先等待结束或停止。',
|
|
1311
|
-
patch_conflict: '
|
|
1339
|
+
patch_conflict: '补丁状态冲突,不会强制覆盖。请复制脱敏诊断;不要先卸载、还原或删除配置。',
|
|
1340
|
+
client_version_mismatch: '网页与插件后端版本不一致,已禁止维护。请重启原引擎并刷新网页后重新检查。',
|
|
1341
|
+
unsupported_patch_build: '此宿主构建尚无经过验证的新版协议补丁,已阻止不完整更新;仍可关闭或卸载,不会改动密钥。',
|
|
1312
1342
|
backup_missing: '缺少可验证的原版备份,无法安全恢复。',
|
|
1313
1343
|
restart_required: '需要重启原引擎后重新检查。',
|
|
1314
1344
|
inspection_failed: '状态检查失败,请使用安装诊断排查。',
|
|
@@ -1334,8 +1364,8 @@ function maintenanceBlockerText(blocker) {
|
|
|
1334
1364
|
inspection_unknown: '无法确认运行状态,请先停止任务并使用安装诊断排查。',
|
|
1335
1365
|
profile_unknown: '无法确认当前配置档,不会执行维护。',
|
|
1336
1366
|
cross_profile_bundle: '其他配置档也在使用此插件,请先处理跨配置档依赖。',
|
|
1337
|
-
alias_dependency: '
|
|
1338
|
-
key_config_dependency: '
|
|
1367
|
+
alias_dependency: '仍有会话或设置在使用多密钥模型。请先切换为普通模型;插件不会自动转换。',
|
|
1368
|
+
key_config_dependency: '仍有供应商启用了多密钥。请先在「设置 → 模型」中关闭对应开关并保存;已保存的密钥会保留,配置不会自动转换。',
|
|
1339
1369
|
expression_unknown: '存在无法安全判断的配置表达式,请先人工检查配置。',
|
|
1340
1370
|
other_process: '其他引擎进程可能正在使用补丁,请先停止相关进程。',
|
|
1341
1371
|
};
|
|
@@ -1346,7 +1376,7 @@ function maintenanceBlockerText(blocker) {
|
|
|
1346
1376
|
// Copy only allowlisted codes and enumerated state: unknown codes may contain secrets or paths.
|
|
1347
1377
|
function maintenanceDiagnostic(state) {
|
|
1348
1378
|
const snapshot = state.snapshot;
|
|
1349
|
-
const knownCodes = new Set(['active_sessions', 'active_turns', 'patch_conflict', 'backup_missing', 'restart_required',
|
|
1379
|
+
const knownCodes = new Set(['active_sessions', 'active_turns', 'patch_conflict', 'client_version_mismatch', 'unsupported_patch_build', 'backup_missing', 'restart_required',
|
|
1350
1380
|
'inspection_failed', 'config_inspection_failed', 'session_inspection_failed', 'session_service_missing',
|
|
1351
1381
|
'process_inspection_failed', 'profile_inspection_failed', 'patch_inspection_failed', 'default_inspection_failed',
|
|
1352
1382
|
'agent_inspection_failed', 'activity_unverified', 'not_removable', 'not_applicable', 'manager_unavailable',
|
|
@@ -1360,6 +1390,8 @@ function maintenanceDiagnostic(state) {
|
|
|
1360
1390
|
return JSON.stringify({
|
|
1361
1391
|
status: snapshot?.phase ?? 'unknown', busy: !!state.busy, resultUnverified: !!state.error,
|
|
1362
1392
|
restartRequired: snapshot?.restartRequired ?? null,
|
|
1393
|
+
maintenanceProtocol: snapshot?.maintenanceProtocol ?? null, pluginVersion: publicVersion(snapshot?.pluginVersion),
|
|
1394
|
+
compatibility: compatibilitySnapshot(snapshot?.compatibility),
|
|
1363
1395
|
capabilities: snapshot ? { apply: snapshot.canApply, close: snapshot.canRestoreOnly, uninstall: snapshot.canRestore } : null,
|
|
1364
1396
|
patches: snapshot?.patches.map(patch => patch.state) ?? [],
|
|
1365
1397
|
blockers: snapshot?.blockers.map(diagnosticItem) ?? [],
|
|
@@ -1378,7 +1410,8 @@ function maintenanceWarningText(warning) {
|
|
|
1378
1410
|
+ (warning.count ? `(${warning.count} 项)` : '');
|
|
1379
1411
|
}
|
|
1380
1412
|
|
|
1381
|
-
function createMaintenanceController({ request = (...args) => fetch(...args), storage = () => globalThis.localStorage
|
|
1413
|
+
function createMaintenanceController({ request = (...args) => fetch(...args), storage = () => globalThis.localStorage,
|
|
1414
|
+
clientVersion = typeof MODEL_GROUPS_CLIENT_VERSION === 'string' ? MODEL_GROUPS_CLIENT_VERSION : null } = {}) {
|
|
1382
1415
|
let state = { snapshot: null, busy: false, error: '', cleanupWarning: false, patchedWarning: false };
|
|
1383
1416
|
let disposed = false;
|
|
1384
1417
|
let generation = 0;
|
|
@@ -1389,17 +1422,22 @@ function createMaintenanceController({ request = (...args) => fetch(...args), st
|
|
|
1389
1422
|
if (!response.ok) throw new Error('Maintenance HTTP error');
|
|
1390
1423
|
const envelope = await response.json();
|
|
1391
1424
|
if (envelope?.ok !== true) throw new Error('Maintenance refused');
|
|
1392
|
-
|
|
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;
|
|
1393
1431
|
}
|
|
1394
1432
|
function accept(snapshot) {
|
|
1395
1433
|
let cleanupWarning = false;
|
|
1396
|
-
if (snapshot.phase === 'removed') {
|
|
1434
|
+
if (snapshot.phase === 'removed' && !snapshot.versionMismatch) {
|
|
1397
1435
|
for (const key of MAINTENANCE_KEYS) {
|
|
1398
1436
|
try { storage()?.removeItem(key); } catch { cleanupWarning = true; }
|
|
1399
1437
|
}
|
|
1400
1438
|
}
|
|
1401
1439
|
update({ snapshot, cleanupWarning, patchedWarning: snapshot.patches.some(patch => patch.state === 'patched'),
|
|
1402
|
-
error: snapshot.phase === 'failed' ? '维护操作失败,未确认完成。请刷新检查,不要强制覆盖。' : '' });
|
|
1440
|
+
error: snapshot.versionMismatch ? '网页与插件后端版本不一致,已停止维护。请重启原引擎并刷新网页后复查。' : snapshot.phase === 'failed' ? '维护操作失败,未确认完成。请刷新检查,不要强制覆盖。' : '' });
|
|
1403
1441
|
}
|
|
1404
1442
|
async function run(action = null) {
|
|
1405
1443
|
if (disposed || state.busy || (action && !maintenanceCanAction(state.snapshot, action))) return;
|
|
@@ -1419,7 +1457,7 @@ function createMaintenanceController({ request = (...args) => fetch(...args), st
|
|
|
1419
1457
|
}
|
|
1420
1458
|
update({ snapshot: { ...snapshot, canApply: false, canRestoreOnly: false, canRestore: false, token: null } });
|
|
1421
1459
|
snapshot = await readSnapshot({ method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1422
|
-
body: JSON.stringify({ action, token, confirmation: MAINTENANCE_CONFIRMATION }) });
|
|
1460
|
+
body: JSON.stringify({ action, token, confirmation: MAINTENANCE_CONFIRMATION, ...(clientVersion === null ? {} : { maintenanceProtocol: MAINTENANCE_CLIENT_PROTOCOL, clientVersion }) }) });
|
|
1423
1461
|
if (!current()) return;
|
|
1424
1462
|
}
|
|
1425
1463
|
accept(snapshot);
|
|
@@ -1453,8 +1491,11 @@ function ModelGroupsSettingsPage() {
|
|
|
1453
1491
|
const snapshot = state.snapshot;
|
|
1454
1492
|
const phaseLabels = { idle: '检查完成,可选择操作', applied: '多密钥已应用(磁盘状态)', restored: '多密钥已关闭,插件仍保留(磁盘状态)', removing: '正在卸载,尚未确认完成', removed: '已确认关闭多密钥并卸载插件', failed: '维护失败,未确认完成' };
|
|
1455
1493
|
const updatesAvailable = snapshot?.patches.some(patch => patch.updateAvailable) ?? false;
|
|
1456
|
-
const statusLabel = snapshot?.phase === 'idle'
|
|
1457
|
-
? snapshot.patches.
|
|
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') ? '多密钥已启用(磁盘状态)'
|
|
1458
1499
|
: snapshot.patches.length && snapshot.patches.every(patch => patch.state === 'pristine') ? '多密钥已关闭(磁盘状态)' : '磁盘状态待处理,请查看操作限制'
|
|
1459
1500
|
: phaseLabels[snapshot?.phase];
|
|
1460
1501
|
const patchLabels = { patched: '已应用', pristine: '原版', conflict: '冲突(不会强制覆盖)' };
|
|
@@ -1467,16 +1508,18 @@ function ModelGroupsSettingsPage() {
|
|
|
1467
1508
|
catch { setCopyStatus('无法复制,请检查浏览器剪贴板权限。'); }
|
|
1468
1509
|
};
|
|
1469
1510
|
return h('section', { className: 'mg-maintenance', 'aria-label': '模型分组设置', 'aria-busy': state.busy },
|
|
1470
|
-
h('h2', null, '
|
|
1471
|
-
h('p', null, '
|
|
1511
|
+
h('h2', null, '多密钥管理'),
|
|
1512
|
+
h('p', null, '在这里启用、更新或关闭多密钥。如果只想按供应商浏览模型,无需启用多密钥。'),
|
|
1513
|
+
snapshot?.pluginVersion && h('p', { className: 'mg-maintenance-help' }, `插件 ${snapshot.pluginVersion} · DSH ${snapshot.compatibility?.hostVersion ?? '版本未确认'}`),
|
|
1472
1514
|
h('section', { className: 'mg-maintenance-card', 'aria-label': '当前状态' },
|
|
1473
1515
|
h('div', { className: 'mg-maintenance-row' }, h('h3', null, '当前状态'), h('button', { type: 'button', disabled: state.busy, onClick: refresh }, state.busy ? '处理中…' : '刷新检查')),
|
|
1474
1516
|
h('p', { role: 'status', 'aria-live': 'polite', className: 'mg-maintenance-help' }, state.busy ? '正在检查或执行维护,请勿关闭或重复操作。' : snapshot ? statusLabel : '尚无可验证状态,请刷新检查。'),
|
|
1475
1517
|
state.error && h('p', { role: 'alert', className: 'mg-maintenance-error' }, state.error),
|
|
1476
|
-
state.patchedWarning && h('p', { role: 'alert', className: 'mg-maintenance-error' }, '
|
|
1477
|
-
snapshot?.restartRequired && h('p', { role: 'status' }, '
|
|
1518
|
+
state.patchedWarning && h('p', { role: 'alert', className: 'mg-maintenance-error' }, '从普通插件列表卸载只移除插件,不会还原 DSH 文件。需要卸载时,请使用此页的「关闭并卸载插件」。'),
|
|
1519
|
+
snapshot?.restartRequired && h('p', { role: 'status' }, 'DSH 程序文件已更新,需要重启 DSH 引擎,再刷新网页。只刷新网页不会生效。'),
|
|
1520
|
+
compatibilityGuidance(snapshot?.compatibility) && h('p', { role: 'status', className: 'mg-maintenance-help' }, compatibilityGuidance(snapshot.compatibility)),
|
|
1478
1521
|
updatesAvailable && h('p', { role: 'status' }, '多密钥补丁有更新可用;确认后将备份、校验并更新已识别的旧补丁。'),
|
|
1479
|
-
snapshot?.phase === 'removed' && h('p', { role: 'status' }, '卸载已确认:仅清理本插件的两个浏览器显示偏好,密钥、历史和备份均保留。'),
|
|
1522
|
+
snapshot?.phase === 'removed' && !snapshot.versionMismatch && h('p', { role: 'status' }, '卸载已确认:仅清理本插件的两个浏览器显示偏好,密钥、历史和备份均保留。'),
|
|
1480
1523
|
snapshot?.warnings.length > 0 && h('p', { className: 'mg-maintenance-warning', role: 'status' }, '部分使用情况未验证,关闭前请阅读提示;检查提醒不会额外禁用操作。'),
|
|
1481
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))))),
|
|
1482
1525
|
state.cleanupWarning && h('p', { role: 'status' }, '卸载已确认,但浏览器阻止清理本插件的显示偏好;其他数据未受影响。')),
|
|
@@ -1486,15 +1529,20 @@ function ModelGroupsSettingsPage() {
|
|
|
1486
1529
|
!anyAllowed && h('p', null, '请刷新检查或处理上方操作限制。检查提醒本身不会禁用按钮。'),
|
|
1487
1530
|
pending && h('div', { role: 'dialog', 'aria-labelledby': confirmationId, 'aria-describedby': `${confirmationId}-help`, className: 'mg-maintenance-dialog' },
|
|
1488
1531
|
h('h3', { id: confirmationId }, `确认${actions.find(([action]) => action === pending)[1]}?`),
|
|
1489
|
-
h('p', { id: `${confirmationId}-help` }, pending === 'apply'
|
|
1490
|
-
|
|
1491
|
-
|
|
1532
|
+
h('p', { id: `${confirmationId}-help` }, pending === 'apply'
|
|
1533
|
+
? `插件会检查并备份 DSH 程序文件,再${updatesAvailable ? '更新多密钥补丁' : '启用多密钥'}。不会自动切换模型。`
|
|
1534
|
+
: '插件会还原 DSH 程序文件,保留密钥、配置和聊天记录。请先将仍使用多密钥的会话和默认模型改为普通模型;残留的多密钥配置可能影响 DSH 启动。'),
|
|
1535
|
+
h('p', null, '请先保存草稿并停止所有任务。完成后按提示重启 DSH 引擎,再刷新网页。'),
|
|
1536
|
+
snapshot?.warnings.length > 0 && h('p', null, '部分使用情况无法检查,请先阅读页面中的检查详情。'),
|
|
1537
|
+
pending === 'restore-and-uninstall' && h('p', null, '还原成功后才会卸载插件。确认卸载后会清除本浏览器中的模型菜单显示偏好;连接中断不代表卸载成功。'),
|
|
1492
1538
|
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 }, '确认执行')))),
|
|
1493
1539
|
h('details', { className: 'mg-maintenance-card' },
|
|
1494
1540
|
h('summary', null, '检查详情与脱敏诊断'),
|
|
1495
|
-
h('p', null, '
|
|
1541
|
+
h('p', null, '这里能检查 DSH 文件,但无法确认正在运行的引擎是否已加载更新。确认操作后会再次检查;如果状态变化,需要重新确认。无法识别的文件不会被覆盖。'),
|
|
1496
1542
|
snapshot?.warnings.length > 0 && h('ul', null, ...snapshot.warnings.map((warning, index) => h('li', { key: index }, maintenanceWarningText(warning)))),
|
|
1497
|
-
snapshot && h('ul', null, ...snapshot.patches.map((patch, index) => h('li', { key: index }, `宿主文件 ${index + 1}:${patchLabels[patch.state]}${patch.updateAvailable ? '(更新可用)' : ''}`))),
|
|
1543
|
+
snapshot && h('ul', null, ...snapshot.patches.map((patch, index) => h('li', { key: index }, `宿主文件 ${index + 1}:${patchLabels[patch.state]}${patch.applySupported === false ? '(此构建不支持新版补丁)' : patch.updateAvailable ? '(更新可用)' : ''}`))),
|
|
1544
|
+
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 ?? '不可读取'}`))),
|
|
1545
|
+
h('p', null, '诊断仅含程序版本、固定文件标识与程序 SHA-256,不含文件路径、配置正文、密钥或聊天。升级失败请先保留现场,不要用另一构建的文件覆盖。'),
|
|
1498
1546
|
h('button', { type: 'button', onClick: copy }, '复制脱敏诊断'),
|
|
1499
1547
|
h('p', { role: 'status' }, copyStatus)));
|
|
1500
1548
|
}
|
|
@@ -1522,8 +1570,8 @@ function apply(ctx) {
|
|
|
1522
1570
|
statusNotice.setAttribute('role', 'status');
|
|
1523
1571
|
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;';
|
|
1524
1572
|
statusNotice.textContent = status.failed
|
|
1525
|
-
? '
|
|
1526
|
-
: '
|
|
1573
|
+
? '无法检查多密钥功能所需的 DSH 文件。请打开「设置 → 模型分组」查看详情,不要手动覆盖文件。'
|
|
1574
|
+
: 'DSH 程序文件已更新。请先保存草稿,再重启 DSH 引擎并刷新网页。重启前请暂缓修改模型设置。';
|
|
1527
1575
|
document.body.appendChild(statusNotice);
|
|
1528
1576
|
}).catch(() => {});
|
|
1529
1577
|
ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });
|