dsh-model-groups 0.3.4 → 0.3.5

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
@@ -9,6 +9,8 @@
9
9
  - 记住各级折叠状态;搜索时临时展开匹配项。
10
10
  - 不同密钥配置可使用相同模型 ID,选择时使用对应配置的密钥。
11
11
  - 手动选择配置,不自动轮换密钥或切换到其他配置。
12
+ - 模型设置中已保存的密钥项默认折叠,名称和原密钥栏仍可见;展开编辑模型,新建和错误项保持展开,折叠不丢失草稿。
13
+ - 实际修改补丁才提示重启;普通检查不要求重启,关闭状态标记不再被误当成待重启。
12
14
 
13
15
  ## 环境要求
14
16
 
@@ -17,32 +19,17 @@
17
19
  - Harness 网页界面;在 dsh-go 中使用时,功能出现在对应网页,不影响 App 原生模型菜单。
18
20
  - DSH 安装目录可写,且目标程序文件符合支持的版本与哈希。
19
21
 
20
- **本插件包含宿主兼容补丁。** 插件加载时会检查并备份原文件,自动应用五项补丁;不修改现有供应商、密钥或会话配置。版本或文件内容不受支持时会停止应用,不强制覆盖。
22
+ **本版本不会自动打补丁。** 请在网页设置 → 模型分组明确选择安装。安装会校验、备份并修改五个宿主文件,之后须重启引擎。普通卸载插件不会撤销宿主修改。未知版本/文件不强制覆盖。
21
23
 
22
24
  ## 安装
23
25
 
24
26
  在目标设备的 DSH 环境中执行:
25
27
 
26
28
  ```bash
27
- dsh plugin --profile web add dsh-model-groups@0.3.4 --registry=https://registry.npmjs.org/
29
+ dsh plugin --profile web add https://github.com/betsunninda9-lgtm/dsh-model-groups/releases/download/v0.3.5/dsh-model-groups-0.3.5.tgz
28
30
  ```
29
31
 
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。
31
-
32
- 插件加载并提示“补丁已自动安装”后,保存草稿,**重启原 DSH 引擎并刷新网页**。无需先停止引擎,也不需要外部终端手工打补丁。
33
-
34
- 如果安装后插件尚未加载,先启动或重启引擎;若此次启动才应用补丁,再重启一次以载入新模块。补丁已在磁盘就绪时,后续加载不会重复写入。
35
-
36
- 当前正式版为 `0.3.4`,包含已验证的自动补丁安装与侧边面板修复。
37
-
38
- ### 交给 AI 安装
39
-
40
- ```text
41
- 请按 https://github.com/betsunninda9-lgtm/dsh-model-groups 的安装说明,
42
- 使用 npm 正式版安装到当前 DSH profile。
43
- 我允许插件自动校验、备份并应用宿主补丁;保留现有配置。
44
- 检查加载日志,成功后提醒我重启原引擎并刷新网页;遇到不兼容就停止报告。
45
- ```
32
+ 此包只用于 DSH 0.2.2-rc.1 网页端。安装或更新插件后需重启原引擎使新后端生效,再刷新页面。已有 beta.3~beta.5 的磁盘补丁会保留,不会自动重打或还原。需要新折叠界面时,在模型分组设置页点击“更新多密钥补丁”,完成后重启原引擎并刷新网页;旧补丁可直接更新或还原,不需要先关闭多密钥。未打补丁时仍可使用普通模型;多密钥需在本页安装补丁并再次重启后启用。
46
33
 
47
34
  ## 使用
48
35
 
@@ -77,7 +64,7 @@ DSHMarket 的收录需经过社区审核。普通插件的市场更新跟随 npm
77
64
 
78
65
  ## 安装诊断
79
66
 
80
- 没有出现“多密钥配置”开关时,先确认原引擎已在自动补丁后重启,并刷新原网页。仍未恢复,可在已安装插件目录执行:
67
+ 没有出现“多密钥配置”开关时,先确认原引擎已在启用多密钥后重启,并刷新原网页。仍未恢复,可在已安装插件目录执行:
81
68
 
