dsh-model-groups 0.3.3 → 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/lib/client.js CHANGED
@@ -294,6 +294,29 @@ const MEASURE_STYLE = {
294
294
  * store/verbs) + the standard locale seat.
295
295
  * @returns the trigger and, while open, the two-level menu.
296
296
  */
297
+ // Lay out the entire cascade together: positioning a child alone can put it
298
+ // over an ancestor when neither side has room for another full-size card.
299
+ function layoutModelFlyouts(rect, viewportWidth, viewportHeight, count) {
300
+ const margin = Math.min(12, Math.max(0, viewportWidth / 8));
301
+ const available = Math.max(1, viewportWidth - margin * 2);
302
+ const preferredWidth = rect.width > 0 ? rect.width : 280;
303
+ const width = Math.min(preferredWidth, available / count);
304
+ const height = Math.min(rect.height > 0 ? rect.height : 360, Math.max(1, viewportHeight - margin * 2));
305
+ const leftRoom = rect.left - margin;
306
+ const rightRoom = viewportWidth - margin - (rect.left + preferredWidth);
307
+ const direction = leftRoom >= width * (count - 1) ? -1
308
+ : rightRoom >= width * (count - 1) ? 1 : leftRoom >= rightRoom ? -1 : 1;
309
+ const desiredLeft = direction < 0 ? rect.left - width * (count - 1) : rect.left;
310
+ const start = Math.max(margin, Math.min(desiredLeft, viewportWidth - margin - width * count));
311
+ const top = Math.max(margin, Math.min(rect.top, viewportHeight - margin - height));
312
+ return Array.from({ length: count }, (_, index) => ({
313
+ left: start + width * (direction < 0 ? count - 1 - index : index),
314
+ top, width, height,
315
+ // Override the native menu's intrinsic minimum and maximum at narrow widths.
316
+ minWidth: 0, maxWidth: width, maxHeight: height,
317
+ }));
318
+ }
319
+
297
320
  function ModelSelect({ locked, available, directory, load, select, t, renderSlot, providersForm }) {
298
321
  const preferences = react.useSyncExternalStore(groupPreferences.subscribe, groupPreferences.getSnapshot);
299
322
  const configuration = react.useSyncExternalStore(listener => providersForm?.subscribe(listener) ?? (() => {}), () => providersForm?.getSnapshot() ?? EMPTY_PROVIDERS_SNAPSHOT);
@@ -332,75 +355,18 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
332
355
  }
333
356
  };
334
357
 
335
- // 完全读取并匹配官方原版主卡片尺寸(宽、高严格保持与主卡片完全一致)
336
358
  const triggerFlyout = (entry, anchorEl) => {
337
359
  clearFlyoutTimer();
338
360
  if (activeFlyout?.key === entry.key) return;
339
361
  setActiveSubFlyout(null);
340
- const menuEl = menuRef.current;
341
- const menuRect = menuEl ? menuEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
342
- const margin = 12;
343
- const winW = window.innerWidth;
344
- const winH = window.innerHeight;
345
- // 直接读取官方主卡片的实际渲染宽度与高度
346
- const panelW = menuRect.width > 0 ? menuRect.width : 280;
347
- const panelH = menuRect.height > 0 ? menuRect.height : 360;
348
-
349
- // 紧靠主卡片左侧,0px 缝隙严丝合缝
350
- let left = menuRect.left - panelW;
351
- if (left < margin) {
352
- if (winW - menuRect.right >= panelW + margin) {
353
- left = menuRect.right;
354
- } else {
355
- left = Math.max(margin, Math.min(menuRect.left, winW - panelW - margin));
356
- }
357
- }
358
-
359
- // 顶部与官方主卡片绝对平齐,高度与官方主卡片 1:1 等高
360
- let top = menuRect.top;
361
- setActiveFlyout({
362
- key: entry.key,
363
- entry,
364
- pos: {
365
- left: Math.round(left),
366
- top: Math.round(top),
367
- width: Math.round(panelW),
368
- height: Math.round(panelH)
369
- }
370
- });
362
+ const rect = activeFlyout?.anchorRect ?? (menuRef.current ?? anchorEl).getBoundingClientRect();
363
+ setActiveFlyout({ key: entry.key, entry, anchorRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height } });
371
364
  };
