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/README.md +68 -103
- package/lib/client.js +308 -86
- package/native/pi-ai.patch.json +12 -0
- package/native/settings-models.patch.json +189 -1
- package/package.json +5 -2
- package/scripts/build.mjs +5 -1
- package/scripts/doctor.mjs +39 -0
- package/scripts/native-patches.mjs +18 -6
- package/scripts/setup.mjs +40 -0
- package/src/auto-patch.js +48 -0
- package/src/client.js +20 -0
- package/src/groups.css +20 -0
- package/src/index.js +37 -4
- package/src/maintenance-fallback.js +160 -0
- package/src/maintenance-host.js +250 -0
- package/src/maintenance-state.js +171 -0
- package/src/maintenance.js +164 -0
- package/src/model-select.js +46 -85
- package/src/settings-page.css +21 -0
- package/src/settings-page.js +56 -0
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
|
|
341
|
-
|
|
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
|
|
366
|
+
const triggerSubFlyout = (childGroup) => {
|
|
374
367
|
clearFlyoutTimer();
|
|
375
368
|
if (activeSubFlyout?.key === childGroup.key) return;
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
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
|
};
|
package/native/pi-ai.patch.json
CHANGED
|
@@ -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",
|