82
69
  ```bash
83
70
  node scripts/doctor.mjs --dsh-root /opt/dsh --json
@@ -89,15 +76,27 @@ node scripts/doctor.mjs --dsh-root /opt/dsh --json
89
76
 
90
77
  原文件备份保存在 `DSH_HOME/dsh-model-groups/<安装目录指纹>/backups`,不会随插件包更新丢失。安装意外中断可能留下 `patch.lock`;确认没有安装进程后再处理,不要并发重试。
91
78
 
92
- ## 卸载与还原
79
+ ## 独立补丁管理页(0.3.5)
80
+
81
+ 网页设置 → 模型分组,提供:**启用多密钥 / 关闭多密钥**,以及次要操作 **关闭并卸载插件**。点操作后只确认一次,无需勾选或输入英文。首次安装不会修改宿主,点击刷新也只读检查。详细检查折叠显示,可以复制脱敏诊断。
82
+
83
+ 普通卸载保留宿主修改,后续引擎升级、其他插件修改同一文件可能冲突;补丁被覆盖后旧多密钥选择可能失效。页面会持续警告,而非只弹一次。磁盘校验不等于运行中模块已刷新;操作完成需重启原引擎。无法证明运行中版本时不显示虚假生效结论。
84
+
85
+ 本阶段已实现手动补丁生命周期与诊断细分。**备用模型批量切换、私有配置归档和清理尚未接入页面,不声称已完成上一轮设计的全部迁移功能。** 多密钥配置、别名选择、历史子智能体策略及未能完成的检查改为风险提示,不再锁死操作。确认关闭意味着接受旧选择可能失效、旧配置可能影响原版引擎重启的风险;不声称无损迁移。文件冲突、已确认运行任务或共用安装的其他引擎仍阻止操作。没有修改凭据或聊天历史,不会自动请求模型、偷偷切换 DeepSeek 或拆分供应商。
86
+
87
+ 安装和移除都要求已认证的 owner、短期一次性票据、完整文件校验和互斥锁。未知文件不覆盖。移除只操作本插件已知补丁;卸载必须在还原验证成功后才执行。断连、卸载失败、仅要求重启不算成功。恢复标记与备份保留;重新安装补丁时仅在明确确认应用并验证成功后清理自身恢复标记。
88
+
89
+ 检查范围限当前 home/安装,维护时先停止任务、保存草稿,不要在其他页变更选择或配置;插件锁不是全局请求屏障。未验证的依赖或活动状态会明确提示但不冒充已发现危险;请自行确保所有任务已停止。依赖检查不是无损卸载证明。诊断只展示固定错误类别,不展示秘密、表达式正文或原始路径。
90
+
91
+ ## 卸载与还原(手工备用路径)
93
92
 
94
- 移除插件不会自动还原宿主补丁或删除凭据。完整还原前,请先:
93
+ 普通插件列表移除仍不会自动还原宿主补丁或删除凭据;更新/HMR 也不会触发还原。开发预览版可优先用上述专用入口。手工完整还原前,请先:
95
94
 
96
95
  1. 将相关会话和子代理切换回普通单密钥模型。
97
96
  2. 关闭多密钥配置,备份配置并清理原版 DSH 不支持的 `multiKey`、`keyEntries` 字段,保留仍需使用的凭据。
98
97
  3. 在独立维护目录保留插件包,确保卸载后仍能运行还原脚本。
99
98
 
100
- 先停用或移除插件,避免其再次加载时重新应用补丁:
99
+ 普通卸载不还原补丁。以下为高级手工备用路径(本预览启动不再自动应用):
101
100
 
102
101
  ```bash
103
102
  dsh plugin --profile web remove dsh-model-groups