372
365
 
373
- const triggerSubFlyout = (childGroup, anchorEl) => {
366
+ const triggerSubFlyout = (childGroup) => {
374
367
  clearFlyoutTimer();
375
368
  if (activeSubFlyout?.key === childGroup.key) return;
376
- const flyoutEl = flyoutRef.current;
377
- const flyoutRect = flyoutEl ? flyoutEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
378
- const margin = 12;
379
- const winW = window.innerWidth;
380
- const winH = window.innerHeight;
381
- const panelW = flyoutRect.width > 0 ? flyoutRect.width : 280;
382
- const panelH = flyoutRect.height > 0 ? flyoutRect.height : 360;
383
-
384
- let left = flyoutRect.left - panelW;
385
- if (left < margin) {
386
- if (winW - flyoutRect.right >= panelW + margin) {
387
- left = flyoutRect.right;
388
- } else {
389
- left = Math.max(margin, Math.min(flyoutRect.left, winW - panelW - margin));
390
- }
391
- }
392
-
393
- let top = flyoutRect.top;
394
- setActiveSubFlyout({
395
- key: childGroup.key,
396
- childGroup,
397
- pos: {
398
- left: Math.round(left),
399
- top: Math.round(top),
400
- width: Math.round(panelW),
401
- height: Math.round(panelH)
402
- }
403
- });
369
+ setActiveSubFlyout({ key: childGroup.key, childGroup });
404
370
  };
405
371
 
406
372
  const scheduleCloseFlyout = (delay = 240) => {
@@ -412,11 +378,14 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
412
378
  };
413
379
 
414
380
  const scheduleCloseSubFlyout = (delay = 240) => {
415
- if (subFlyoutTimerRef.current !== null) clearTimeout(subFlyoutTimerRef.current);
381
+ // Leaving the key panel must not let an older parent-leave timer
382
+ // destroy both the provider and key panels.
383
+ clearFlyoutTimer();
416
384
  subFlyoutTimerRef.current = setTimeout(() => {
417
385
  setActiveSubFlyout(null);
418
386
  }, delay);
419
387
  };
388
+ (0, react.useEffect)(() => () => clearFlyoutTimer(), []);
420
389
  const [toast, setToast] = (0, react.useState)(null);
421
390
  const toastSeq = (0, react.useRef)(0);
422
391
  const rootRef = (0, react.useRef)(null);
@@ -590,6 +559,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
590
559
  state,
591
560
  query
592
561
  ]);
562
+ const cascade = open && pane === "model" && menuMode === "panel" && !searching && activeFlyout
563
+ ? layoutModelFlyouts({ ...activeFlyout.anchorRect, ...(menuPos ?? {}) }, window.innerWidth, window.innerHeight, activeSubFlyout ? 3 : 2)
564
+ : null;
593
565
  if (!available) return null;
594
566
  const show = () => {
595
567
  setSelectionFocus(false);
@@ -853,7 +825,7 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
853
825
  title: searching ? text('搜索时自动展开,清空搜索后恢复', 'Expanded during search; clearing restores your groups') : entry.id || entry.name,
854
826
  onClick: (e) => {
855
827
  if (isFlyoutMode) {
856
- if (isFlyoutActive) setActiveFlyout(null);
828
+ if (isFlyoutActive) { clearFlyoutTimer(); setActiveFlyout(null); setActiveSubFlyout(null); }
857
829
  else triggerFlyout(entry, e.currentTarget);
858
830
  } else {
859
831
  setHighlightedIndex(null);
@@ -936,8 +908,9 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
936
908
  open && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
937
909
  ref: menuRef,
938
910
  id: `${id}-menu`,
939
- className: ModelSelect_module_css_default.menu,
940
- style: menuPos ?? MEASURE_STYLE,
911
+ className: clsx(ModelSelect_module_css_default.menu, cascade && "dsh-mg-cascade-panel"),
912
+ "data-flyout-level": cascade ? "0" : undefined,
913
+ style: cascade?.[0] ?? menuPos ?? MEASURE_STYLE,
941
914
  role: pane === "model" ? "group" : "menu",
942
915
  "aria-label": t("menu.aria"),
943
916
  "aria-busy": state.status === "loading" || busy,
@@ -1126,18 +1099,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
1126
1099
  }, level.key))] })
1127
1100
  ]
1128
1101
  }), document.body),
1129
- open && pane === "model" && menuMode === "panel" && activeFlyout !== null && (0, react_dom.createPortal)(
1102
+ cascade !== null && activeFlyout !== null && (0, react_dom.createPortal)(
1130
1103
  react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1131
1104
  ref: flyoutRef,
1132
- className: ModelSelect_module_css_default.menu,
1133
- style: {
1134
- left: activeFlyout.pos.left,
1135
- top: activeFlyout.pos.top,
1136
- width: activeFlyout.pos.width,
1137
- height: activeFlyout.pos.height,
1138
- position: "fixed",
1139
- margin: 0
1140
- },
1105
+ className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
1106
+ "data-flyout-level": "1",
1107
+ style: { ...cascade[1], position: "fixed", margin: 0 },
1141
1108
  onMouseEnter: clearFlyoutTimer,
1142
1109
  onMouseLeave: () => scheduleCloseFlyout(200),
1143
1110
  onMouseDown: (e) => {
@@ -1176,18 +1143,12 @@ function ModelSelect({ locked, available, directory, load, select, t, renderSlot
1176
1143
  }),
1177
1144
  document.body
1178
1145
  ),
1179
- open && pane === "model" && menuMode === "panel" && activeSubFlyout !== null && (0, react_dom.createPortal)(
1146
+ cascade !== null && activeSubFlyout !== null && (0, react_dom.createPortal)(
1180
1147
  react_jsx_runtime.jsx(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
1181
1148
  ref: subFlyoutRef,
1182
- className: ModelSelect_module_css_default.menu,
1183
- style: {
1184
- left: activeSubFlyout.pos.left,
1185
- top: activeSubFlyout.pos.top,
1186
- width: activeSubFlyout.pos.width,
1187
- height: activeSubFlyout.pos.height,
1188
- position: "fixed",
1189
- margin: 0
1190
- },
1149
+ className: clsx(ModelSelect_module_css_default.menu, "dsh-mg-cascade-panel"),
1150
+ "data-flyout-level": "2",
1151
+ style: { ...cascade[2], position: "fixed", margin: 0 },
1191
1152
  onMouseEnter: clearFlyoutTimer,
1192
1153
  onMouseLeave: () => scheduleCloseSubFlyout(200),
1193
1154
  onMouseDown: (e) => {
@@ -1286,7 +1247,7 @@ class ModelSeatBoundary extends react.Component {
1286
1247
  }
1287
1248
 
1288
1249
 
1289
- const MODEL_GROUPS_CSS = "/* DSH Model Groups: 100% native token-compatible styling */\n\n.dsh-mg-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 4px;\n flex-shrink: 0;\n margin: 2px 0 4px;\n padding: 0 4px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-summary {\n flex: 1;\n min-width: 0;\n padding: 0 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dsh-mg-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n}\n\n.dsh-mg-toolbar button {\n flex: none;\n height: 22px;\n padding: 0 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n line-height: 22px;\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-toolbar button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-toolbar button:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.dsh-mg-groupStart {\n display: block;\n height: 0;\n pointer-events: none;\n}\n\n.dsh-mg-groupHeading {\n border-radius: var(--dsw-radius-md);\n margin: 1px 0;\n}\n\n.dsh-mg-majorHeading {\n position: static;\n}\n\n.dsh-mg-minorHeading {\n position: relative;\n}\n\n.dsh-mg-majorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-minorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-source {\n display: block;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n.dsh-mg-triggerSource {\n color: var(--dsw-alias-label-caption);\n font-size: 13px;\n}\n\n.dsh-mg-groupHeading button {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 32px;\n padding: 4px 7px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n transition: background .12s;\n}\n\n.dsh-mg-groupHeading button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-groupHeading button:disabled {\n cursor: default;\n}\n\n.dsh-mg-groupHeading button:focus-visible,\n.dsh-mg-toolbar button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n}\n\n.dsh-mg-groupArrow {\n flex: none;\n width: 14px;\n height: 14px;\n color: var(--dsw-alias-label-caption);\n transition: transform .12s;\n}\n\n.dsh-mg-groupArrowOpen {\n transform: rotate(90deg);\n}\n\n.dsh-mg-groupName {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-groupCount {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-weight: 400;\n line-height: 16px;\n}\n\n.dsh-mg-currentBadge {\n flex: none;\n color: var(--dsw-alias-state-business-primary);\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n}\n\n.dsh-mg-groups {\n min-height: 0;\n}\n\n.dsh-mg-groups [hidden] {\n display: none !important;\n}\n\n/* ── 飞出卡片:与官方原版菜单 100% 结构一致,剔除 header 伪分层与白色叠加 ── */\n.dsh-mg-flyout-subgroup-list {\n display: flex;\n flex-direction: column;\n}\n\n.dsh-mg-subgroup-item {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-md);\n width: auto;\n min-width: 100%;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n background: 0 0;\n border: none;\n outline: none;\n align-items: center;\n gap: 6px;\n padding: 5px 7px;\n display: flex;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n transition: background .12s;\n}\n\n.dsh-mg-subgroup-item:hover,\n.dsh-mg-subgroup-item[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-subgroup-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-subgroup-count {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-subgroup-arrow {\n flex: none;\n color: var(--dsw-alias-menu-icon, var(--dsw-alias-label-caption));\n}\n\n.dsh-mg-active-heading {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ── 底部模式切换:原生轻量化,无突兀高饱和蓝块 ── */\n.dsh-mg-modebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 6px 2px;\n margin-top: 3px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-mode-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-mode-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-mode-btn[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-active, rgba(255, 255, 255, 0.1));\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n@media (pointer: coarse) {\n .dsh-mg-groupHeading button { min-height: 36px; }\n .dsh-mg-toolbar button { height: 26px; }\n .dsh-mg-mode-btn { min-height: 24px; padding: 2px 8px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dsh-mg-groupArrow { transition: none; }\n}\n\n";
1250
+ const MODEL_GROUPS_CSS = "/* DSH Model Groups: 100% native token-compatible styling */\n\n.dsh-mg-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 4px;\n flex-shrink: 0;\n margin: 2px 0 4px;\n padding: 0 4px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-summary {\n flex: 1;\n min-width: 0;\n padding: 0 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dsh-mg-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: none;\n}\n\n.dsh-mg-toolbar button {\n flex: none;\n height: 22px;\n padding: 0 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n line-height: 22px;\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-toolbar button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-toolbar button:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.dsh-mg-groupStart {\n display: block;\n height: 0;\n pointer-events: none;\n}\n\n.dsh-mg-groupHeading {\n border-radius: var(--dsw-radius-md);\n margin: 1px 0;\n}\n\n.dsh-mg-majorHeading {\n position: static;\n}\n\n.dsh-mg-minorHeading {\n position: relative;\n}\n\n.dsh-mg-majorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-minorContent {\n margin-left: 6px;\n padding-left: 6px;\n border-left: 1px solid var(--dsw-alias-border-l1);\n}\n\n.dsh-mg-source {\n display: block;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 16px;\n}\n\n.dsh-mg-triggerSource {\n color: var(--dsw-alias-label-caption);\n font-size: 13px;\n}\n\n.dsh-mg-groupHeading button {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 32px;\n padding: 4px 7px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n font-weight: 500;\n line-height: 18px;\n text-align: left;\n cursor: pointer;\n transition: background .12s;\n}\n\n.dsh-mg-groupHeading button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-groupHeading button:disabled {\n cursor: default;\n}\n\n.dsh-mg-groupHeading button:focus-visible,\n.dsh-mg-toolbar button:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n}\n\n.dsh-mg-groupArrow {\n flex: none;\n width: 14px;\n height: 14px;\n color: var(--dsw-alias-label-caption);\n transition: transform .12s;\n}\n\n.dsh-mg-groupArrowOpen {\n transform: rotate(90deg);\n}\n\n.dsh-mg-groupName {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-groupCount {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-weight: 400;\n line-height: 16px;\n}\n\n.dsh-mg-currentBadge {\n flex: none;\n color: var(--dsw-alias-state-business-primary);\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n}\n\n.dsh-mg-groups {\n min-height: 0;\n}\n\n.dsh-mg-groups [hidden] {\n display: none !important;\n}\n\n/* ── 飞出卡片:与官方原版菜单 100% 结构一致,剔除 header 伪分层与白色叠加 ── */\n.dsh-mg-flyout-subgroup-list {\n display: flex;\n flex-direction: column;\n}\n\n.dsh-mg-subgroup-item {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-md);\n width: auto;\n min-width: 100%;\n min-height: 34px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n background: 0 0;\n border: none;\n outline: none;\n align-items: center;\n gap: 6px;\n padding: 5px 7px;\n display: flex;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n transition: background .12s;\n}\n\n.dsh-mg-subgroup-item:hover,\n.dsh-mg-subgroup-item[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-mg-subgroup-name {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dsh-mg-subgroup-count {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-subgroup-arrow {\n flex: none;\n color: var(--dsw-alias-menu-icon, var(--dsw-alias-label-caption));\n}\n\n.dsh-mg-active-heading {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\n/* ── 底部模式切换:原生轻量化,无突兀高饱和蓝块 ── */\n.dsh-mg-modebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 6px 2px;\n margin-top: 3px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dsh-mg-mode-btn {\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n font-size: 11px;\n padding: 2px 6px;\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n transition: background .12s, color .12s;\n}\n\n.dsh-mg-mode-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n\n.dsh-mg-mode-btn[data-active=\"true\"] {\n background: var(--dsw-alias-interactive-bg-active, rgba(255, 255, 255, 0.1));\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n@media (pointer: coarse) {\n .dsh-mg-groupHeading button { min-height: 36px; }\n .dsh-mg-toolbar button { height: 26px; }\n .dsh-mg-mode-btn { min-height: 24px; padding: 2px 8px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dsh-mg-groupArrow { transition: none; }\n}\n\n\n/* Cascade columns share the viewport rather than covering their ancestors. */\n.dsh-mg-cascade-panel {\n box-sizing: border-box;\n min-width: 0;\n}\n.dsh-mg-cascade-panel button,\n.dsh-mg-cascade-panel [role=\"menu\"],\n.dsh-mg-cascade-panel .dsh-mg-modebar,\n.dsh-mg-cascade-panel .dsh-mg-toolbar {\n min-width: 0;\n}\n.dsh-mg-cascade-panel .dsh-mg-toolbar,\n.dsh-mg-cascade-panel .dsh-mg-modebar,\n.dsh-mg-cascade-panel .dsh-mg-toolbar-actions {\n flex-wrap: wrap;\n}\n.dsh-mg-cascade-panel .dsh-mg-summary {\n white-space: normal;\n}\n";
1290
1251
  function installModelGroupsStyles() {
1291
1252
  const tagId = 'dsh-model-groups/groups.css';
1292
1253
  if (document.querySelector('style[data-plugin-css="' + tagId + '"]') !== null) return;
@@ -1297,6 +1258,247 @@ function installModelGroupsStyles() {
1297
1258
  document.head.appendChild(style);
1298
1259
  }
1299
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
+
1300
1502
  const groupPreferences = createGroupPreferences();
1301
1503
  const EMPTY_PROVIDERS_SNAPSHOT = Object.freeze({ status: 'unavailable', value: {} });
1302
1504
 
@@ -1304,7 +1506,27 @@ const inject = ['slots', 'modelDirectories', 'sessions', 'locale', 'remote', 're
1304
1506
  function apply(ctx) {
1305
1507
  installModelSelectStyles();
1306
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));
1307
1513
  const providersForm = ctx.configForms?.get('llm-pi-ai');
1514
+ let statusDisposed = false;
1515
+ let statusNotice;
1516
+ if (typeof fetch === 'function') Promise.resolve().then(() => fetch('/dsh-model-groups/status', { credentials: 'same-origin', cache: 'no-store' }))
1517
+ .then(response => { if (!response.ok) throw new Error('status unavailable'); return response.json(); })
1518
+ .then(status => {
1519
+ if (statusDisposed || (!status.restartRequired && !status.failed)) return;
1520
+ statusNotice = document.createElement('div');
1521
+ statusNotice.dataset.plugin = 'dsh-model-groups';
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;';
1524
+ statusNotice.textContent = status.failed
1525
+ ? '多密钥自动补丁未完成。请回到 App 对话查看引擎日志或运行安装诊断,勿强制覆盖。请暂勿编辑模型配置;可到「设置 → 模型分组」检查并安全恢复。'
1526
+ : '多密钥补丁已自动安装。请先保存其他页面的草稿,再到 App 重启原引擎并刷新此网页。请暂勿编辑模型配置,避免新旧模块混用;维护入口仍可使用。';
1527
+ document.body.appendChild(statusNotice);
1528
+ }).catch(() => {});
1529
+ ctx.on('dispose', () => { statusDisposed = true; statusNotice?.remove(); });
1308
1530
  const onStorage = event => {
1309
1531
  if (event.key === STORAGE_KEY || event.key === null) groupPreferences.sync(event.key === null ? null : event.newValue);
1310
1532
  };
@@ -4,6 +4,18 @@
4
4
  "target": "node_modules/@deepseek-ai/dsh-llm-pi-ai/lib/index.js",
5
5
  "baseSha256": "070c839e2c00e2d5f7c5d82307ca3d84dbb2323e46979daf265908751bbe8ae8",
6
6
  "resultSha256": "a2b3bcda38015026ed58074cef0d508bbd2d7cb1423c00331651fcbd1a9f6b55",
7
+ "variants": [
8
+ {
9
+ "source": "0.2.2-rc.1 with tool lookup optimization and credential cancellation",
10
+ "baseSha256": "0a99a2fc903a4285044b09114b3bd20ee1e612f39aa381e4a71b10d48ee9e9f7",
11
+ "resultSha256": "c87f965d9fcc5bbcd386e98ea3b292b05ec7a55d80df1266e18234031b80990e"
12
+ },
13
+ {
14
+ "source": "npm official 0.2.2-rc.1",
15
+ "baseSha256": "1a3ae58f9b4493e15459cc5000ec80f21b5d3f4a9dea6ffb6bc28e5b8817ae8a",
16
+ "resultSha256": "b9416207cc7c27aff40cb2b877c9176e53809eb2f6dc741828c2ed6f6bdf541b"
17
+ }
18
+ ],
7
19
  "replacements": [
8
20
  {
9
21
  "before": "\t...modelFields\n});\n/** A {@link modelProfile} whose id lives in the `modelOverrides` dict key. */\nconst modelOverride = z.object(modelFields);\nconst profile = z.object({\n\tapiKeyEnv: z.string().role(\"credential-ref\"),\n\tdisplayName: z.string(),\n\tapi: z.union(supportedProtocols()),\n\tbaseURL: z.string(),\n",