package/lib/client.js CHANGED
@@ -1258,6 +1258,247 @@ 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 MAINTENANCE_CONFIRMATION = 'CONFIRM PATCH OPERATION';
1274
+ const MAINTENANCE_ENDPOINT = '/dsh-model-groups/maintenance';
1275
+ const MAINTENANCE_KEYS = ['dsh:model-groups:v1', 'dsh:model-groups:mode'];
1276
+ const ACTION_CAPABILITIES = { apply: 'canApply', 'restore-only': 'canRestoreOnly', 'restore-and-uninstall': 'canRestore' };
1277
+
1278
+ // Never project arbitrary server messages, patch paths, or tokens into the DOM.
1279
+ function maintenanceSnapshot(value) {
1280
+ const phases = ['idle', 'applied', 'restored', 'removing', 'removed', 'failed'];
1281
+ if (!value || !phases.includes(value.phase) || typeof value.restartRequired !== 'boolean'
1282
+ || !Object.values(ACTION_CAPABILITIES).every(key => typeof value[key] === 'boolean')
1283
+ || !(value.token == null || (typeof value.token === 'string' && value.token.length > 0))
1284
+ || (Object.values(ACTION_CAPABILITIES).some(key => value[key]) && !(typeof value.token === 'string' && value.token.length > 0))
1285
+ || !Array.isArray(value.patches) || !Array.isArray(value.blockers)
1286
+ || !(value.warnings == null || (Array.isArray(value.warnings) && value.warnings.every(warning => warning && typeof warning.code === 'string'
1287
+ && (warning.count == null || (Number.isSafeInteger(warning.count) && warning.count > 0)))))
1288
+ || !value.patches.every(patch => patch && ['patched', 'pristine', 'conflict'].includes(patch.state)
1289
+ && (patch.updateAvailable == null || typeof patch.updateAvailable === 'boolean'))
1290
+ || !value.blockers.every(blocker => blocker && typeof blocker.code === 'string' && typeof blocker.message === 'string'
1291
+ && (blocker.count == null || (Number.isSafeInteger(blocker.count) && blocker.count > 0)))) {
1292
+ throw new Error('Invalid maintenance snapshot');
1293
+ }
1294
+ return { phase: value.phase, restartRequired: value.restartRequired, canApply: value.canApply,
1295
+ canRestoreOnly: value.canRestoreOnly, canRestore: value.canRestore,
1296
+ token: value.token ?? null, patches: value.patches.map(patch => ({ state: patch.state, updateAvailable: patch.updateAvailable === true })),
1297
+ blockers: value.blockers.map(blocker => ({ code: blocker.code, count: blocker.count ?? null })),
1298
+ warnings: (value.warnings ?? []).map(warning => ({ code: warning.code, count: warning.count ?? null })) };
1299
+ }
1300
+ function maintenanceCanAction(snapshot, action) {
1301
+ const capability = Object.hasOwn(ACTION_CAPABILITIES, action) ? ACTION_CAPABILITIES[action] : null;
1302
+ return !!capability && !!snapshot && ['idle', 'applied', 'restored', 'failed'].includes(snapshot.phase) && snapshot[capability]
1303
+ && typeof snapshot.token === 'string' && snapshot.token.length > 0
1304
+ && !snapshot.patches.some(patch => patch.state === 'conflict');
1305
+ }
1306
+ function maintenanceCanRestore(snapshot) { return maintenanceCanAction(snapshot, 'restore-and-uninstall'); }
1307
+ function maintenanceBlockerText(blocker) {
1308
+ const labels = {
1309
+ active_sessions: '存在正在运行的会话,请先停止运行中的任务。',
1310
+ active_turns: '存在正在运行的回合,请先等待结束或停止。',
1311
+ patch_conflict: '补丁状态冲突,请先人工诊断,不会强制覆盖。',
1312
+ backup_missing: '缺少可验证的原版备份,无法安全恢复。',
1313
+ restart_required: '需要重启原引擎后重新检查。',
1314
+ inspection_failed: '状态检查失败,请使用安装诊断排查。',
1315
+ config_inspection_failed: '配置检查失败,无法验证模型与密钥依赖;请使用安装诊断排查。',
1316
+ session_inspection_failed: '会话检查失败,无法验证活动回合与模型引用;请使用安装诊断排查。',
1317
+ session_service_missing: '缺少会话查询或持久化服务,无法验证历史会话的模型依赖;请使用安装诊断排查。',
1318
+ process_inspection_failed: '进程检查失败,无法排除其他引擎使用补丁;请使用安装诊断排查。',
1319
+ profile_inspection_failed: '配置档检查失败,无法排除跨配置档依赖;请使用安装诊断排查。',
1320
+ patch_inspection_failed: '补丁文件检查失败,无法验证磁盘状态;不会修改未知文件。',
1321
+ not_removable: '当前插件状态不允许安全卸载。',
1322
+ not_applicable: '当前状态不允许安全应用补丁。',
1323
+ manager_unavailable: '插件管理服务不可用,请稍后重试。',
1324
+ host_unknown: '无法验证宿主版本,不会修改未知文件。',
1325
+ state_changed: '状态已发生变化,请刷新检查后重新打开操作并确认。',
1326
+ apply_failed: '补丁应用未完成,请刷新检查,不要视为成功。',
1327
+ restore_failed: '原版恢复未完成,请刷新检查,不要视为成功。',
1328
+ remove_failed: '卸载未完成,请刷新检查并使用安装诊断排查。',
1329
+ remove_unverified: '无法验证卸载结果,请刷新检查,不要视为成功。',
1330
+ verify_failed: '安全验证失败,不会强制覆盖文件。',
1331
+ busy: '维护或其他任务正在运行,请结束后刷新。',
1332
+ confirmation_required: '安全确认已失效,请刷新检查后重新打开操作并确认。',
1333
+ maintenance_failed: '维护失败,请刷新检查并使用安装诊断排查。',
1334
+ inspection_unknown: '无法确认运行状态,请先停止任务并使用安装诊断排查。',
1335
+ profile_unknown: '无法确认当前配置档,不会执行维护。',
1336
+ cross_profile_bundle: '其他配置档也在使用此插件,请先处理跨配置档依赖。',
1337
+ alias_dependency: '仍有会话或配置引用多密钥模型,请先切换为普通模型再检查;本预览不会自动转换。',
1338
+ key_config_dependency: '仍有多密钥配置依赖:请先禁用 multiKey 并清理 keyEntries 配置;已保存的密钥会保留。本预览不会自动转换。',
1339
+ expression_unknown: '存在无法安全判断的配置表达式,请先人工检查配置。',
1340
+ other_process: '其他引擎进程可能正在使用补丁,请先停止相关进程。',
1341
+ };
1342
+ return (labels[blocker.code.toLowerCase()] || '安全检查未通过,请先处理运行中的任务或使用安装诊断排查。')
1343
+ + (blocker.count ? `(${blocker.count} 项)` : '');
1344
+ }
1345
+
1346
+ // Copy only allowlisted codes and enumerated state: unknown codes may contain secrets or paths.
1347
+ function maintenanceDiagnostic(state) {
1348
+ const snapshot = state.snapshot;
1349
+ const knownCodes = new Set(['active_sessions', 'active_turns', 'patch_conflict', 'backup_missing', 'restart_required',
1350
+ 'inspection_failed', 'config_inspection_failed', 'session_inspection_failed', 'session_service_missing',
1351
+ 'process_inspection_failed', 'profile_inspection_failed', 'patch_inspection_failed', 'default_inspection_failed',
1352
+ 'agent_inspection_failed', 'activity_unverified', 'not_removable', 'not_applicable', 'manager_unavailable',
1353
+ 'host_unknown', 'state_changed', 'apply_failed', 'restore_failed', 'remove_failed', 'remove_unverified',
1354
+ 'verify_failed', 'busy', 'confirmation_required', 'maintenance_failed', 'inspection_unknown', 'profile_unknown',
1355
+ 'cross_profile_bundle', 'alias_dependency', 'key_config_dependency', 'expression_unknown', 'other_process']);
1356
+ const diagnosticItem = item => {
1357
+ const code = item.code.toLowerCase();
1358
+ return { code: knownCodes.has(code) ? code : 'unknown', count: item.count };
1359
+ };
1360
+ return JSON.stringify({
1361
+ status: snapshot?.phase ?? 'unknown', busy: !!state.busy, resultUnverified: !!state.error,
1362
+ restartRequired: snapshot?.restartRequired ?? null,
1363
+ capabilities: snapshot ? { apply: snapshot.canApply, close: snapshot.canRestoreOnly, uninstall: snapshot.canRestore } : null,
1364
+ patches: snapshot?.patches.map(patch => patch.state) ?? [],
1365
+ blockers: snapshot?.blockers.map(diagnosticItem) ?? [],
1366
+ warnings: snapshot?.warnings.map(diagnosticItem) ?? [],
1367
+ }, null, 2);
1368
+ }
1369
+ function maintenanceWarningText(warning) {
1370
+ const labels = {
1371
+ alias_dependency: '旧多密钥模型选择可能影响后续使用或重启;本页不会自动切换服务或模型。',
1372
+ key_config_dependency: '旧多密钥配置可能影响后续使用或重启;关闭会保留密钥和历史,不自动转换配置。',
1373
+ expression_unknown: '部分配置表达式无法完整检查,请留意关闭后的使用和重启情况。',
1374
+ };
1375
+ const code = warning.code.toLowerCase();
1376
+ 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'];
1377
+ return (labels[code] || (inspection.includes(code) ? '检查不完整,无法确认全部依赖;这只是提醒,不会额外禁用操作。' : '部分使用情况未能验证,不等于安全;这只是提醒,不会额外禁用操作。'))
1378
+ + (warning.count ? `(${warning.count} 项)` : '');
1379
+ }
1380
+
1381
+ function createMaintenanceController({ request = (...args) => fetch(...args), storage = () => globalThis.localStorage } = {}) {
1382
+ let state = { snapshot: null, busy: false, error: '', cleanupWarning: false, patchedWarning: false };
1383
+ let disposed = false;
1384
+ let generation = 0;
1385
+ const listeners = new Set();
1386
+ const update = next => { if (disposed) return; state = { ...state, ...next }; for (const listener of listeners) listener(); };
1387
+ async function readSnapshot(options) {
1388
+ const response = await request(MAINTENANCE_ENDPOINT, { credentials: 'same-origin', cache: 'no-store', ...options });
1389
+ if (!response.ok) throw new Error('Maintenance HTTP error');
1390
+ const envelope = await response.json();
1391
+ if (envelope?.ok !== true) throw new Error('Maintenance refused');
1392
+ return maintenanceSnapshot(envelope.value);
1393
+ }
1394
+ function accept(snapshot) {
1395
+ let cleanupWarning = false;
1396
+ if (snapshot.phase === 'removed') {
1397
+ for (const key of MAINTENANCE_KEYS) {
1398
+ try { storage()?.removeItem(key); } catch { cleanupWarning = true; }
1399
+ }
1400
+ }
1401
+ update({ snapshot, cleanupWarning, patchedWarning: snapshot.patches.some(patch => patch.state === 'patched'),
1402
+ error: snapshot.phase === 'failed' ? '维护操作失败,未确认完成。请刷新检查,不要强制覆盖。' : '' });
1403
+ }
1404
+ async function run(action = null) {
1405
+ if (disposed || state.busy || (action && !maintenanceCanAction(state.snapshot, action))) return;
1406
+ const sequence = ++generation;
1407
+ const token = state.snapshot?.token;
1408
+ const current = () => !disposed && sequence === generation;
1409
+ update({ busy: true, error: '' });
1410
+ try {
1411
+ // Recheck immediately before using the confirmed token; a changed state needs new consent.
1412
+ let snapshot = await readSnapshot({ method: 'GET' });
1413
+ if (!current()) return;
1414
+ if (action) {
1415
+ if (snapshot.token !== token || !maintenanceCanAction(snapshot, action)) {
1416
+ accept(snapshot);
1417
+ update({ error: '状态已发生变化或操作被阻止,未执行操作。请检查当前状态并重新打开操作并确认。' });
1418
+ return;
1419
+ }
1420
+ update({ snapshot: { ...snapshot, canApply: false, canRestoreOnly: false, canRestore: false, token: null } });
1421
+ snapshot = await readSnapshot({ method: 'POST', headers: { 'Content-Type': 'application/json' },
1422
+ body: JSON.stringify({ action, token, confirmation: MAINTENANCE_CONFIRMATION }) });
1423
+ if (!current()) return;
1424
+ }
1425
+ accept(snapshot);
1426
+ } catch {
1427
+ if (current()) update({ snapshot: null,
1428
+ error: action ? '未能确认维护结果:连接中断、请求被拒绝或响应无效不代表成功。请刷新检查。' : '状态检查失败,请确认连接后刷新重试。' });
1429
+ } finally { if (current()) update({ busy: false }); }
1430
+ }
1431
+ const operate = (action, checked) => checked === true && Object.hasOwn(ACTION_CAPABILITIES, action) ? run(action) : Promise.resolve();
1432
+ return {
1433
+ getSnapshot: () => state,
1434
+ subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); },
1435
+ activate: () => { disposed = false; generation++; update({ busy: false }); },
1436
+ refresh: () => run(),
1437
+ operate,
1438
+ apply: checked => operate('apply', checked),
1439
+ restoreOnly: checked => operate('restore-only', checked),
1440
+ restore: checked => operate('restore-and-uninstall', checked),
1441
+ dispose: () => { disposed = true; generation++; listeners.clear(); },
1442
+ };
1443
+ }
1444
+
1445
+ function ModelGroupsSettingsPage() {
1446
+ const h = react.createElement;
1447
+ const controller = react.useMemo(() => createMaintenanceController(), []);
1448
+ const state = react.useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
1449
+ const [pending, setPending] = react.useState(null);
1450
+ const [copyStatus, setCopyStatus] = react.useState('');
1451
+ const confirmationId = react.useId();
1452
+ react.useEffect(() => { controller.activate(); controller.refresh(); return () => controller.dispose(); }, [controller]);
1453
+ const snapshot = state.snapshot;
1454
+ const phaseLabels = { idle: '检查完成,可选择操作', applied: '多密钥已应用(磁盘状态)', restored: '多密钥已关闭,插件仍保留(磁盘状态)', removing: '正在卸载,尚未确认完成', removed: '已确认关闭多密钥并卸载插件', failed: '维护失败,未确认完成' };
1455
+ const updatesAvailable = snapshot?.patches.some(patch => patch.updateAvailable) ?? false;
1456
+ const statusLabel = snapshot?.phase === 'idle'
1457
+ ? snapshot.patches.length && snapshot.patches.every(patch => patch.state === 'patched') ? '多密钥已启用(磁盘状态)'
1458
+ : snapshot.patches.length && snapshot.patches.every(patch => patch.state === 'pristine') ? '多密钥已关闭(磁盘状态)' : '磁盘状态待处理,请查看操作限制'
1459
+ : phaseLabels[snapshot?.phase];
1460
+ const patchLabels = { patched: '已应用', pristine: '原版', conflict: '冲突(不会强制覆盖)' };
1461
+ const actions = [['apply', updatesAvailable ? '更新多密钥补丁' : '启用多密钥'], ['restore-only', '关闭多密钥'], ['restore-and-uninstall', '关闭并卸载插件']];
1462
+ const anyAllowed = actions.some(([action]) => maintenanceCanAction(snapshot, action));
1463
+ const refresh = () => { setPending(null); controller.refresh(); };
1464
+ const confirm = () => { const action = pending; setPending(null); if (action) controller.operate(action, true); };
1465
+ const copy = async () => {
1466
+ try { await globalThis.navigator.clipboard.writeText(maintenanceDiagnostic(state)); setCopyStatus('已复制脱敏诊断'); }
1467
+ catch { setCopyStatus('无法复制,请检查浏览器剪贴板权限。'); }
1468
+ };
1469
+ return h('section', { className: 'mg-maintenance', 'aria-label': '模型分组设置', 'aria-busy': state.busy },
1470
+ h('h2', null, '模型分组 · 多密钥管理'),
1471
+ h('p', null, '选择操作,再确认一次即可。不会调用 AI,不会显示或删除 API 密钥。'),
1472
+ h('section', { className: 'mg-maintenance-card', 'aria-label': '当前状态' },
1473
+ h('div', { className: 'mg-maintenance-row' }, h('h3', null, '当前状态'), h('button', { type: 'button', disabled: state.busy, onClick: refresh }, state.busy ? '处理中…' : '刷新检查')),
1474
+ h('p', { role: 'status', 'aria-live': 'polite', className: 'mg-maintenance-help' }, state.busy ? '正在检查或执行维护,请勿关闭或重复操作。' : snapshot ? statusLabel : '尚无可验证状态,请刷新检查。'),
1475
+ 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' }, '本进程已实际修改宿主文件,需要重启原引擎并刷新;页面刷新或插件重载不等于进程重启。'),
1478
+ updatesAvailable && h('p', { role: 'status' }, '多密钥补丁有更新可用;确认后将备份、校验并更新已识别的旧补丁。'),
1479
+ snapshot?.phase === 'removed' && h('p', { role: 'status' }, '卸载已确认:仅清理本插件的两个浏览器显示偏好,密钥、历史和备份均保留。'),
1480
+ snapshot?.warnings.length > 0 && h('p', { className: 'mg-maintenance-warning', role: 'status' }, '部分使用情况未验证,关闭前请阅读提示;检查提醒不会额外禁用操作。'),
1481
+ 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
+ state.cleanupWarning && h('p', { role: 'status' }, '卸载已确认,但浏览器阻止清理本插件的显示偏好;其他数据未受影响。')),
1483
+ h('section', { className: 'mg-maintenance-card', 'aria-label': '多密钥操作' },
1484
+ h('h3', null, '多密钥操作'),
1485
+ 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))),
1486
+ !anyAllowed && h('p', null, '请刷新检查或处理上方操作限制。检查提醒本身不会禁用按钮。'),
1487
+ pending && h('div', { role: 'dialog', 'aria-labelledby': confirmationId, 'aria-describedby': `${confirmationId}-help`, className: 'mg-maintenance-dialog' },
1488
+ h('h3', { id: confirmationId }, `确认${actions.find(([action]) => action === pending)[1]}?`),
1489
+ h('p', { id: `${confirmationId}-help` }, pending === 'apply' ? '将备份、校验并修改宿主文件以启用多密钥。不会自动换服务或模型。请保存草稿、停止任务,完成后重启原引擎。' : '将校验并恢复宿主原版,关闭多密钥。密钥、配置和历史保留,不自动换服务或模型;旧多密钥选择和配置可能影响后续使用或重启。请保存草稿、停止任务,完成后重启原引擎。'),
1490
+ h('p', null, '请先停止所有任务。部分使用情况未能检查不等于安全;检查提醒不会替代您的判断。'),
1491
+ pending === 'restore-and-uninstall' && h('p', null, '确认原版恢复成功后才卸载插件;只有确认卸载完成才清理本插件的两个浏览器显示偏好。连接中断不代表成功。'),
1492
+ 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
+ h('details', { className: 'mg-maintenance-card' },
1494
+ h('summary', null, '检查详情与脱敏诊断'),
1495
+ h('p', null, '这里只验证磁盘和安全权限,运行时是否已切换未知。每次确认后会重新检查;状态变化需重新确认。未知文件不会强制覆盖,检查不完整仅提醒;操作仍以各自安全权限为准。'),
1496
+ 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 ? '(更新可用)' : ''}`))),
1498
+ h('button', { type: 'button', onClick: copy }, '复制脱敏诊断'),
1499
+ h('p', { role: 'status' }, copyStatus)));
1500
+ }
1501
+
1261
1502
  const groupPreferences = createGroupPreferences();
1262
1503
  const EMPTY_PROVIDERS_SNAPSHOT = Object.freeze({ status: 'unavailable', value: {} });
1263
1504
 
@@ -1265,6 +1506,10 @@ const inject = ['slots', 'modelDirectories', 'sessions', 'locale', 'remote', 're
1265
1506
  function apply(ctx) {
1266
1507
  installModelSelectStyles();
1267
1508
  installModelGroupsStyles();
1509
+ installSettingsPageStyles();
1510
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
1511
+ name: 'settings.section', id: 'dsh-model-groups', order: 35, label: '模型分组',
1512
+ }, ModelGroupsSettingsPage));
1268
1513
  const providersForm = ctx.configForms?.get('llm-pi-ai');
1269
1514
  let statusDisposed = false;
1270
1515
  let statusNotice;
@@ -1274,11 +1519,11 @@ function apply(ctx) {
1274
1519
  if (statusDisposed || (!status.restartRequired && !status.failed)) return;
1275
1520
  statusNotice = document.createElement('div');
1276
1521
  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;';
1522
+ statusNotice.setAttribute('role', 'status');
1523
+ 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
1524
  statusNotice.textContent = status.failed
1280
- ? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。当前网页暂不可用于编辑模型配置。'
1281
- : '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。当前网页暂停操作,避免新旧模块混用。';
1525
+ ? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。请暂勿编辑模型配置;可到「设置 → 模型分组」检查并安全恢复。'
1526
+ : '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。请暂勿编辑模型配置,避免新旧模块混用;维护入口仍可使用。';
1282
1527
  document.body.appendChild(statusNotice);
1283
1528
  }).catch(() => {});
1284
1529
  ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });