dsh-router-core 0.2.0 → 0.3.1
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 +40 -11
- package/docs/screenshot.png +0 -0
- package/docs/suppliers.md +302 -0
- package/lib/client-registry.js +1119 -213
- package/lib/client.js +1119 -213
- package/lib/index.js +1066 -50
- package/lib/types/llm/adapter.d.ts +2 -0
- package/lib/types/router/index.d.ts +34 -5
- package/lib/types/router/usage-store.d.ts +93 -0
- package/lib/types/router/usage-tokens.d.ts +118 -0
- package/lib/types/shared.d.ts +86 -1
- package/lib/types/supplier-config.d.ts +15 -1
- package/package.json +21 -4
- package/src/client/CheckinCard.tsx +180 -0
- package/src/client/CombosTab.tsx +28 -18
- package/src/client/RouterView.tsx +44 -27
- package/src/client/StatsTab.tsx +531 -0
- package/src/client/index.tsx +61 -11
- package/src/client/router.css +688 -35
- package/src/client/settings-nav-icon.ts +55 -0
- package/src/client/settings-section.tsx +21 -0
- package/src/client/workspace-mount.tsx +2 -9
- package/src/index.ts +51 -3
- package/src/llm/adapter.test.ts +108 -0
- package/src/llm/adapter.ts +18 -6
- package/src/router/index.ts +461 -49
- package/src/router/models-cache.test.ts +269 -0
- package/src/router/precise-dispatch.test.ts +198 -0
- package/src/router/usage-store.ts +499 -0
- package/src/router/usage-tokens.ts +327 -0
- package/src/router/usage.test.ts +558 -0
- package/src/shared.ts +90 -1
- package/src/supplier-config.ts +30 -2
- package/src/suppliers/loader.ts +2 -1
- package/src/suppliers/nvidia/plugin.ts +2 -3
- package/src/suppliers/opencode/plugin.ts +2 -3
- package/src/suppliers/openrouter/plugin.ts +2 -3
- package/src/suppliers/registry.ts +7 -2
package/lib/client-registry.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
try {
|
|
4
4
|
if (typeof document != "undefined") {
|
|
5
5
|
var elementStyle = document.createElement("style");
|
|
6
|
-
elementStyle.appendChild(document.createTextNode("/* ============================================================\n dsh-router — sidebar entry\n ============================================================ */\n\n.dshr-entry {\n box-sizing: border-box;\n display: flex;\n width: 100%;\n min-height: 36px;\n align-items: center;\n gap: 10px;\n border: none;\n border-radius: 8px;\n padding: 0 10px;\n color: var(--dsw-alias-label-secondary);\n background: transparent;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.dshr-entry:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshr-entry[data-active] {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-active);\n font-weight: 600;\n}\n\n.dshr-entryIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n\n.dshr-entryIcon svg {\n display: block;\n width: 18px;\n height: 18px;\n}\n\n.dshr-entryLabel {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n[data-dsh-frame][data-sidebar-collapsed] .dshr-entry {\n width: 36px;\n min-height: 36px;\n justify-content: center;\n margin: 0 auto 12px;\n padding: 0;\n border-radius: 50%;\n}\n\n[data-dsh-frame][data-sidebar-collapsed] .dshr-entryLabel {\n display: none;\n}\n\n/* ============================================================\n dsh-router — center-column panel\n ============================================================ */\n\n[data-pane='conversation'] {\n position: relative;\n}\n\n.dshDesktopConversationSurface {\n position: relative;\n}\n\n[data-dsh-router-view] {\n position: absolute;\n z-index: 60;\n inset: 0;\n display: none;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n background: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base));\n}\n\n.dshr-panelView {\n isolation: isolate;\n}\n\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [data-dsh-router-view] {\n display: block;\n}\n\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [data-pane='conversation'] > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [class*='centerCol'] > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) .dshDesktopConversationSurface > :not([data-dsh-router-view]) {\n display: none !important;\n}\n\n/* ============================================================\n dsh-router — panel shell\n ============================================================ */\n\n.dshr-shell {\n --rs-bg: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base));\n --rs-layer-1: var(--dsw-alias-bg-layer-1);\n --rs-layer-2: var(--dsw-alias-bg-layer-2);\n --rs-input: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-2));\n --rs-text: var(--dsw-alias-label-primary);\n --rs-muted: var(--dsw-alias-label-secondary);\n --rs-faint: var(--dsw-alias-label-tertiary);\n --rs-line: var(--dsw-alias-border-l1);\n --rs-line-strong: var(--dsw-alias-border-l2);\n --rs-accent: var(--dsw-alias-state-business-primary);\n --rs-hover: var(--dsw-alias-interactive-bg-hover);\n --rs-danger: var(--dsw-alias-state-error-primary);\n --rs-success: var(--dsw-alias-state-success-primary);\n --rs-ok: var(--rs-success);\n --rs-warn: #9a6a18;\n --rs-mono: var(--ds-font-family-code, 'SFMono-Regular', Consolas, monospace);\n --rs-sans: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);\n box-sizing: border-box;\n display: flex;\n height: 100%;\n min-width: 0;\n min-height: 0;\n flex-direction: column;\n overflow: hidden;\n color: var(--rs-text);\n background: var(--rs-bg);\n font: 13px/1.55 var(--rs-sans);\n -webkit-tap-highlight-color: transparent;\n}\n\n.dshr-shell *,\n.dshr-shell *::before,\n.dshr-shell *::after { box-sizing: border-box; }\n\n.dshr-shell button,\n.dshr-shell input,\n.dshr-shell select,\n.dshr-shell textarea { color: inherit; font: inherit; }\n\n.dshr-shell button:focus-visible,\n.dshr-shell input:focus-visible,\n.dshr-shell select:focus-visible,\n.dshr-shell textarea:focus-visible {\n outline: 2px solid var(--rs-accent);\n outline-offset: 2px;\n}\n\n/* Masthead */\n.dshr-masthead {\n display: flex;\n min-height: 56px;\n flex: 0 0 auto;\n align-items: center;\n gap: 16px;\n padding: 0 18px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-brand {\n display: grid;\n min-width: 0;\n flex: 0 1 auto;\n align-content: center;\n gap: 1px 9px;\n}\n\n.dshr-brandTitle {\n overflow: hidden;\n font-size: 15px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-brandSub {\n overflow: hidden;\n color: var(--rs-faint);\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-statusCluster {\n display: inline-flex;\n min-width: 0;\n flex: 0 0 auto;\n align-items: center;\n gap: 7px;\n margin-left: auto;\n color: var(--rs-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n\n.dshr-statusEndpoint {\n overflow: hidden;\n max-width: 220px;\n color: var(--rs-faint);\n font: 11px var(--rs-mono);\n text-overflow: ellipsis;\n}\n\n.dshr-headerActions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: 10px;\n}\n\n.dshr-refreshedAt {\n color: var(--rs-faint);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.dshr-iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-1);\n cursor: pointer;\n}\n\n.dshr-iconButton:hover { color: var(--rs-text); background: var(--rs-hover); }\n.dshr-iconButton:disabled { opacity: 0.5; cursor: default; }\n\n.dshr-backButton {\n display: inline-flex;\n height: 28px;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-text);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.dshr-backButton:hover { background: var(--rs-hover); }\n.dshr-backButton svg { display: block; }\n\n/* Alert banner */\n.dshr-alert {\n display: flex;\n flex: 0 0 auto;\n align-items: baseline;\n gap: 10px;\n margin: 12px 18px 0;\n padding: 10px 12px;\n border: 1px solid var(--rs-line-strong);\n border-radius: 8px;\n background: var(--rs-layer-1);\n}\n\n.dshr-alert strong { color: var(--rs-danger); font-size: 12px; }\n.dshr-alert span { color: var(--rs-muted); font-size: 12px; }\n\n/* Nav tabs */\n.dshr-nav {\n display: flex;\n flex: 0 0 auto;\n gap: 2px;\n padding: 0 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-nav button {\n position: relative;\n min-height: 42px;\n padding: 0 12px;\n border: none;\n color: var(--rs-muted);\n background: transparent;\n font-size: 13px;\n cursor: pointer;\n}\n\n.dshr-nav button:hover { color: var(--rs-text); }\n\n.dshr-nav button[aria-current='page'] {\n color: var(--rs-text);\n font-weight: 600;\n}\n\n.dshr-nav button[aria-current='page']::after {\n position: absolute;\n right: 0;\n bottom: -1px;\n left: 0;\n height: 2px;\n border-radius: 2px 2px 0 0;\n content: '';\n background: var(--rs-accent);\n}\n\n.dshr-navCount {\n display: inline-block;\n margin-left: 5px;\n padding: 0 6px;\n border-radius: 9px;\n color: var(--rs-faint);\n background: var(--rs-layer-2);\n font-size: 11px;\n line-height: 16px;\n}\n\n/* Content */\n.dshr-content {\n display: flex;\n min-width: 0;\n min-height: 0;\n flex: 1 1 auto;\n flex-direction: column;\n overflow: auto;\n}\n\n.dshr-empty {\n display: flex;\n min-height: 120px;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n font-size: 13px;\n}\n\n/* Accounts table */\n.dshr-tableWrap {\n overflow: auto;\n padding: 12px 18px;\n}\n\n.dshr-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n}\n\n.dshr-table th {\n position: sticky;\n top: 0;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rs-line-strong);\n color: var(--rs-faint);\n background: var(--rs-bg);\n font-size: 11px;\n font-weight: 600;\n text-align: left;\n white-space: nowrap;\n}\n\n.dshr-table td {\n padding: 7px 10px;\n border-bottom: 1px solid var(--rs-line);\n vertical-align: middle;\n}\n\n.dshr-table tbody tr:hover { background: var(--rs-hover); }\n\n.dshr-mono { font-family: var(--rs-mono); font-size: 11px; }\n.dshr-num { text-align: right; }\n.dshr-muted { color: var(--rs-faint); }\n.dshr-reason { color: var(--rs-muted); font-size: 11px; }\n\n.dshr-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 9px;\n font-size: 11px;\n line-height: 16px;\n}\n\n.dshr-badge-ok { color: var(--rs-success); background: color-mix(in srgb, var(--rs-success) 12%, transparent); }\n.dshr-badge-warn { color: var(--rs-warn); background: color-mix(in srgb, var(--rs-warn) 14%, transparent); }\n.dshr-badge-danger { color: var(--rs-danger); background: color-mix(in srgb, var(--rs-danger) 12%, transparent); }\n\n/* Models grid */\n.dshr-modelGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n gap: 10px;\n padding: 14px 18px;\n}\n\n.dshr-modelCard {\n display: flex;\n min-width: 0;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 12px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-1);\n}\n\n.dshr-modelCard:hover { border-color: var(--rs-line-strong); }\n\n.dshr-modelId {\n overflow: hidden;\n font-family: var(--rs-mono);\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-modelCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n/* ============================================================\n dsh-router — 9router 风格 tab 内容(供应商 / 组合 / 端点与密钥)\n ============================================================ */\n\n.dshr-tabBody {\n display: flex;\n flex: 1 1 auto; /* 填满 content;自身滚动 */\n min-width: 0;\n min-height: 0;\n flex-direction: column;\n gap: 14px;\n padding: 14px 18px;\n overflow-y: auto;\n}\n\n.dshr-card {\n flex: none; /* 不参与 flex 伸缩:高度=内容,避免被压缩裁切 */\n min-width: 0;\n border: 1px solid var(--rs-line);\n border-radius: 10px;\n background: var(--rs-layer-1);\n overflow: hidden;\n}\n\n.dshr-cardHead {\n display: flex;\n min-height: 44px;\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-cardTitle {\n font-size: 13px;\n font-weight: 650;\n}\n\n.dshr-cardMeta {\n margin-left: auto;\n font-size: 11px;\n white-space: nowrap;\n}\n\n/* 组合链 */\n.dshr-comboChain {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 14px;\n}\n\n.dshr-comboStep {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dshr-comboArrow {\n color: var(--rs-faint);\n margin-right: 8px;\n}\n\n.dshr-comboSupplier {\n font-family: var(--rs-mono);\n font-size: 12px;\n}\n\n/* 端点与密钥 */\n.dshr-endpointRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n}\n\n.dshr-endpointCode {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n padding: 8px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-2);\n color: var(--rs-text);\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-miniButton {\n display: inline-flex;\n flex: none;\n height: 28px;\n align-items: center;\n justify-content: center;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.dshr-miniButton:hover { color: var(--rs-text); background: var(--rs-hover); }\n\n.dshr-code {\n margin: 0;\n padding: 12px 14px;\n overflow-x: auto;\n color: var(--rs-text);\n background: var(--rs-layer-2);\n font: 12px/1.6 var(--rs-mono);\n white-space: pre-wrap;\n word-break: break-all;\n}\n\n/* ============================================================\n dsh-router — 供应商详情 / 端点与密钥 / 通用控件\n ============================================================ */\n\n/* 详情页头 */\n.dshr-detailHead {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 36px;\n}\n\n.dshr-detailTitle {\n font-size: 14px;\n font-weight: 650;\n}\n\n/* 可点击卡片 */\n.dshr-card-click {\n cursor: pointer;\n transition: border-color .12s ease;\n}\n\n.dshr-card-click:hover { border-color: var(--rs-accent); }\n\n.dshr-cardHint {\n padding: 0 14px 12px;\n}\n\n/* 供应商小卡片(grid 布局,替代整行大卡片) */\n.dshr-supplierGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));\n gap: 12px;\n}\n\n/* 供应商分组(内置 / 插件分开) */\n.dshr-supplierGroup {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-supplierGroup + .dshr-supplierGroup {\n margin-top: 22px;\n}\n\n.dshr-supplierGroupTitle {\n font-size: 12px;\n font-weight: 600;\n color: var(--rs-muted);\n letter-spacing: .04em;\n padding-left: 2px;\n}\n\n.dshr-supplierCard {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n padding: 16px 18px;\n border: 1px solid var(--rs-line);\n border-radius: 12px;\n background: var(--rs-layer-1);\n cursor: pointer;\n transition: border-color .12s ease;\n}\n\n.dshr-supplierCard:hover { border-color: var(--rs-accent); }\n\n.dshr-supplierName {\n font-size: 14px;\n font-weight: 650;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dshr-supplierRow {\n display: flex;\n align-items: center;\n gap: 9px;\n min-width: 0;\n}\n\n.dshr-supplierIcon {\n width: 24px;\n height: 24px;\n flex: none;\n border-radius: 5px;\n object-fit: contain;\n color: var(--rs-faint);\n}\n\n.dshr-supplierMeta {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.dshr-chevron {\n color: var(--rs-faint);\n font-size: 18px;\n line-height: 1;\n margin-left: auto;\n}\n\n/* 登录框 */\n.dshr-loginBox {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n background: var(--rs-layer-2);\n}\n\n.dshr-loginUrl {\n display: block;\n overflow: hidden;\n padding: 8px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-1);\n color: var(--rs-accent);\n font: 11px/1.5 var(--rs-mono);\n text-overflow: ellipsis;\n white-space: nowrap;\n word-break: break-all;\n}\n\n/* 输入框 */\n.dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-1);\n color: var(--rs-text);\n font-size: 12px;\n}\n\n.dshr-input:focus {\n outline: none;\n border-color: var(--rs-accent);\n}\n\n/* 成功提示 */\n.dshr-alert-ok {\n border-color: color-mix(in srgb, var(--rs-ok) 45%, transparent);\n background: color-mix(in srgb, var(--rs-ok) 10%, transparent);\n}\n\n.dshr-createdKey {\n display: block;\n margin: 6px 0;\n padding: 8px 10px;\n border-radius: 6px;\n background: var(--rs-layer-1);\n word-break: break-all;\n}\n\n/* 模型卡片行 */\n.dshr-modelRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.dshr-modelCard-off {\n opacity: .55;\n}\n\n/* 开关 */\n.dshr-toggle {\n position: relative;\n flex: none;\n width: 30px;\n height: 16px;\n border: none;\n border-radius: 8px;\n background: var(--rs-line);\n cursor: pointer;\n padding: 0;\n transition: background .15s ease;\n}\n\n.dshr-toggle-on { background: var(--rs-ok); }\n\n.dshr-toggleKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n transition: transform .15s ease;\n}\n\n.dshr-toggle-on .dshr-toggleKnob { transform: translateX(14px); }\n\n/* 操作按钮组 */\n.dshr-ops {\n display: flex;\n gap: 6px;\n white-space: nowrap;\n}\n\n.dshr-danger { color: var(--rs-danger); }\n.dshr-danger:hover { color: var(--rs-danger); border-color: var(--rs-danger); }\n\n/* ============================================================\n dsh-router — 弹窗 + 按钮 + 整体打磨\n ============================================================ */\n\n/* 弹窗 */\n.dshr-modalOverlay {\n position: fixed;\n z-index: 1000;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: color-mix(in srgb, #000 42%, transparent);\n backdrop-filter: blur(2px);\n animation: dshrFadeIn .12s ease;\n}\n\n.dshr-modal {\n width: min(560px, 100%);\n max-height: min(80vh, 640px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n border: 1px solid var(--rs-line-strong);\n border-radius: 12px;\n background: var(--rs-layer-1);\n box-shadow: 0 20px 50px rgb(0 0 0 / .35), 0 0 0 1px rgb(0 0 0 / .05);\n animation: dshrModalIn .16s ease;\n}\n\n.dshr-modalHead {\n display: flex;\n min-height: 48px;\n flex: 0 0 auto;\n align-items: center;\n gap: 10px;\n padding: 0 16px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-modalTitle {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n font-size: 14px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-modalClose {\n display: inline-flex;\n flex: none;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n cursor: pointer;\n}\n\n.dshr-modalClose:hover { color: var(--rs-text); background: var(--rs-hover); }\n\n.dshr-modalBody {\n min-height: 0;\n overflow: auto;\n padding: 16px;\n}\n\n.dshr-modalActions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 18px;\n}\n\n@keyframes dshrFadeIn { from { opacity: 0; } to { opacity: 1; } }\n@keyframes dshrModalIn { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }\n\n/* 按钮 */\n.dshr-shell .dshr-primaryButton {\n display: inline-flex;\n height: 30px;\n align-items: center;\n justify-content: center;\n padding: 0 14px;\n border: none;\n border-radius: 7px;\n color: #fff;\n background: var(--rs-accent);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition: filter .12s ease;\n}\n\n.dshr-shell .dshr-primaryButton:hover { filter: brightness(1.08); }\n.dshr-shell .dshr-primaryButton:disabled { opacity: .55; cursor: default; }\n\n.dshr-shell .dshr-dangerButton {\n display: inline-flex;\n height: 30px;\n align-items: center;\n justify-content: center;\n padding: 0 14px;\n border: none;\n border-radius: 7px;\n color: #fff;\n background: var(--rs-danger);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition: filter .12s ease;\n}\n\n.dshr-shell .dshr-dangerButton:hover { filter: brightness(1.1); }\n\n/* 登录步骤 */\n.dshr-loginBox {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-loginStep {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n color: var(--rs-muted);\n font-size: 12px;\n line-height: 1.6;\n}\n\n.dshr-loginNum {\n display: inline-flex;\n flex: none;\n width: 18px;\n height: 18px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n font-size: 11px;\n font-weight: 700;\n}\n\n.dshr-loginUrl {\n display: block;\n overflow: hidden;\n padding: 9px 11px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-2);\n color: var(--rs-accent);\n font: 11px/1.5 var(--rs-mono);\n text-overflow: ellipsis;\n white-space: nowrap;\n word-break: break-all;\n}\n\n/* 新 Key 弹窗 */\n.dshr-createdKeyBox {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-createdKey {\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--rs-layer-2);\n word-break: break-all;\n user-select: all;\n}\n\n/* ============================================================\n dsh-router — 整体打磨\n ============================================================ */\n\n/* Masthead: 细腻渐变 + 底部微阴影 */\n.dshr-masthead {\n background: linear-gradient(to bottom, color-mix(in srgb, var(--rs-layer-1) 62%, transparent), transparent);\n}\n\n/* Brand 标题更突出 */\n.dshr-brandTitle {\n letter-spacing: .01em;\n}\n\n/* 卡片:阴影 + hover 微抬升 */\n.dshr-card {\n box-shadow: 0 1px 2px rgb(0 0 0 / .04);\n transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;\n}\n\n.dshr-card-click:hover {\n transform: translateY(-1px);\n border-color: color-mix(in srgb, var(--rs-accent) 55%, var(--rs-line));\n box-shadow: 0 6px 18px rgb(0 0 0 / .10);\n}\n\n/* Tab:悬停背景 + 计数徽标配色 */\n.dshr-nav button {\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-nav button:hover {\n background: var(--rs-hover);\n border-radius: 6px 6px 0 0;\n}\n\n.dshr-navCount {\n background: color-mix(in srgb, var(--rs-accent) 12%, transparent);\n color: var(--rs-accent);\n font-weight: 600;\n}\n\n/* 徽标:更柔和 */\n.dshr-badge {\n box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 12%, transparent);\n}\n\n/* 表格行悬停 + 表头层级 */\n.dshr-table tbody tr { transition: background .1s ease; }\n.dshr-table th { letter-spacing: .02em; }\n\n/* 输入框聚焦 */\n.dshr-input:focus {\n border-color: var(--rs-accent);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--rs-accent) 18%, transparent);\n}\n\n/* 开关微过渡 */\n.dshr-toggle { transition: background .15s ease, box-shadow .15s ease; }\n.dshr-toggle:hover { box-shadow: 0 0 0 3px color-mix(in srgb, var(--rs-accent) 18%, transparent); }\n\n/* 内容区细滚动条 */\n.dshr-content::-webkit-scrollbar,\n.dshr-tableWrap::-webkit-scrollbar,\n.dshr-modalBody::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.dshr-content::-webkit-scrollbar-thumb,\n.dshr-tableWrap::-webkit-scrollbar-thumb,\n.dshr-modalBody::-webkit-scrollbar-thumb {\n border-radius: 4px;\n background: var(--rs-line-strong);\n}\n\n.dshr-content::-webkit-scrollbar-track,\n.dshr-tableWrap::-webkit-scrollbar-track,\n.dshr-modalBody::-webkit-scrollbar-track {\n background: transparent;\n}\n\n/* ============================================================\n dsh-router — 端点与密钥(9router 风格布局)\n ============================================================ */\n\n/* 卡片头图标 */\n.dshr-cardIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 6px;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 12%, transparent);\n}\n\n.dshr-cardAction {\n margin-left: auto;\n}\n\n/* Endpoint 列表 */\n.dshr-endpointList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 12px 14px;\n}\n\n.dshr-endpointRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.dshr-endpointLabel {\n flex: none;\n min-width: 88px;\n padding: 3px 8px;\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-2);\n font: 11px var(--rs-mono);\n text-align: center;\n}\n\n.dshr-endpointRow .dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--rs-muted);\n}\n\n.dshr-endpointNote {\n padding: 2px 2px 0;\n}\n\n/* 图标按钮 */\n.dshr-iconBtn {\n display: inline-flex;\n flex: none;\n width: 30px;\n height: 30px;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n cursor: pointer;\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-iconBtn:hover { color: var(--rs-accent); background: var(--rs-hover); }\n\n.dshr-iconBtn svg { display: block; }\n\n.dshr-iconBtn-sm { width: 26px; height: 26px; }\n\n/* 禁用态必须看得出来:纯图标按钮没有文字可读(不像「刷新中…」),\n 只靠 hover 反馈会让人以为还能点。 */\n.dshr-iconBtn:disabled { opacity: .4; cursor: default; }\n\n.dshr-iconBtn:disabled:hover { color: var(--rs-muted); background: transparent; }\n\n/* Require API key 行 */\n.dshr-requireRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-requireTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshr-requireDesc {\n margin: 2px 0 0;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* 空状态 */\n.dshr-keyEmpty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n padding: 40px 16px 44px;\n}\n\n.dshr-keyEmptyIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n margin-bottom: 6px;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 10%, transparent);\n}\n\n.dshr-keyEmptyTitle {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n}\n\n.dshr-keyEmptyDesc {\n margin: 0 0 8px;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* key 列表(flex 行,非表格) */\n.dshr-keyList {\n display: flex;\n flex-direction: column;\n}\n\n.dshr-keyRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n transition: opacity .12s ease;\n}\n\n.dshr-keyRow:last-child { border-bottom: none; }\n.dshr-keyRow-paused { opacity: .6; }\n\n.dshr-keyInfo {\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.dshr-keyNameRow {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.dshr-keyName {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshr-keyValue {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 2px;\n}\n\n.dshr-keyValue code {\n overflow: hidden;\n color: var(--rs-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-keyCreated {\n margin: 3px 0 0;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-keyPaused { color: var(--rs-warn); }\n\n.dshr-keyOps {\n display: flex;\n flex: none;\n align-items: center;\n gap: 4px;\n}\n\n/* 删除按钮 hover 才明显(9router:group-hover 显示) */\n.dshr-keyDelete {\n color: var(--rs-muted);\n opacity: 0;\n transition: opacity .12s ease, color .12s ease, background .12s ease;\n}\n\n.dshr-keyRow:hover .dshr-keyDelete { opacity: 1; }\n.dshr-keyDelete:focus-visible { opacity: 1; }\n.dshr-keyDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 弹窗表单 */\n.dshr-modalForm {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n\n.dshr-fieldLabel {\n font-size: 12px;\n font-weight: 650;\n color: var(--rs-text);\n margin-top: 4px;\n}\n\n.dshr-warnBox {\n padding: 12px 14px;\n border: 1px solid color-mix(in srgb, var(--rs-warn) 40%, transparent);\n border-radius: 8px;\n background: color-mix(in srgb, var(--rs-warn) 8%, transparent);\n}\n\n.dshr-warnTitle {\n margin: 0 0 4px;\n color: var(--rs-warn);\n font-size: 13px;\n font-weight: 700;\n}\n\n.dshr-warnDesc {\n margin: 0;\n color: var(--rs-muted);\n font-size: 12px;\n}\n\n.dshr-createdRow { padding: 0; }\n\n.dshr-createdRow .dshr-input { color: var(--rs-text); }\n\n.dshr-fullButton { width: 100%; }\n\n/* ============================================================\n dsh-router — 供应商详情(9router 风格)\n ============================================================ */\n\n/* Header */\n.dshr-providerHead {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-backLink {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n gap: 4px;\n border: none;\n padding: 2px 0;\n color: var(--rs-muted);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n transition: color .12s ease;\n}\n\n.dshr-backLink:hover { color: var(--rs-accent); }\n\n.dshr-providerTitleRow {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dshr-providerIcon {\n display: inline-flex;\n flex: none;\n width: 44px;\n height: 44px;\n align-items: center;\n justify-content: center;\n border-radius: 10px;\n}\n\n.dshr-providerImg {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.dshr-providerName {\n margin: 0;\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -.01em;\n}\n\n.dshr-providerCount {\n margin: 2px 0 0;\n color: var(--rs-muted);\n font-size: 12px;\n}\n\n/* 链接池空状态 */\n.dshr-linkEmpty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 36px 16px 40px;\n}\n\n.dshr-linkEmptyIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 10%, transparent);\n}\n\n.dshr-linkEmptyText { text-align: center; }\n\n.dshr-linkEmptyTitle {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n}\n\n.dshr-linkEmptyDesc {\n margin: 2px 0 0;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* 链接列表(9router ConnectionRow 风格行) */\n.dshr-linkList {\n display: flex;\n flex-direction: column;\n}\n\n.dshr-linkRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n transition: background .1s ease;\n}\n\n.dshr-linkRow:last-child { border-bottom: none; }\n.dshr-linkRow:hover { background: var(--rs-hover); }\n.dshr-linkRow[draggable='true'] { cursor: grab; }\n.dshr-linkRow[draggable='true']:active { cursor: grabbing; }\n.dshr-linkRowDragging { opacity: .45; background: var(--rs-hover); }\n\n.dshr-linkGrip {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--rs-muted);\n flex: 0 0 auto;\n cursor: grab;\n padding: 4px 2px;\n user-select: none;\n}\n.dshr-linkGrip:active { cursor: grabbing; }\n.dshr-linkGrip svg { opacity: .65; }\n.dshr-linkRow:hover .dshr-linkGrip svg { opacity: 1; }\n\n.dshr-linkMain {\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.dshr-linkTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.dshr-linkName {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-linkSub {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 3px;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-linkUid { color: var(--rs-faint); }\n.dshr-linkCredits { color: var(--rs-muted); }\n.dshr-linkErr { color: var(--rs-warn); }\n.dshr-linkCooling { color: var(--rs-warn); }\n.dshr-linkDot { color: var(--rs-faint); opacity: .6; }\n\n.dshr-linkOps {\n display: flex;\n flex: none;\n align-items: center;\n}\n\n/* 删除链接 hover 显示 + 红色 */\n.dshr-linkDelete {\n color: var(--rs-muted);\n opacity: 0;\n transition: opacity .12s ease, color .12s ease, background .12s ease;\n}\n\n.dshr-linkRow:hover .dshr-linkDelete { opacity: 1; }\n.dshr-linkDelete:focus-visible { opacity: 1; }\n.dshr-linkDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 模型 chips(9router ModelRow 风格) */\n.dshr-modelChips {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 14px;\n}\n\n.dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n transition: border-color .12s ease, background .12s ease, color .12s ease;\n}\n\n.dshr-modelChip:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChip:disabled { opacity: .6; cursor: default; }\n\n.dshr-modelChipId {\n font-family: var(--rs-mono);\n font-size: 11px;\n}\n\n.dshr-modelChipCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n.dshr-modelChip-off {\n border-style: dashed;\n color: var(--rs-faint);\n opacity: .8;\n}\n\n.dshr-modelChip-off:hover {\n color: var(--rs-accent);\n border-color: var(--rs-accent);\n}\n\n.dshr-modelChipRestore {\n color: var(--rs-accent);\n font-size: 10px;\n}\n\n/* 已禁用区 */\n.dshr-disabledModels {\n width: 100%;\n}\n\n.dshr-disabledLabel {\n margin: 4px 0 0;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-disabledModels .dshr-modelChips {\n padding: 8px 0 0;\n}\n\n/* ============================================================\n dsh-router — 模型 chips 升级(9router 测试/复制 + 批量按钮)\n ============================================================ */\n\n/* 卡片头操作组 */\n.dshr-cardActions {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n/* 连接池策略选择 */\n.dshr-poolStrategy {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n}\n.dshr-select {\n appearance: none;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-surface, transparent);\n color: inherit;\n font-size: 12px;\n padding: 3px 22px 3px 8px;\n cursor: pointer;\n background-image: linear-gradient(45deg, transparent 50%, var(--rs-muted) 50%), linear-gradient(135deg, var(--rs-muted) 50%, transparent 50%);\n background-position: calc(100% - 12px) 55%, calc(100% - 7px) 55%;\n background-size: 5px 5px;\n background-repeat: no-repeat;\n}\n.dshr-select:focus { outline: none; border-color: var(--rs-accent, #4c7dff); }\n\n\n/* 模型 chip 包装(状态边框) */\n.dshr-modelChipWrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-1);\n transition: border-color .12s ease, background .12s ease;\n}\n\n.dshr-modelChipWrap:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChipWrap.dshr-modelChip-ok { border-color: var(--rs-ok); }\n.dshr-modelChipWrap.dshr-modelChip-err { border-color: var(--rs-danger); }\n\n/* chip 本体(点击禁用) */\n.dshr-modelChipWrap .dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 8px 6px 6px;\n border: none;\n border-radius: 8px;\n color: var(--rs-text);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n}\n\n.dshr-modelChipStatus {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n}\n\n.dshr-modelChipWrap.dshr-modelChip-ok .dshr-modelChipStatus { color: var(--rs-ok); }\n.dshr-modelChipWrap.dshr-modelChip-err .dshr-modelChipStatus { color: var(--rs-danger); }\n\n/* chip 右侧操作(hover 显示) */\n.dshr-modelChipOps {\n display: inline-flex;\n align-items: center;\n gap: 0;\n padding-right: 4px;\n opacity: 0;\n transition: opacity .12s ease;\n}\n\n.dshr-modelChipWrap:hover .dshr-modelChipOps,\n.dshr-modelChipOps:focus-within { opacity: 1; }\n\n/* 旧 .dshr-modelChip 独立样式(disabled 区仍用) */\n.dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n transition: border-color .12s ease, background .12s ease, color .12s ease;\n}\n\n.dshr-modelChip:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChip:disabled { opacity: .6; cursor: default; }\n\n.dshr-modelChipId {\n font-family: var(--rs-mono);\n font-size: 11px;\n}\n\n.dshr-modelChipCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n.dshr-modelChip-off {\n border-style: dashed;\n color: var(--rs-faint);\n opacity: .8;\n}\n\n.dshr-modelChip-off:hover {\n color: var(--rs-accent);\n border-color: var(--rs-accent);\n}\n\n.dshr-modelChipRestore {\n color: var(--rs-accent);\n font-size: 10px;\n}\n\n/* 计数徽标跟在标题/操作按钮右侧(模型卡片) */\n.dshr-cardMetaAfter {\n margin-left: 0;\n}\n\n/* 测试 loading 转圈 */\n.dshr-spin {\n display: inline-flex;\n animation: dshrSpin .8s linear infinite;\n}\n\n@keyframes dshrSpin {\n to { transform: rotate(360deg); }\n}\n\n/* 供应商标题行编辑按钮(靠右) */\n.dshr-providerEdit {\n margin-left: auto;\n}\n\n/* 模型搜索栏 */\n.dshr-modelSearch {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 14px 0;\n}\n\n.dshr-modelSearchIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n}\n\n.dshr-modelSearch .dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* 自定义模型删除按钮(hover 红色) */\n.dshr-modelDelete {\n color: var(--rs-muted);\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-modelDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 全局浮窗提示(toast) */\n.dshr-toast {\n position: fixed;\n z-index: 2000;\n left: 50%;\n bottom: 28px;\n transform: translateX(-50%);\n padding: 8px 16px;\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-2);\n border: 1px solid var(--rs-line-strong);\n box-shadow: 0 6px 20px rgb(0 0 0 / .18);\n font-size: 12px;\n font-weight: 500;\n white-space: nowrap;\n animation: dshrToastIn .18s ease;\n}\n\n@keyframes dshrToastIn {\n from { opacity: 0; transform: translateX(-50%) translateY(8px); }\n to { opacity: 1; transform: translateX(-50%) translateY(0); }\n}\n\n/* ============ 组合(9router combos 布局交互) ============ */\n.dshr-comboIntro { overflow: visible; }\n\n.dshr-comboIntroDesc {\n font-size: 12px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dshr-comboIntroBody {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px 14px;\n padding: 10px 14px;\n font-size: 12px;\n color: var(--rs-muted);\n border-top: 1px solid var(--rs-line);\n}\n\n.dshr-comboHeaderKey {\n font-weight: 600;\n color: var(--rs-text);\n background: color-mix(in srgb, var(--rs-success) 14%, transparent);\n border-radius: 4px;\n padding: 1px 6px;\n font-size: 11px;\n}\n\n.dshr-comboHeaderKey-2 {\n margin-left: 4px;\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n}\n\n.dshr-comboList { display: flex; flex-direction: column; gap: 10px; }\n\n.dshr-comboCard { display: flex; flex-direction: column; }\n\n.dshr-comboName {\n font-family: var(--rs-mono);\n font-size: 13px;\n font-weight: 600;\n color: var(--rs-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 200px;\n}\n\n.dshr-comboOps {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n margin-left: 10px;\n}\n\n.dshr-comboOpBtn-danger { color: var(--rs-danger); }\n.dshr-comboOpBtn-danger:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 12%, transparent);\n}\n\n.dshr-comboCardBody {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border-top: 1px solid var(--rs-line);\n}\n\n.dshr-comboChips {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dshr-comboChip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: color-mix(in srgb, var(--rs-text) 6%, transparent);\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n padding: 3px 8px 3px 5px;\n}\n\n.dshr-comboChipNum {\n width: 15px;\n height: 15px;\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 10px;\n font-weight: 600;\n color: var(--rs-primary);\n background: color-mix(in srgb, var(--rs-primary) 12%, transparent);\n}\n\n.dshr-comboChipSupplier {\n font-family: var(--rs-mono);\n font-size: 11px;\n color: var(--rs-text);\n}\n\n.dshr-comboEmptySteps { font-style: italic; font-size: 12px; }\n\n.dshr-comboMore { font-size: 10px; color: var(--rs-muted); }\n\n.dshr-comboStrategySelect {\n width: 84px;\n height: 26px;\n padding: 0 6px;\n font-size: 12px;\n}\n\n.dshr-comboStrategySelect:disabled {\n opacity: .55;\n cursor: not-allowed;\n}\n\n/* 表单 */\n.dshr-formLabel {\n font-size: 12px;\n font-weight: 600;\n color: var(--rs-text);\n margin: 0 0 6px;\n}\n\n.dshr-formHint {\n font-size: 10px;\n color: var(--rs-muted);\n margin: 4px 0 0;\n}\n\n.dshr-comboEmptyStepsBox {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n padding: 20px 0;\n border: 1px dashed color-mix(in srgb, var(--rs-text) 16%, transparent);\n border-radius: 9px;\n color: var(--rs-muted);\n}\n\n.dshr-comboStepList {\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 260px;\n overflow-y: auto;\n}\n\n.dshr-comboStepRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 3px 4px;\n border: 1px solid transparent;\n border-radius: 7px;\n cursor: grab;\n}\n\n.dshr-comboStepRow:active { cursor: grabbing; }\n\n.dshr-comboStepRow-dragging {\n opacity: .45;\n border-color: var(--rs-accent);\n}\n\n.dshr-comboStepRow-over {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 8%, transparent);\n}\n\n.dshr-comboGrip {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n cursor: grab;\n}\n\n.dshr-comboStepNum {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 11px;\n font-weight: 600;\n color: var(--rs-primary);\n background: color-mix(in srgb, var(--rs-primary) 12%, transparent);\n}\n\n.dshr-comboStepSelect { flex: 1; min-width: 0; }\n\n.dshr-comboStepRemove:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n.dshr-comboAddStep {\n width: 100%;\n margin-top: 8px;\n padding: 8px 0;\n border: 1px dashed color-mix(in srgb, var(--rs-text) 20%, transparent);\n border-radius: 9px;\n background: transparent;\n color: var(--rs-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: border-color .12s ease, background .12s ease;\n}\n\n.dshr-comboAddStep:hover {\n border-color: color-mix(in srgb, var(--rs-primary) 50%, transparent);\n background: color-mix(in srgb, var(--rs-primary) 5%, transparent);\n}\n\n/* 组合:已选模型行 + 添加模型挑选列表 */\n.dshr-comboSelectedModel {\n flex: 1;\n min-width: 0;\n font-family: var(--rs-mono);\n font-size: 12px;\n color: var(--rs-text);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-comboPickLabel { margin-top: 12px; }\n\n.dshr-comboModelSearch { margin-bottom: 6px; }\n\n.dshr-comboPickList {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 180px;\n overflow-y: auto;\n}\n\n.dshr-comboPickItem {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n padding: 6px 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rs-text);\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n transition: background .12s ease;\n}\n\n.dshr-comboPickItem:hover { background: var(--rs-hover); }\n\n.dshr-comboPickName {\n flex: 1;\n min-width: 0;\n font-family: var(--rs-mono);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-comboPickEmpty {\n font-size: 12px;\n font-style: italic;\n padding: 8px 2px;\n}\n\n/* 组合:添加模型按供应商分组 */\n.dshr-comboPickGroup { margin-bottom: 6px; }\n\n.dshr-comboPickGroupTitle {\n margin: 0 0 2px;\n padding: 0 8px;\n font-size: 11px;\n font-weight: 600;\n color: var(--rs-faint);\n}\n\n/* 组合表单:左右分栏(左=组合名/策略/已选,右=添加模型) */\n.dshr-comboFormSplit {\n position: relative;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 24px;\n align-items: start;\n}\n\n.dshr-comboFormLeft {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n}\n\n.dshr-comboFormRight {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n max-height: 340px;\n}\n\n.dshr-comboFormRight .dshr-comboPickList {\n flex: 1;\n min-height: 0;\n}\n\n/* 组合编辑弹窗:加宽 */\n.dshr-modal-wide {\n width: min(760px, 100%);\n}\n\n/* 左右分栏之间的分割线 */\n.dshr-comboFormDivider {\n position: absolute;\n top: 12px;\n bottom: 12px;\n left: 50%;\n width: 1px;\n background: var(--rs-line);\n}\n/*$vite$:1*/"));
|
|
6
|
+
elementStyle.appendChild(document.createTextNode("/* ============================================================\n dsh-router — sidebar entry\n ============================================================ */\n\n.dshr-entry {\n box-sizing: border-box;\n display: flex;\n width: 100%;\n min-height: 36px;\n align-items: center;\n gap: 10px;\n border: none;\n border-radius: 8px;\n padding: 0 10px;\n color: var(--dsw-alias-label-secondary);\n background: transparent;\n font-size: 13px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.dshr-entry:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshr-entry[data-active] {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-active);\n font-weight: 600;\n}\n\n.dshr-entryIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n}\n\n.dshr-entryIcon svg {\n display: block;\n width: 18px;\n height: 18px;\n}\n\n.dshr-entryLabel {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n[data-dsh-frame][data-sidebar-collapsed] .dshr-entry {\n width: 36px;\n min-height: 36px;\n justify-content: center;\n margin: 0 auto 12px;\n padding: 0;\n border-radius: 50%;\n}\n\n[data-dsh-frame][data-sidebar-collapsed] .dshr-entryLabel {\n display: none;\n}\n\n/* ============================================================\n dsh-router — 主题变量(设置页 .dshr-settings / 老中心栏 .dshr-shell 共用)\n ============================================================ */\n\n.dshr-settings,\n.dshr-shell {\n --rs-bg: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base));\n --rs-layer-1: var(--dsw-alias-bg-layer-1);\n --rs-layer-2: var(--dsw-alias-bg-layer-2);\n --rs-input: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-2));\n --rs-text: var(--dsw-alias-label-primary);\n --rs-muted: var(--dsw-alias-label-secondary);\n --rs-faint: var(--dsw-alias-label-tertiary);\n --rs-line: var(--dsw-alias-border-l1);\n --rs-line-strong: var(--dsw-alias-border-l2);\n --rs-accent: var(--dsw-alias-state-business-primary);\n --rs-hover: var(--dsw-alias-interactive-bg-hover);\n --rs-danger: var(--dsw-alias-state-error-primary);\n --rs-success: var(--dsw-alias-state-success-primary);\n --rs-ok: var(--rs-success);\n --rs-warn: #9a6a18;\n --rs-mono: var(--ds-font-family-code, 'SFMono-Regular', Consolas, monospace);\n --rs-sans: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);\n box-sizing: border-box;\n min-width: 0;\n color: var(--rs-text);\n font: 13px/1.55 var(--rs-sans);\n -webkit-tap-highlight-color: transparent;\n}\n\n/* ============================================================\n dsh-router — 设置页容器(settings.section 座位)\n ============================================================ */\n\n/* 容器几何(浏览器实测,改之前先量别凭感觉):\n - 内容宽 564px(面板 612 − 左右 padding 24)\n - 外壳 .VOzbGW_options 是 `overflow-y:auto` + `flex:1 1 0%`,高度确定\n (视口 813 时可视高 703~746)\n - 根节点外面套了一层 display:contents(宿主 slot renderer 给的),**没有\n 盒子** —— 但实测 `height:100%` 仍能穿过它解析到外壳高度\n (验过:root 从 1332px 溢出态变成 722px,正好填满容器)\n\n 布局规则:**tab 条固定,只有下面的内容区滚**。\n 所以根节点必须限高(不限高内容会把外壳撑开,变成整页滚),内容区再\n min-height:0 + overflow-y:auto 自己滚。\n 关键字是 min-height:0 —— flex 子项默认 min-height:auto,不给 0 的话\n 子项缩不小,overflow 永远不触发。 */\n.dshr-settings {\n display: flex;\n height: 100%;\n min-height: 0;\n flex-direction: column;\n gap: 14px;\n}\n\n.dshr-settings *,\n.dshr-settings *::before,\n.dshr-settings *::after { box-sizing: border-box; }\n\n/* 主导航 = tab 条。照 DSH 原生的内容区 tab(插件页那套 pbvGtq_tab):\n 容器一条 1px 底部分隔线,激活项用 ::after 画 2px 下划线压在线上。\n 刻意**不用**填充色块——那会跟下面的周期按钮组撞成同一种控件,\n 层级也反了(导航是页面级,周期是筛选级)。 */\n.dshr-tabs {\n display: flex;\n flex: none; /* 固定:内容区滚,tab 条不动 */\n align-items: center;\n gap: 4px;\n border-bottom: 1px solid var(--rs-line);\n /* 右端跟卡片对齐(实测数字,别凭感觉调):\n root = 564;内容区因 padding-right 8px → 内容可视宽 556;\n 卡片再贴着可视区排 → 548。\n tab 条是 .dshr-settings 的直接子项,起点是 root(564),\n 要落到 548 就得让掉 8(padding) + 8(滚动条) = 16px。\n 写成 margin 而不用 padding,是为了不把分隔线画到 padding 区域外。 */\n margin-right: 16px;\n}\n\n.dshr-tab {\n position: relative;\n min-width: 0;\n height: 36px;\n padding: 0 12px;\n border: none;\n border-radius: 0;\n color: var(--rs-muted);\n background: transparent;\n font-size: 13px;\n font-weight: 400;\n white-space: nowrap;\n cursor: pointer;\n transition: color .12s ease;\n}\n\n.dshr-tab:hover { color: var(--rs-text); }\n\n/* 激活:主文字色 + 2px 下划线(bottom:-1px 压在容器的分隔线上)。\n 双类是因为上面有 `.dshr-settings button { color: inherit; font: inherit }` (1,1),\n 单类的 .dshr-tab-on 只有 (1,0),颜色和字号会被吃掉。 */\n.dshr-settings .dshr-tab-on {\n color: var(--rs-text);\n font-size: 13px;\n font-weight: 500;\n}\n\n.dshr-settings .dshr-tab-on::after {\n position: absolute;\n right: 0;\n bottom: -1px;\n left: 0;\n height: 2px;\n border-radius: 2px 2px 0 0;\n content: '';\n background: var(--rs-text);\n}\n\n.dshr-tabBack {\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n}\n\n.dshr-tabBack:hover { color: var(--rs-text); background: var(--rs-hover); }\n\n/* 内容区:吃掉剩余高度并**自己滚**,这样 tab 条永远留在顶部。\n flex:1 + min-height:0 是必须的(见 .dshr-settings 的注释)。 */\n.dshr-settingsBody {\n display: flex;\n min-width: 0;\n min-height: 0;\n flex: 1 1 auto;\n flex-direction: column;\n overflow-y: auto;\n /* 恒定预留滚动条槽位。\n 实测不设它时 clientWidth 在 564(内容短、无滚动条)和 556(内容长、有滚动条)\n 之间跳,卡片宽也跟着 556/548 跳 —— 切 tab 时内容会横向抖一下。\n 不支持 scrollbar-gutter 的浏览器只是退化成「照样占宽」,不会坏。 */\n scrollbar-gutter: stable;\n /* 右内边距:内容别贴到滚动条上。滚动条宽 8px 且占布局宽度\n (offsetWidth 564 vs clientWidth 556),不撑开的话内容右边缘正好压在它上面。 */\n padding-right: 8px;\n}\n\n/* 细滚动条:跟 .dshr-content / .dshr-modalBody 一个套路 */\n.dshr-settingsBody::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.dshr-settingsBody::-webkit-scrollbar-thumb {\n border-radius: 4px;\n background: var(--rs-line-strong);\n}\n\n.dshr-settingsBody::-webkit-scrollbar-track {\n background: transparent;\n}\n\n/* ============================================================\n dsh-router — 老的中心栏劫持骨架(settings 不可用时的回退,非主路径)\n ============================================================ */\n\n[data-pane='conversation'] { position: relative; }\n.dshDesktopConversationSurface { position: relative; }\n\n[data-dsh-router-view] {\n position: absolute;\n z-index: 60;\n inset: 0;\n display: none;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n background: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-base));\n}\n\n.dshr-panelView { isolation: isolate; }\n\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [data-dsh-router-view] {\n display: block;\n}\n\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [data-pane='conversation'] > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) [class*='centerCol'] > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]):not([data-dsh-mnemon-active]) .dshDesktopConversationSurface > :not([data-dsh-router-view]) {\n display: none !important;\n}\n\n.dshr-shell {\n display: flex;\n height: 100%;\n min-height: 0;\n flex-direction: column;\n overflow: hidden;\n background: var(--rs-bg);\n}\n\n.dshr-settings *,\n.dshr-settings *::before,\n.dshr-settings *::after,\n.dshr-shell *,\n.dshr-shell *::before,\n.dshr-shell *::after { box-sizing: border-box; }\n\n/* 按钮/表单重置 + 聚焦环:两个挂载容器都要覆盖。\n 只写 .dshr-shell 的话设置页整套控件样式全掉(踩过:周期按钮变裸文字)。 */\n.dshr-settings button,\n.dshr-settings input,\n.dshr-settings select,\n.dshr-settings textarea,\n.dshr-shell button,\n.dshr-shell input,\n.dshr-shell select,\n.dshr-shell textarea { color: inherit; font: inherit; }\n\n.dshr-settings button:focus-visible,\n.dshr-settings input:focus-visible,\n.dshr-settings select:focus-visible,\n.dshr-settings textarea:focus-visible,\n.dshr-shell button:focus-visible,\n.dshr-shell input:focus-visible,\n.dshr-shell select:focus-visible,\n.dshr-shell textarea:focus-visible {\n outline: 2px solid var(--rs-accent);\n outline-offset: 2px;\n}\n\n/* Masthead */\n.dshr-masthead {\n display: flex;\n min-height: 56px;\n flex: 0 0 auto;\n align-items: center;\n gap: 16px;\n padding: 0 18px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-brand {\n display: grid;\n min-width: 0;\n flex: 0 1 auto;\n align-content: center;\n gap: 1px 9px;\n}\n\n.dshr-brandTitle {\n overflow: hidden;\n font-size: 15px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-brandSub {\n overflow: hidden;\n color: var(--rs-faint);\n font-size: 10px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-statusCluster {\n display: inline-flex;\n min-width: 0;\n flex: 0 0 auto;\n align-items: center;\n gap: 7px;\n margin-left: auto;\n color: var(--rs-muted);\n font-size: 12px;\n white-space: nowrap;\n}\n\n.dshr-statusEndpoint {\n overflow: hidden;\n max-width: 220px;\n color: var(--rs-faint);\n font: 11px var(--rs-mono);\n text-overflow: ellipsis;\n}\n\n.dshr-headerActions {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n gap: 10px;\n}\n\n.dshr-refreshedAt {\n color: var(--rs-faint);\n font-size: 11px;\n white-space: nowrap;\n}\n\n.dshr-iconButton {\n display: inline-flex;\n width: 28px;\n height: 28px;\n align-items: center;\n justify-content: center;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-1);\n cursor: pointer;\n}\n\n.dshr-iconButton:hover { color: var(--rs-text); background: var(--rs-hover); }\n.dshr-iconButton:disabled { opacity: 0.5; cursor: default; }\n\n.dshr-backButton {\n display: inline-flex;\n height: 28px;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-text);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.dshr-backButton:hover { background: var(--rs-hover); }\n.dshr-backButton svg { display: block; }\n\n/* Alert banner */\n.dshr-alert {\n display: flex;\n flex: 0 0 auto;\n align-items: baseline;\n gap: 10px;\n margin: 12px 18px 0;\n padding: 10px 12px;\n border: 1px solid var(--rs-line-strong);\n border-radius: 8px;\n background: var(--rs-layer-1);\n}\n\n.dshr-alert strong { color: var(--rs-danger); font-size: 12px; }\n.dshr-alert span { color: var(--rs-muted); font-size: 12px; }\n\n/* Nav tabs */\n.dshr-nav {\n display: flex;\n flex: 0 0 auto;\n gap: 2px;\n padding: 0 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-nav button {\n position: relative;\n min-height: 42px;\n padding: 0 12px;\n border: none;\n color: var(--rs-muted);\n background: transparent;\n font-size: 13px;\n cursor: pointer;\n}\n\n.dshr-nav button:hover { color: var(--rs-text); }\n\n.dshr-nav button[aria-current='page'] {\n color: var(--rs-text);\n font-weight: 600;\n}\n\n.dshr-nav button[aria-current='page']::after {\n position: absolute;\n right: 0;\n bottom: -1px;\n left: 0;\n height: 2px;\n border-radius: 2px 2px 0 0;\n content: '';\n background: var(--rs-accent);\n}\n\n.dshr-navCount {\n display: inline-block;\n margin-left: 5px;\n padding: 0 6px;\n border-radius: 9px;\n color: var(--rs-faint);\n background: var(--rs-layer-2);\n font-size: 11px;\n line-height: 16px;\n}\n\n/* Content */\n.dshr-content {\n display: flex;\n min-width: 0;\n min-height: 0;\n flex: 1 1 auto;\n flex-direction: column;\n overflow: auto;\n}\n\n.dshr-empty {\n display: flex;\n min-height: 120px;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n font-size: 13px;\n}\n\n/* Accounts table */\n.dshr-tableWrap {\n overflow: auto;\n padding: 12px 18px;\n}\n\n.dshr-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n}\n\n.dshr-table th {\n position: sticky;\n top: 0;\n padding: 8px 10px;\n border-bottom: 1px solid var(--rs-line-strong);\n color: var(--rs-faint);\n background: var(--rs-bg);\n font-size: 11px;\n font-weight: 600;\n text-align: left;\n white-space: nowrap;\n}\n\n.dshr-table td {\n padding: 7px 10px;\n border-bottom: 1px solid var(--rs-line);\n vertical-align: middle;\n}\n\n.dshr-table tbody tr:hover { background: var(--rs-hover); }\n\n.dshr-mono { font-family: var(--rs-mono); font-size: 11px; }\n.dshr-num { text-align: right; }\n.dshr-muted { color: var(--rs-faint); }\n.dshr-reason { color: var(--rs-muted); font-size: 11px; }\n\n.dshr-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 9px;\n font-size: 11px;\n line-height: 16px;\n}\n\n.dshr-badge-ok { color: var(--rs-success); background: color-mix(in srgb, var(--rs-success) 12%, transparent); }\n.dshr-badge-warn { color: var(--rs-warn); background: color-mix(in srgb, var(--rs-warn) 14%, transparent); }\n.dshr-badge-danger { color: var(--rs-danger); background: color-mix(in srgb, var(--rs-danger) 12%, transparent); }\n\n/* Models grid */\n.dshr-modelGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n gap: 10px;\n padding: 14px 18px;\n}\n\n/* 设置页里横向 padding 归零:内容区已经贴着容器边,再加 18px 会把卡片挤成\n 1 列、图表也跟着变窄。上下 padding 保留。 */\n.dshr-settings .dshr-modelGrid,\n.dshr-settings .dshr-tableWrap {\n padding-right: 0;\n padding-left: 0;\n}\n\n.dshr-modelCard {\n display: flex;\n min-width: 0;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 10px 12px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-1);\n}\n\n.dshr-modelCard:hover { border-color: var(--rs-line-strong); }\n\n.dshr-modelId {\n overflow: hidden;\n font-family: var(--rs-mono);\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-modelCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n/* ============================================================\n dsh-router — 9router 风格 tab 内容(供应商 / 组合 / 端点与密钥)\n ============================================================ */\n\n/* tab 内容:设置页里**不自己滚** —— 滚动交给 .dshr-settingsBody(tab 条下方\n 那一块)。这里再给 overflow 就会出现嵌套滚动条(内容区一个、tabBody 一个)。\n 老的 .dshr-shell 中心栏挂载方式没有 settingsBody 那层,滚动由它自己承担。 */\n.dshr-tabBody {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 14px;\n padding: 0;\n}\n\n.dshr-shell .dshr-tabBody {\n flex: 1 1 auto;\n min-height: 0;\n padding: 14px 18px;\n overflow-y: auto;\n}\n\n.dshr-card {\n flex: none; /* 不参与 flex 伸缩:高度=内容,避免被压缩裁切 */\n min-width: 0;\n border: 1px solid var(--rs-line);\n border-radius: 10px;\n background: var(--rs-layer-1);\n overflow: hidden;\n}\n\n.dshr-cardHead {\n display: flex;\n min-height: 44px;\n align-items: center;\n gap: 10px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-cardTitle {\n font-size: 13px;\n font-weight: 650;\n}\n\n.dshr-cardMeta {\n margin-left: auto;\n font-size: 11px;\n white-space: nowrap;\n}\n\n/* 组合链 */\n.dshr-comboChain {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 14px;\n}\n\n.dshr-comboStep {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dshr-comboArrow {\n color: var(--rs-faint);\n margin-right: 8px;\n}\n\n.dshr-comboSupplier {\n font-family: var(--rs-mono);\n font-size: 12px;\n}\n\n/* 端点与密钥 */\n.dshr-endpointRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 14px;\n}\n\n.dshr-endpointCode {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n padding: 8px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-2);\n color: var(--rs-text);\n font-size: 12px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-miniButton {\n display: inline-flex;\n flex: none;\n height: 28px;\n align-items: center;\n justify-content: center;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.dshr-miniButton:hover { color: var(--rs-text); background: var(--rs-hover); }\n\n.dshr-code {\n margin: 0;\n padding: 12px 14px;\n overflow-x: auto;\n color: var(--rs-text);\n background: var(--rs-layer-2);\n font: 12px/1.6 var(--rs-mono);\n white-space: pre-wrap;\n word-break: break-all;\n}\n\n/* ============================================================\n dsh-router — 供应商详情 / 端点与密钥 / 通用控件\n ============================================================ */\n\n/* 详情页头 */\n.dshr-detailHead {\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 36px;\n}\n\n.dshr-detailTitle {\n font-size: 14px;\n font-weight: 650;\n}\n\n/* 可点击卡片 */\n.dshr-card-click {\n cursor: pointer;\n transition: border-color .12s ease;\n}\n\n.dshr-card-click:hover { border-color: var(--rs-accent); }\n\n.dshr-cardHint {\n padding: 0 14px 12px;\n}\n\n/* 供应商小卡片(grid 布局,替代整行大卡片) */\n.dshr-supplierGrid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));\n gap: 12px;\n}\n\n/* 供应商分组(内置 / 插件分开) */\n.dshr-supplierGroup {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-supplierGroup + .dshr-supplierGroup {\n margin-top: 22px;\n}\n\n.dshr-supplierGroupTitle {\n font-size: 12px;\n font-weight: 600;\n color: var(--rs-muted);\n letter-spacing: .04em;\n padding-left: 2px;\n}\n\n.dshr-supplierCard {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n padding: 16px 18px;\n border: 1px solid var(--rs-line);\n border-radius: 12px;\n background: var(--rs-layer-1);\n cursor: pointer;\n transition: border-color .12s ease;\n}\n\n.dshr-supplierCard:hover { border-color: var(--rs-accent); }\n\n.dshr-supplierName {\n font-size: 14px;\n font-weight: 650;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dshr-supplierRow {\n display: flex;\n align-items: center;\n gap: 9px;\n min-width: 0;\n}\n\n.dshr-supplierIcon {\n width: 24px;\n height: 24px;\n flex: none;\n border-radius: 5px;\n object-fit: contain;\n color: var(--rs-faint);\n}\n\n.dshr-supplierMeta {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 11px;\n white-space: nowrap;\n}\n\n.dshr-chevron {\n color: var(--rs-faint);\n font-size: 18px;\n line-height: 1;\n margin-left: auto;\n}\n\n/* 登录框 */\n.dshr-loginBox {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n background: var(--rs-layer-2);\n}\n\n.dshr-loginUrl {\n display: block;\n overflow: hidden;\n padding: 8px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-1);\n color: var(--rs-accent);\n font: 11px/1.5 var(--rs-mono);\n text-overflow: ellipsis;\n white-space: nowrap;\n word-break: break-all;\n}\n\n/* 输入框 */\n.dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n height: 30px;\n padding: 0 10px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-layer-1);\n color: var(--rs-text);\n font-size: 12px;\n}\n\n.dshr-input:focus {\n outline: none;\n border-color: var(--rs-accent);\n}\n\n/* 成功提示 */\n.dshr-alert-ok {\n border-color: color-mix(in srgb, var(--rs-ok) 45%, transparent);\n background: color-mix(in srgb, var(--rs-ok) 10%, transparent);\n}\n\n.dshr-createdKey {\n display: block;\n margin: 6px 0;\n padding: 8px 10px;\n border-radius: 6px;\n background: var(--rs-layer-1);\n word-break: break-all;\n}\n\n/* 模型卡片行 */\n.dshr-modelRow {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n\n.dshr-modelCard-off {\n opacity: .55;\n}\n\n/* 开关 */\n.dshr-toggle {\n position: relative;\n flex: none;\n width: 30px;\n height: 16px;\n border: none;\n border-radius: 8px;\n background: var(--rs-line);\n cursor: pointer;\n padding: 0;\n transition: background .15s ease;\n}\n\n.dshr-toggle-on { background: var(--rs-ok); }\n\n.dshr-toggleKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: #fff;\n transition: transform .15s ease;\n}\n\n.dshr-toggle-on .dshr-toggleKnob { transform: translateX(14px); }\n\n/* 操作按钮组 */\n.dshr-ops {\n display: flex;\n gap: 6px;\n white-space: nowrap;\n}\n\n.dshr-danger { color: var(--rs-danger); }\n.dshr-danger:hover { color: var(--rs-danger); border-color: var(--rs-danger); }\n\n/* ============================================================\n dsh-router — 弹窗 + 按钮 + 整体打磨\n ============================================================ */\n\n/* 弹窗 */\n.dshr-modalOverlay {\n position: fixed;\n z-index: 1000;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: color-mix(in srgb, #000 42%, transparent);\n backdrop-filter: blur(2px);\n animation: dshrFadeIn .12s ease;\n}\n\n.dshr-modal {\n width: min(560px, 100%);\n max-height: min(80vh, 640px);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n border: 1px solid var(--rs-line-strong);\n border-radius: 12px;\n background: var(--rs-layer-1);\n box-shadow: 0 20px 50px rgb(0 0 0 / .35), 0 0 0 1px rgb(0 0 0 / .05);\n animation: dshrModalIn .16s ease;\n}\n\n.dshr-modalHead {\n display: flex;\n min-height: 48px;\n flex: 0 0 auto;\n align-items: center;\n gap: 10px;\n padding: 0 16px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-modalTitle {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n font-size: 14px;\n font-weight: 650;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-modalClose {\n display: inline-flex;\n flex: none;\n width: 26px;\n height: 26px;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n cursor: pointer;\n}\n\n.dshr-modalClose:hover { color: var(--rs-text); background: var(--rs-hover); }\n\n.dshr-modalBody {\n min-height: 0;\n overflow: auto;\n padding: 16px;\n}\n\n.dshr-modalActions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 18px;\n}\n\n@keyframes dshrFadeIn { from { opacity: 0; } to { opacity: 1; } }\n@keyframes dshrModalIn { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }\n\n/* 按钮 */\n.dshr-settings .dshr-primaryButton,\n.dshr-shell .dshr-primaryButton {\n display: inline-flex;\n height: 30px;\n align-items: center;\n justify-content: center;\n padding: 0 14px;\n border: none;\n border-radius: 7px;\n color: #fff;\n background: var(--rs-accent);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition: filter .12s ease;\n}\n\n.dshr-settings .dshr-primaryButton:hover,\n.dshr-shell .dshr-primaryButton:hover { filter: brightness(1.08); }\n.dshr-settings .dshr-primaryButton:disabled,\n.dshr-shell .dshr-primaryButton:disabled { opacity: .55; cursor: default; }\n\n.dshr-settings .dshr-dangerButton,\n.dshr-shell .dshr-dangerButton {\n display: inline-flex;\n height: 30px;\n align-items: center;\n justify-content: center;\n padding: 0 14px;\n border: none;\n border-radius: 7px;\n color: #fff;\n background: var(--rs-danger);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n transition: filter .12s ease;\n}\n\n.dshr-settings .dshr-dangerButton:hover,\n.dshr-shell .dshr-dangerButton:hover { filter: brightness(1.1); }\n\n/* 登录步骤 */\n.dshr-loginBox {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-loginStep {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n color: var(--rs-muted);\n font-size: 12px;\n line-height: 1.6;\n}\n\n.dshr-loginNum {\n display: inline-flex;\n flex: none;\n width: 18px;\n height: 18px;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n font-size: 11px;\n font-weight: 700;\n}\n\n.dshr-loginUrl {\n display: block;\n overflow: hidden;\n padding: 9px 11px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-2);\n color: var(--rs-accent);\n font: 11px/1.5 var(--rs-mono);\n text-overflow: ellipsis;\n white-space: nowrap;\n word-break: break-all;\n}\n\n/* 新 Key 弹窗 */\n.dshr-createdKeyBox {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-createdKey {\n padding: 10px 12px;\n border-radius: 8px;\n background: var(--rs-layer-2);\n word-break: break-all;\n user-select: all;\n}\n\n/* ============================================================\n dsh-router — 整体打磨\n ============================================================ */\n\n/* Masthead: 细腻渐变 + 底部微阴影 */\n.dshr-masthead {\n background: linear-gradient(to bottom, color-mix(in srgb, var(--rs-layer-1) 62%, transparent), transparent);\n}\n\n/* Brand 标题更突出 */\n.dshr-brandTitle {\n letter-spacing: .01em;\n}\n\n/* 卡片:阴影 + hover 微抬升 */\n.dshr-card {\n box-shadow: 0 1px 2px rgb(0 0 0 / .04);\n transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;\n}\n\n.dshr-card-click:hover {\n transform: translateY(-1px);\n border-color: color-mix(in srgb, var(--rs-accent) 55%, var(--rs-line));\n box-shadow: 0 6px 18px rgb(0 0 0 / .10);\n}\n\n/* Tab:悬停背景 + 计数徽标配色 */\n.dshr-nav button {\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-nav button:hover {\n background: var(--rs-hover);\n border-radius: 6px 6px 0 0;\n}\n\n.dshr-navCount {\n background: color-mix(in srgb, var(--rs-accent) 12%, transparent);\n color: var(--rs-accent);\n font-weight: 600;\n}\n\n/* 徽标:更柔和 */\n.dshr-badge {\n box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 12%, transparent);\n}\n\n/* 表格行悬停 + 表头层级 */\n.dshr-table tbody tr { transition: background .1s ease; }\n.dshr-table th { letter-spacing: .02em; }\n\n/* 输入框聚焦 */\n.dshr-input:focus {\n border-color: var(--rs-accent);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--rs-accent) 18%, transparent);\n}\n\n/* 开关微过渡 */\n.dshr-toggle { transition: background .15s ease, box-shadow .15s ease; }\n.dshr-toggle:hover { box-shadow: 0 0 0 3px color-mix(in srgb, var(--rs-accent) 18%, transparent); }\n\n/* 内容区细滚动条 */\n.dshr-content::-webkit-scrollbar,\n.dshr-tableWrap::-webkit-scrollbar,\n.dshr-modalBody::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.dshr-content::-webkit-scrollbar-thumb,\n.dshr-tableWrap::-webkit-scrollbar-thumb,\n.dshr-modalBody::-webkit-scrollbar-thumb {\n border-radius: 4px;\n background: var(--rs-line-strong);\n}\n\n.dshr-content::-webkit-scrollbar-track,\n.dshr-tableWrap::-webkit-scrollbar-track,\n.dshr-modalBody::-webkit-scrollbar-track {\n background: transparent;\n}\n\n/* ============================================================\n dsh-router — 端点与密钥(9router 风格布局)\n ============================================================ */\n\n/* 卡片头图标 */\n.dshr-cardIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 6px;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 12%, transparent);\n}\n\n.dshr-cardAction {\n margin-left: auto;\n}\n\n/* Endpoint 列表 */\n.dshr-endpointList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 12px 14px;\n}\n\n.dshr-endpointRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.dshr-endpointLabel {\n flex: none;\n min-width: 88px;\n padding: 3px 8px;\n border-radius: 6px;\n color: var(--rs-muted);\n background: var(--rs-layer-2);\n font: 11px var(--rs-mono);\n text-align: center;\n}\n\n.dshr-endpointRow .dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--rs-muted);\n}\n\n.dshr-endpointNote {\n padding: 2px 2px 0;\n}\n\n/* 图标按钮 */\n.dshr-iconBtn {\n display: inline-flex;\n flex: none;\n width: 30px;\n height: 30px;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n cursor: pointer;\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-iconBtn:hover { color: var(--rs-accent); background: var(--rs-hover); }\n\n.dshr-iconBtn svg { display: block; }\n\n.dshr-iconBtn-sm { width: 26px; height: 26px; }\n\n/* 禁用态必须看得出来:纯图标按钮没有文字可读(不像「刷新中…」),\n 只靠 hover 反馈会让人以为还能点。 */\n.dshr-iconBtn:disabled { opacity: .4; cursor: default; }\n\n.dshr-iconBtn:disabled:hover { color: var(--rs-muted); background: transparent; }\n\n/* Require API key 行 */\n.dshr-requireRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n}\n\n.dshr-requireTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshr-requireDesc {\n margin: 2px 0 0;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* 空状态 */\n.dshr-keyEmpty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n padding: 40px 16px 44px;\n}\n\n.dshr-keyEmptyIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 56px;\n height: 56px;\n margin-bottom: 6px;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 10%, transparent);\n}\n\n.dshr-keyEmptyTitle {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n}\n\n.dshr-keyEmptyDesc {\n margin: 0 0 8px;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* key 列表(flex 行,非表格) */\n.dshr-keyList {\n display: flex;\n flex-direction: column;\n}\n\n.dshr-keyRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n transition: opacity .12s ease;\n}\n\n.dshr-keyRow:last-child { border-bottom: none; }\n.dshr-keyRow-paused { opacity: .6; }\n\n.dshr-keyInfo {\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.dshr-keyNameRow {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.dshr-keyName {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshr-keyValue {\n display: flex;\n align-items: center;\n gap: 2px;\n margin-top: 2px;\n}\n\n.dshr-keyValue code {\n overflow: hidden;\n color: var(--rs-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-keyCreated {\n margin: 3px 0 0;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-keyPaused { color: var(--rs-warn); }\n\n.dshr-keyOps {\n display: flex;\n flex: none;\n align-items: center;\n gap: 4px;\n}\n\n/* 删除按钮 hover 才明显(9router:group-hover 显示) */\n.dshr-keyDelete {\n color: var(--rs-muted);\n opacity: 0;\n transition: opacity .12s ease, color .12s ease, background .12s ease;\n}\n\n.dshr-keyRow:hover .dshr-keyDelete { opacity: 1; }\n.dshr-keyDelete:focus-visible { opacity: 1; }\n.dshr-keyDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 弹窗表单 */\n.dshr-modalForm {\n display: flex;\n flex-direction: column;\n gap: 14px;\n}\n\n.dshr-fieldLabel {\n font-size: 12px;\n font-weight: 650;\n color: var(--rs-text);\n margin-top: 4px;\n}\n\n.dshr-warnBox {\n padding: 12px 14px;\n border: 1px solid color-mix(in srgb, var(--rs-warn) 40%, transparent);\n border-radius: 8px;\n background: color-mix(in srgb, var(--rs-warn) 8%, transparent);\n}\n\n.dshr-warnTitle {\n margin: 0 0 4px;\n color: var(--rs-warn);\n font-size: 13px;\n font-weight: 700;\n}\n\n.dshr-warnDesc {\n margin: 0;\n color: var(--rs-muted);\n font-size: 12px;\n}\n\n.dshr-createdRow { padding: 0; }\n\n.dshr-createdRow .dshr-input { color: var(--rs-text); }\n\n.dshr-fullButton { width: 100%; }\n\n/* ============================================================\n dsh-router — 供应商详情(9router 风格)\n ============================================================ */\n\n/* Header */\n.dshr-providerHead {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-backLink {\n display: inline-flex;\n align-items: center;\n align-self: flex-start;\n gap: 4px;\n border: none;\n padding: 2px 0;\n color: var(--rs-muted);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n transition: color .12s ease;\n}\n\n.dshr-backLink:hover { color: var(--rs-accent); }\n\n.dshr-providerTitleRow {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dshr-providerIcon {\n display: inline-flex;\n flex: none;\n width: 44px;\n height: 44px;\n align-items: center;\n justify-content: center;\n border-radius: 10px;\n}\n\n.dshr-providerImg {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.dshr-providerName {\n margin: 0;\n font-size: 20px;\n font-weight: 700;\n letter-spacing: -.01em;\n}\n\n.dshr-providerCount {\n margin: 2px 0 0;\n color: var(--rs-muted);\n font-size: 12px;\n}\n\n/* 链接池空状态 */\n.dshr-linkEmpty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 36px 16px 40px;\n}\n\n.dshr-linkEmptyIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 52px;\n height: 52px;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 10%, transparent);\n}\n\n.dshr-linkEmptyText { text-align: center; }\n\n.dshr-linkEmptyTitle {\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n}\n\n.dshr-linkEmptyDesc {\n margin: 2px 0 0;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n/* 链接列表(9router ConnectionRow 风格行) */\n.dshr-linkList {\n display: flex;\n flex-direction: column;\n}\n\n.dshr-linkRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-bottom: 1px solid var(--rs-line);\n transition: background .1s ease;\n}\n\n.dshr-linkRow:last-child { border-bottom: none; }\n.dshr-linkRow:hover { background: var(--rs-hover); }\n.dshr-linkRow[draggable='true'] { cursor: grab; }\n.dshr-linkRow[draggable='true']:active { cursor: grabbing; }\n.dshr-linkRowDragging { opacity: .45; background: var(--rs-hover); }\n\n.dshr-linkGrip {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--rs-muted);\n flex: 0 0 auto;\n cursor: grab;\n padding: 4px 2px;\n user-select: none;\n}\n.dshr-linkGrip:active { cursor: grabbing; }\n.dshr-linkGrip svg { opacity: .65; }\n.dshr-linkRow:hover .dshr-linkGrip svg { opacity: 1; }\n\n.dshr-linkMain {\n min-width: 0;\n flex: 1 1 auto;\n}\n\n.dshr-linkTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.dshr-linkName {\n overflow: hidden;\n font-size: 13px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-linkSub {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 3px;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-linkUid { color: var(--rs-faint); }\n.dshr-linkCredits { color: var(--rs-muted); }\n.dshr-linkErr { color: var(--rs-warn); }\n.dshr-linkCooling { color: var(--rs-warn); }\n.dshr-linkDot { color: var(--rs-faint); opacity: .6; }\n\n.dshr-linkOps {\n display: flex;\n flex: none;\n align-items: center;\n}\n\n/* 删除链接 hover 显示 + 红色 */\n.dshr-linkDelete {\n color: var(--rs-muted);\n opacity: 0;\n transition: opacity .12s ease, color .12s ease, background .12s ease;\n}\n\n.dshr-linkRow:hover .dshr-linkDelete { opacity: 1; }\n.dshr-linkDelete:focus-visible { opacity: 1; }\n.dshr-linkDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 模型 chips(9router ModelRow 风格) */\n.dshr-modelChips {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 14px;\n}\n\n.dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n transition: border-color .12s ease, background .12s ease, color .12s ease;\n}\n\n.dshr-modelChip:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChip:disabled { opacity: .6; cursor: default; }\n\n.dshr-modelChipId {\n font-family: var(--rs-mono);\n font-size: 11px;\n}\n\n.dshr-modelChipCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n.dshr-modelChip-off {\n border-style: dashed;\n color: var(--rs-faint);\n opacity: .8;\n}\n\n.dshr-modelChip-off:hover {\n color: var(--rs-accent);\n border-color: var(--rs-accent);\n}\n\n.dshr-modelChipRestore {\n color: var(--rs-accent);\n font-size: 10px;\n}\n\n/* 已禁用区 */\n.dshr-disabledModels {\n width: 100%;\n}\n\n.dshr-disabledLabel {\n margin: 4px 0 0;\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-disabledModels .dshr-modelChips {\n padding: 8px 0 0;\n}\n\n/* ============================================================\n dsh-router — 模型 chips 升级(9router 测试/复制 + 批量按钮)\n ============================================================ */\n\n/* 卡片头操作组 */\n.dshr-cardActions {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n/* 连接池策略选择 */\n.dshr-poolStrategy {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n}\n.dshr-select {\n appearance: none;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n background: var(--rs-surface, transparent);\n color: inherit;\n font-size: 12px;\n padding: 3px 22px 3px 8px;\n cursor: pointer;\n background-image: linear-gradient(45deg, transparent 50%, var(--rs-muted) 50%), linear-gradient(135deg, var(--rs-muted) 50%, transparent 50%);\n background-position: calc(100% - 12px) 55%, calc(100% - 7px) 55%;\n background-size: 5px 5px;\n background-repeat: no-repeat;\n}\n.dshr-select:focus { outline: none; border-color: var(--rs-accent, #4c7dff); }\n\n\n/* 模型 chip 包装(状态边框) */\n.dshr-modelChipWrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-1);\n transition: border-color .12s ease, background .12s ease;\n}\n\n.dshr-modelChipWrap:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChipWrap.dshr-modelChip-ok { border-color: var(--rs-ok); }\n.dshr-modelChipWrap.dshr-modelChip-err { border-color: var(--rs-danger); }\n\n/* chip 本体(点击禁用) */\n.dshr-modelChipWrap .dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 8px 6px 6px;\n border: none;\n border-radius: 8px;\n color: var(--rs-text);\n background: transparent;\n font-size: 12px;\n cursor: pointer;\n}\n\n.dshr-modelChipStatus {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n}\n\n.dshr-modelChipWrap.dshr-modelChip-ok .dshr-modelChipStatus { color: var(--rs-ok); }\n.dshr-modelChipWrap.dshr-modelChip-err .dshr-modelChipStatus { color: var(--rs-danger); }\n\n/* chip 右侧操作(hover 显示) */\n.dshr-modelChipOps {\n display: inline-flex;\n align-items: center;\n gap: 0;\n padding-right: 4px;\n opacity: 0;\n transition: opacity .12s ease;\n}\n\n.dshr-modelChipWrap:hover .dshr-modelChipOps,\n.dshr-modelChipOps:focus-within { opacity: 1; }\n\n/* 旧 .dshr-modelChip 独立样式(disabled 区仍用) */\n.dshr-modelChip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 10px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-1);\n font-size: 12px;\n cursor: pointer;\n transition: border-color .12s ease, background .12s ease, color .12s ease;\n}\n\n.dshr-modelChip:hover {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 6%, transparent);\n}\n\n.dshr-modelChip:disabled { opacity: .6; cursor: default; }\n\n.dshr-modelChipId {\n font-family: var(--rs-mono);\n font-size: 11px;\n}\n\n.dshr-modelChipCtx {\n flex: none;\n color: var(--rs-faint);\n font-size: 10px;\n}\n\n.dshr-modelChip-off {\n border-style: dashed;\n color: var(--rs-faint);\n opacity: .8;\n}\n\n.dshr-modelChip-off:hover {\n color: var(--rs-accent);\n border-color: var(--rs-accent);\n}\n\n.dshr-modelChipRestore {\n color: var(--rs-accent);\n font-size: 10px;\n}\n\n/* 计数徽标跟在标题/操作按钮右侧(模型卡片) */\n.dshr-cardMetaAfter {\n margin-left: 0;\n}\n\n/* 测试 loading 转圈 */\n.dshr-spin {\n display: inline-flex;\n animation: dshrSpin .8s linear infinite;\n}\n\n@keyframes dshrSpin {\n to { transform: rotate(360deg); }\n}\n\n/* 供应商标题行编辑按钮(靠右) */\n.dshr-providerEdit {\n margin-left: auto;\n}\n\n/* 模型搜索栏 */\n.dshr-modelSearch {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 14px 0;\n}\n\n.dshr-modelSearchIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n}\n\n.dshr-modelSearch .dshr-input {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* 自定义模型删除按钮(hover 红色) */\n.dshr-modelDelete {\n color: var(--rs-muted);\n transition: color .12s ease, background .12s ease;\n}\n\n.dshr-modelDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 全局浮窗提示(toast) */\n.dshr-toast {\n position: fixed;\n z-index: 2000;\n left: 50%;\n bottom: 28px;\n transform: translateX(-50%);\n padding: 8px 16px;\n border-radius: 8px;\n color: var(--rs-text);\n background: var(--rs-layer-2);\n border: 1px solid var(--rs-line-strong);\n box-shadow: 0 6px 20px rgb(0 0 0 / .18);\n font-size: 12px;\n font-weight: 500;\n white-space: nowrap;\n animation: dshrToastIn .18s ease;\n}\n\n@keyframes dshrToastIn {\n from { opacity: 0; transform: translateX(-50%) translateY(8px); }\n to { opacity: 1; transform: translateX(-50%) translateY(0); }\n}\n\n/* ============ 组合(9router combos 布局交互) ============ */\n.dshr-comboIntro { overflow: visible; }\n\n.dshr-comboIntroDesc {\n font-size: 12px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dshr-comboIntroBody {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px 14px;\n padding: 10px 14px;\n font-size: 12px;\n color: var(--rs-muted);\n border-top: 1px solid var(--rs-line);\n}\n\n.dshr-comboHeaderKey {\n font-weight: 600;\n color: var(--rs-text);\n background: color-mix(in srgb, var(--rs-success) 14%, transparent);\n border-radius: 4px;\n padding: 1px 6px;\n font-size: 11px;\n}\n\n.dshr-comboHeaderKey-2 {\n margin-left: 4px;\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n}\n\n.dshr-comboList { display: flex; flex-direction: column; gap: 10px; }\n\n.dshr-comboCard { display: flex; flex-direction: column; }\n\n.dshr-comboName {\n font-family: var(--rs-mono);\n font-size: 13px;\n font-weight: 600;\n color: var(--rs-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 200px;\n}\n\n.dshr-comboOps {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n margin-left: 10px;\n}\n\n.dshr-comboOpBtn-danger { color: var(--rs-danger); }\n.dshr-comboOpBtn-danger:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 12%, transparent);\n}\n\n.dshr-comboCardBody {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border-top: 1px solid var(--rs-line);\n}\n\n.dshr-comboChips {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dshr-comboChip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: color-mix(in srgb, var(--rs-text) 6%, transparent);\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n padding: 3px 8px 3px 5px;\n}\n\n.dshr-comboChipNum {\n width: 15px;\n height: 15px;\n border-radius: 50%;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 10px;\n font-weight: 600;\n color: var(--rs-primary);\n background: color-mix(in srgb, var(--rs-primary) 12%, transparent);\n}\n\n.dshr-comboChipSupplier {\n font-family: var(--rs-mono);\n font-size: 11px;\n color: var(--rs-text);\n}\n\n.dshr-comboEmptySteps { font-style: italic; font-size: 12px; }\n\n.dshr-comboMore { font-size: 10px; color: var(--rs-muted); }\n\n.dshr-comboStrategySelect {\n width: 84px;\n height: 26px;\n padding: 0 6px;\n font-size: 12px;\n}\n\n.dshr-comboStrategySelect:disabled {\n opacity: .55;\n cursor: not-allowed;\n}\n\n/* 表单 */\n.dshr-formLabel {\n font-size: 12px;\n font-weight: 600;\n color: var(--rs-text);\n margin: 0 0 6px;\n}\n\n.dshr-formHint {\n font-size: 10px;\n color: var(--rs-muted);\n margin: 4px 0 0;\n}\n\n.dshr-comboEmptyStepsBox {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n padding: 20px 0;\n border: 1px dashed color-mix(in srgb, var(--rs-text) 16%, transparent);\n border-radius: 9px;\n color: var(--rs-muted);\n}\n\n.dshr-comboStepList {\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 260px;\n overflow-y: auto;\n}\n\n.dshr-comboStepRow {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 3px 4px;\n border: 1px solid transparent;\n border-radius: 7px;\n cursor: grab;\n}\n\n.dshr-comboStepRow:active { cursor: grabbing; }\n\n.dshr-comboStepRow-dragging {\n opacity: .45;\n border-color: var(--rs-accent);\n}\n\n.dshr-comboStepRow-over {\n border-color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 8%, transparent);\n}\n\n.dshr-comboGrip {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--rs-faint);\n cursor: grab;\n}\n\n.dshr-comboStepNum {\n width: 20px;\n height: 20px;\n border-radius: 50%;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 11px;\n font-weight: 600;\n color: var(--rs-primary);\n background: color-mix(in srgb, var(--rs-primary) 12%, transparent);\n}\n\n.dshr-comboStepSelect { flex: 1; min-width: 0; }\n\n.dshr-comboStepRemove:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n.dshr-comboAddStep {\n width: 100%;\n margin-top: 8px;\n padding: 8px 0;\n border: 1px dashed color-mix(in srgb, var(--rs-text) 20%, transparent);\n border-radius: 9px;\n background: transparent;\n color: var(--rs-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n transition: border-color .12s ease, background .12s ease;\n}\n\n.dshr-comboAddStep:hover {\n border-color: color-mix(in srgb, var(--rs-primary) 50%, transparent);\n background: color-mix(in srgb, var(--rs-primary) 5%, transparent);\n}\n\n/* 组合:已选模型行 + 添加模型挑选列表 */\n.dshr-comboSelectedModel {\n flex: 1;\n min-width: 0;\n font-family: var(--rs-mono);\n font-size: 12px;\n color: var(--rs-text);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-comboPickLabel { margin-top: 12px; }\n\n.dshr-comboModelSearch { margin-bottom: 6px; }\n\n.dshr-comboPickList {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 180px;\n overflow-y: auto;\n}\n\n.dshr-comboPickItem {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n padding: 6px 8px;\n border: none;\n border-radius: 6px;\n background: transparent;\n color: var(--rs-text);\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n transition: background .12s ease;\n}\n\n.dshr-comboPickItem:hover { background: var(--rs-hover); }\n\n.dshr-comboPickName {\n flex: 1;\n min-width: 0;\n font-family: var(--rs-mono);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-comboPickEmpty {\n font-size: 12px;\n font-style: italic;\n padding: 8px 2px;\n}\n\n/* 组合:添加模型按供应商分组 */\n.dshr-comboPickGroup { margin-bottom: 6px; }\n\n.dshr-comboPickGroupTitle {\n margin: 0 0 2px;\n padding: 0 8px;\n font-size: 11px;\n font-weight: 600;\n color: var(--rs-faint);\n}\n\n/* 组合表单:左右分栏(左=组合名/策略/已选,右=添加模型) */\n.dshr-comboFormSplit {\n position: relative;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 24px;\n align-items: start;\n}\n\n.dshr-comboFormLeft {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n}\n\n.dshr-comboFormRight {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n max-height: 340px;\n}\n\n.dshr-comboFormRight .dshr-comboPickList {\n flex: 1;\n min-height: 0;\n}\n\n/* 组合编辑弹窗:加宽 */\n.dshr-modal-wide {\n width: min(760px, 100%);\n}\n\n/* 左右分栏之间的分割线 */\n.dshr-comboFormDivider {\n position: absolute;\n top: 12px;\n bottom: 12px;\n left: 50%;\n width: 1px;\n background: var(--rs-line);\n}\n\n/* ============================================================\n dsh-router — 概览(用量看板,9router Overview 风格)\n ============================================================ */\n\n/* 周期切换 + 操作条 */\n.dshr-statsBar {\n display: flex;\n flex: none;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n.dshr-periods {\n display: inline-flex;\n gap: 2px;\n padding: 2px;\n border: 1px solid var(--rs-line);\n border-radius: 8px;\n background: var(--rs-layer-2);\n}\n\n/* 周期按钮三态(按钮组:一组互斥的筛选按钮,不是导航 tab)。\n 特异性阶梯(类/伪类数 , 元素数):\n 文件顶部有 `.dshr-settings button { color: inherit; font: inherit }` = (1,1),\n 单类的 .dshr-period (1,0) 会被它打死(白字、600 字重全丢),所以下面每条\n 都必须带挂载容器(.dshr-settings / .dshr-shell)抬到 (2,0) 以上。\n **两个容器都要写**:只写 .dshr-shell 的话设置页里这套样式全掉\n (踩过:周期按钮退化成浏览器默认按钮)。\n 两种 hover 用 :not() 做成互斥,谁赢跟源码顺序无关(免得以后有人挪块就翻车)。 */\n.dshr-settings .dshr-period,\n.dshr-shell .dshr-period {\n min-width: 46px;\n height: 26px;\n padding: 0 10px;\n border: none;\n border-radius: 6px;\n color: var(--rs-muted);\n background: transparent;\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: color .12s ease, background .12s ease, filter .12s ease;\n}\n\n/* 未激活悬停:跟 .dshr-miniButton / .dshr-iconButton 一个套路(主文字色 + --rs-hover 底) */\n.dshr-settings .dshr-period:not(.dshr-period-on):hover,\n.dshr-shell .dshr-period:not(.dshr-period-on):hover {\n color: var(--rs-text);\n background: var(--rs-hover);\n}\n\n/* 激活:强调色块 + 白字。底色用 layer-1 时跟卡片同色,根本看不出选中了哪个 */\n.dshr-settings .dshr-period-on,\n.dshr-shell .dshr-period-on {\n color: #fff;\n background: var(--rs-accent);\n font-weight: 600;\n box-shadow: 0 1px 2px rgb(0 0 0 / .12);\n}\n\n/* 激活态被悬停:跟 .dshr-primaryButton 一样提亮,表示「它还是能点的」 */\n.dshr-settings .dshr-period-on:hover,\n.dshr-shell .dshr-period-on:hover {\n color: #fff;\n background: var(--rs-accent);\n filter: brightness(1.08);\n}\n\n.dshr-statsBarRight {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n.dshr-lifetime {\n font-size: 11px;\n white-space: nowrap;\n}\n\n/* 汇总卡(照 9router OverviewCards 的自适应五宫格) */\n.dshr-statGrid {\n display: grid;\n flex: none;\n grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));\n gap: 10px;\n}\n\n.dshr-statCard {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 3px;\n padding: 12px 14px;\n border: 1px solid var(--rs-line);\n border-radius: 10px;\n background: var(--rs-layer-1);\n box-shadow: 0 1px 2px rgb(0 0 0 / .04);\n}\n\n.dshr-statLabel {\n overflow: hidden;\n color: var(--rs-faint);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: .03em;\n text-overflow: ellipsis;\n text-transform: uppercase;\n white-space: nowrap;\n}\n\n.dshr-statValue {\n overflow: hidden;\n font-size: 22px;\n font-weight: 700;\n letter-spacing: -.01em;\n line-height: 1.2;\n text-overflow: ellipsis;\n}\n\n.dshr-statHint {\n color: var(--rs-faint);\n font-size: 11px;\n}\n\n.dshr-statCard-primary .dshr-statValue { color: var(--rs-accent); }\n.dshr-statCard-ok .dshr-statValue { color: var(--rs-success); }\n.dshr-statCard-warn .dshr-statValue { color: var(--rs-warn); }\n.dshr-statCard-danger .dshr-statValue { color: var(--rs-danger); }\n\n/* 签到卡:跟统计卡同宽同高,但里面是按钮不是数字。\n gap 沿用卡片的 3px(不覆写)—— 之前改成 6px 让这一行比别的高 6px,\n 两行卡片因此不齐(92 vs 98px)。\n 今日已点过 → 边框转成功色,一眼看出「今天点过了」 */\n.dshr-statCard-checkin { gap: 3px; }\n\n.dshr-statCard-done {\n border-color: color-mix(in srgb, var(--rs-success) 45%, transparent);\n}\n\n/* 按钮撑满卡片宽度:卡片本来就窄,居左的小按钮看着像没对齐 */\n.dshr-checkinButton {\n display: inline-flex;\n width: 100%;\n /* 26px ≈ .dshr-statValue 的行高(22px × 1.2),让按钮那一行跟数字的\n 那一行一样高,卡片整体高度才跟邻卡一致 */\n height: 26px;\n align-items: center;\n justify-content: center;\n gap: 5px;\n border: 1px solid var(--rs-line);\n border-radius: 6px;\n color: var(--rs-text);\n background: var(--rs-layer-2);\n font-size: 12px;\n font-weight: 500;\n white-space: nowrap;\n cursor: pointer;\n transition: color .12s ease, background .12s ease, border-color .12s ease;\n}\n\n.dshr-checkinButton:hover:not(:disabled) {\n border-color: var(--rs-accent);\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 8%, transparent);\n}\n\n.dshr-checkinButton:disabled { opacity: .6; cursor: default; }\n\n/* 已点过:按钮转成成功色柔和态(仍可再点) */\n.dshr-statCard-done .dshr-checkinButton {\n border-color: color-mix(in srgb, var(--rs-success) 40%, transparent);\n color: var(--rs-success);\n background: color-mix(in srgb, var(--rs-success) 8%, transparent);\n}\n\n/* 结果/失败都压在 hint 那一行里(卡片行数必须跟邻卡一致):\n 失败时才转红,其余沿用 hint 的弱色,不成功后多出一行 */\n.dshr-statHint-err {\n overflow: hidden;\n color: var(--rs-danger);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* 忙碌态:按钮转成弱色,跟「已点过」的成功色区分开 */\n.dshr-checkinButton-busy {\n color: var(--rs-faint);\n background: var(--rs-layer-2);\n border-color: var(--rs-line);\n}\n\n.dshr-estimateNote {\n margin: 0;\n color: var(--rs-faint);\n font-size: 11px;\n line-height: 1.6;\n}\n\n.dshr-estimateNote code {\n padding: 0 3px;\n border-radius: 3px;\n background: var(--rs-layer-2);\n font-family: var(--rs-mono);\n}\n\n/* 趋势折线图(纯 SVG + HTML 叠加层,不引图表库) */\n.dshr-trendWrap {\n position: relative;\n padding: 14px 14px 10px;\n}\n\n.dshr-trendBusy {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--rs-layer-1) 55%, transparent);\n pointer-events: none;\n}\n\n/* 绘图区:高度跟 SVG 视口一致,叠加层用百分比定位才对得上 */\n.dshr-linePlot {\n position: relative;\n height: 150px;\n cursor: crosshair;\n}\n\n/* 键盘可达(←/→ 逐格):聚焦环跟面板其它控件一致 */\n.dshr-linePlot:focus-visible {\n outline: 2px solid var(--rs-accent);\n outline-offset: 2px;\n}\n\n/* 峰值:给读数一个量级参照;半透明底是怕压在折线上看不清 */\n.dshr-linePeak {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0 4px;\n border-radius: 3px;\n color: var(--rs-faint);\n background: color-mix(in srgb, var(--rs-layer-1) 72%, transparent);\n font-size: 9px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n}\n\n.dshr-lineSvg {\n position: absolute;\n display: block;\n width: 100%;\n height: 100%;\n inset: 0;\n overflow: visible;\n}\n\n.dshr-lineGrid {\n stroke: var(--rs-line);\n stroke-dasharray: 3 3;\n}\n\n.dshr-lineArea {\n fill: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n stroke: none;\n}\n\n.dshr-linePath {\n fill: none;\n stroke: var(--rs-accent);\n stroke-width: 2;\n stroke-linejoin: round;\n stroke-linecap: round;\n}\n\n/* 数值点:空心小圆(SVG 会被横向拉伸,所以画在 HTML 层) */\n.dshr-linePoint {\n position: absolute;\n width: 5px;\n height: 5px;\n border: 1.5px solid var(--rs-accent);\n border-radius: 50%;\n background: var(--rs-layer-1);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n.dshr-lineGuide {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 1px;\n background: var(--rs-line-strong);\n pointer-events: none;\n}\n\n.dshr-lineDot {\n position: absolute;\n width: 9px;\n height: 9px;\n border: 2px solid var(--rs-layer-1);\n border-radius: 50%;\n background: var(--rs-accent);\n box-shadow: 0 0 0 1px var(--rs-accent);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n.dshr-lineLabels {\n position: relative;\n height: 14px;\n margin-top: 5px;\n}\n\n.dshr-lineLabel {\n position: absolute;\n top: 0;\n overflow: visible;\n color: var(--rs-faint);\n font-size: 9px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n transform: translateX(-50%);\n}\n\n/* 选中的时段:标签跟着高亮,不用抬头对读数才知道看的是哪一段 */\n.dshr-lineLabel-on {\n color: var(--rs-accent);\n font-weight: 700;\n}\n\n.dshr-trendReadout {\n display: flex;\n align-items: center;\n gap: 2px;\n min-height: 18px;\n padding-top: 6px;\n font-size: 11px;\n}\n\n.dshr-trendEmpty {\n display: flex;\n height: 150px;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n color: var(--rs-faint);\n font-size: 12px;\n}\n\n.dshr-trendEmptyIcon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 10%, transparent);\n}\n\n/* Top 榜(供应商 / 模型并排,窄屏堆叠) */\n.dshr-rankPair {\n display: grid;\n flex: none;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n align-items: start;\n}\n\n.dshr-rankList,\n.dshr-recentList {\n display: flex;\n flex-direction: column;\n padding: 6px 14px 12px;\n}\n\n.dshr-rankRow,\n.dshr-recentRow {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n padding: 7px 0;\n border-bottom: 1px solid var(--rs-line);\n font-size: 12px;\n}\n\n.dshr-rankRow:last-child,\n.dshr-recentRow:last-child { border-bottom: none; }\n\n/* 表头:不参与 hover,且不画分隔线(视觉上属于卡片头) */\n.dshr-rankHead {\n border-bottom: 1px solid var(--rs-line);\n color: var(--rs-faint);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: .03em;\n text-transform: uppercase;\n}\n\n.dshr-rankRow:not(.dshr-rankHead):hover { background: var(--rs-hover); }\n\n.dshr-rankName {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-rankNum {\n flex: none;\n min-width: 52px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.dshr-rankFail { color: var(--rs-danger); }\n\n.dshr-tokenIn { color: var(--rs-accent); }\n.dshr-tokenOut { color: var(--rs-success); }\n\n/* 最近请求列宽(比 Top 榜宽,要放得下模型名) */\n.dshr-recentTime {\n display: flex;\n flex: none;\n align-items: center;\n gap: 5px;\n width: 78px;\n color: var(--rs-muted);\n font-variant-numeric: tabular-nums;\n}\n\n.dshr-recentModel {\n min-width: 0;\n flex: 1 1 auto;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-recentSupplier {\n flex: none;\n width: 92px;\n overflow: hidden;\n color: var(--rs-muted);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-recentRow-fail .dshr-recentModel { color: var(--rs-danger); }\n\n/* 状态点 */\n.dshr-dot {\n display: inline-block;\n flex: none;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n}\n\n.dshr-dot-ok { background: var(--rs-success); }\n.dshr-dot-fail { background: var(--rs-danger); }\n\n/* ============================================================\n dsh-router — 设置导航图标(换掉宿主的默认齿轮)\n ============================================================ */\n\n/* DSH 0.1.x 的 settings.section 注册只认 id / order / label,图标是宿主按\n 「内置 id 白名单」选的,插件传不了 —— 外部 section 一律拿到通用齿轮。\n settings-nav-icon.ts 给本插件那一行打上标记,这里用 currentColor mask\n 渲染路由字形:跟随宿主导航的 hover/active 配色,也保持 16px 图标节奏。\n 升级路径:等契约长出 icon 字段,删掉 settings-nav-icon.ts 和这一整块。 */\n[data-dsh-router-settings-nav] > svg:first-child {\n display: none;\n}\n\n[data-dsh-router-settings-nav]::before {\n content: '';\n flex: none;\n width: 16px;\n height: 16px;\n background: currentColor;\n -webkit-mask: url(\"data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='black'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'><circle%20cx='5'%20cy='5'%20r='2.4'/><circle%20cx='19'%20cy='5'%20r='2.4'/><circle%20cx='12'%20cy='19'%20r='2.4'/><path%20d='M7.4%205h9.2'/><path%20d='M5%207.4v6.2c0%202.6%203%203.4%204.6%203.8'/><path%20d='M19%207.4v6.2c0%202.6-3%203.4-4.6%203.8'/></svg>\") center / contain no-repeat;\n mask: url(\"data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='black'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'><circle%20cx='5'%20cy='5'%20r='2.4'/><circle%20cx='19'%20cy='5'%20r='2.4'/><circle%20cx='12'%20cy='19'%20r='2.4'/><path%20d='M7.4%205h9.2'/><path%20d='M5%207.4v6.2c0%202.6%203%203.4%204.6%203.8'/><path%20d='M19%207.4v6.2c0%202.6-3%203.4-4.6%203.8'/></svg>\") center / contain no-repeat;\n}\n/*$vite$:1*/"));
|
|
7
7
|
document.head.appendChild(elementStyle);
|
|
8
8
|
}
|
|
9
9
|
} catch (e) {
|
|
@@ -17,14 +17,14 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
var module = { exports: {} };
|
|
18
18
|
var exports = module.exports;
|
|
19
19
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
20
|
-
let react = require("react");
|
|
21
20
|
let react_dom_client = require("react-dom/client");
|
|
21
|
+
let react = require("react");
|
|
22
22
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
23
23
|
//#region src/shared.ts
|
|
24
24
|
/** Active-state attribute on <html> that shows the panel. */
|
|
25
25
|
var ROUTER_ACTIVE_ATTR = "data-dsh-router-active";
|
|
26
26
|
/** The panel's visible title (also the sidebar entry label). */
|
|
27
|
-
var ROUTER_TITLE = "
|
|
27
|
+
var ROUTER_TITLE = "路由";
|
|
28
28
|
/** Same-origin API base the client panel fetches (host half serves it). */
|
|
29
29
|
var ROUTER_API_BASE = "/router/api";
|
|
30
30
|
//#endregion
|
|
@@ -88,7 +88,7 @@ window.__ModuleLoader__.load({
|
|
|
88
88
|
* + 可用模型卡片(flex-wrap chips,禁用模型单独列出可恢复)。
|
|
89
89
|
* 加链接流程由供应商能力驱动(登录链接 / API key / 轮询登录)。
|
|
90
90
|
*/
|
|
91
|
-
function Icon$
|
|
91
|
+
function Icon$4({ d, size = 18 }) {
|
|
92
92
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
93
93
|
viewBox: "0 0 24 24",
|
|
94
94
|
width: size,
|
|
@@ -102,7 +102,7 @@ window.__ModuleLoader__.load({
|
|
|
102
102
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
|
-
var I$
|
|
105
|
+
var I$4 = {
|
|
106
106
|
back: "M19 12H5M12 19l-7-7 7-7",
|
|
107
107
|
link: "M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7",
|
|
108
108
|
add: "M12 5v14M5 12h14",
|
|
@@ -595,8 +595,8 @@ window.__ModuleLoader__.load({
|
|
|
595
595
|
type: "button",
|
|
596
596
|
className: "dshr-backLink",
|
|
597
597
|
onClick: onBack,
|
|
598
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
599
|
-
d: I$
|
|
598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
599
|
+
d: I$4.back,
|
|
600
600
|
size: 16
|
|
601
601
|
}), "返回"]
|
|
602
602
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -608,8 +608,8 @@ window.__ModuleLoader__.load({
|
|
|
608
608
|
className: "dshr-providerImg",
|
|
609
609
|
src: supplier.icon,
|
|
610
610
|
alt: ""
|
|
611
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
612
|
-
d: I$
|
|
611
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
612
|
+
d: I$4.link,
|
|
613
613
|
size: 22
|
|
614
614
|
})
|
|
615
615
|
}),
|
|
@@ -637,8 +637,8 @@ window.__ModuleLoader__.load({
|
|
|
637
637
|
setAliasError("");
|
|
638
638
|
setShowAlias(true);
|
|
639
639
|
},
|
|
640
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
641
|
-
d: I$
|
|
640
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
641
|
+
d: I$4.edit,
|
|
642
642
|
size: 15
|
|
643
643
|
})
|
|
644
644
|
})
|
|
@@ -668,8 +668,8 @@ window.__ModuleLoader__.load({
|
|
|
668
668
|
"aria-label": "刷新积分并探测健康",
|
|
669
669
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
670
670
|
className: refreshing ? "dshr-spin" : void 0,
|
|
671
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
672
|
-
d: I$
|
|
671
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
672
|
+
d: I$4.refresh,
|
|
673
673
|
size: 15
|
|
674
674
|
})
|
|
675
675
|
})
|
|
@@ -683,8 +683,8 @@ window.__ModuleLoader__.load({
|
|
|
683
683
|
"aria-label": "签到所有链接",
|
|
684
684
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
685
685
|
className: checkingIn ? "dshr-spin" : void 0,
|
|
686
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
687
|
-
d: I$
|
|
686
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
687
|
+
d: I$4.checkCircle,
|
|
688
688
|
size: 15
|
|
689
689
|
})
|
|
690
690
|
})
|
|
@@ -694,8 +694,8 @@ window.__ModuleLoader__.load({
|
|
|
694
694
|
className: "dshr-primaryButton",
|
|
695
695
|
onClick: () => void startLogin(),
|
|
696
696
|
disabled: busy,
|
|
697
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
698
|
-
d: I$
|
|
697
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
698
|
+
d: I$4.add,
|
|
699
699
|
size: 14
|
|
700
700
|
}), "添加链接"]
|
|
701
701
|
})
|
|
@@ -709,8 +709,8 @@ window.__ModuleLoader__.load({
|
|
|
709
709
|
children: [
|
|
710
710
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
711
711
|
className: "dshr-linkEmptyIcon",
|
|
712
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
713
|
-
d: I$
|
|
712
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
713
|
+
d: I$4.link,
|
|
714
714
|
size: 28
|
|
715
715
|
})
|
|
716
716
|
}),
|
|
@@ -729,8 +729,8 @@ window.__ModuleLoader__.load({
|
|
|
729
729
|
className: "dshr-primaryButton",
|
|
730
730
|
onClick: () => void startLogin(),
|
|
731
731
|
disabled: busy,
|
|
732
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
733
|
-
d: I$
|
|
732
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
733
|
+
d: I$4.add,
|
|
734
734
|
size: 14
|
|
735
735
|
}), "添加链接"]
|
|
736
736
|
})
|
|
@@ -855,8 +855,8 @@ window.__ModuleLoader__.load({
|
|
|
855
855
|
className: "dshr-iconBtn dshr-iconBtn-sm dshr-linkDelete",
|
|
856
856
|
title: "删除链接",
|
|
857
857
|
onClick: () => setRemoveTarget(account),
|
|
858
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
859
|
-
d: I$
|
|
858
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
859
|
+
d: I$4.delete,
|
|
860
860
|
size: 15
|
|
861
861
|
})
|
|
862
862
|
})
|
|
@@ -900,8 +900,8 @@ window.__ModuleLoader__.load({
|
|
|
900
900
|
setShowAddModel(true);
|
|
901
901
|
},
|
|
902
902
|
disabled: toggling,
|
|
903
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
904
|
-
d: I$
|
|
903
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
904
|
+
d: I$4.add,
|
|
905
905
|
size: 13
|
|
906
906
|
}), "添加模型"]
|
|
907
907
|
}),
|
|
@@ -927,8 +927,8 @@ window.__ModuleLoader__.load({
|
|
|
927
927
|
className: "dshr-modelSearch",
|
|
928
928
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
929
929
|
className: "dshr-modelSearchIcon",
|
|
930
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
931
|
-
d: I$
|
|
930
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
931
|
+
d: I$4.search,
|
|
932
932
|
size: 13
|
|
933
933
|
})
|
|
934
934
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -959,8 +959,8 @@ window.__ModuleLoader__.load({
|
|
|
959
959
|
onClick: () => void toggleModel(model.id, false),
|
|
960
960
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
961
961
|
className: "dshr-modelChipStatus",
|
|
962
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
963
|
-
d: status === "ok" ? I$
|
|
962
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
963
|
+
d: status === "ok" ? I$4.checkCircle : status === "error" ? I$4.cancel : I$4.robot,
|
|
964
964
|
size: 13
|
|
965
965
|
})
|
|
966
966
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -977,8 +977,8 @@ window.__ModuleLoader__.load({
|
|
|
977
977
|
onClick: () => void testModel(model.id),
|
|
978
978
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
979
979
|
className: testingIds.has(model.id) ? "dshr-spin" : void 0,
|
|
980
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
981
|
-
d: testingIds.has(model.id) ? I$
|
|
980
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
981
|
+
d: testingIds.has(model.id) ? I$4.loading : I$4.test,
|
|
982
982
|
size: 13
|
|
983
983
|
})
|
|
984
984
|
})
|
|
@@ -988,8 +988,8 @@ window.__ModuleLoader__.load({
|
|
|
988
988
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
989
989
|
title: "复制模型名",
|
|
990
990
|
onClick: () => void copyModel(model.id),
|
|
991
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
992
|
-
d: copiedModel === model.id ? I$
|
|
991
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
992
|
+
d: copiedModel === model.id ? I$4.check : I$4.copy,
|
|
993
993
|
size: 13
|
|
994
994
|
})
|
|
995
995
|
}),
|
|
@@ -998,8 +998,8 @@ window.__ModuleLoader__.load({
|
|
|
998
998
|
className: "dshr-iconBtn dshr-iconBtn-sm dshr-modelDelete",
|
|
999
999
|
title: "删除自定义模型",
|
|
1000
1000
|
onClick: () => setRemoveModelTarget(model.id),
|
|
1001
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1002
|
-
d: I$
|
|
1001
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
1002
|
+
d: I$4.delete,
|
|
1003
1003
|
size: 13
|
|
1004
1004
|
})
|
|
1005
1005
|
})
|
|
@@ -1365,7 +1365,7 @@ window.__ModuleLoader__.load({
|
|
|
1365
1365
|
* API Endpoint 卡片 + API Keys 卡片(Require API key + flex 行 key 列表 +
|
|
1366
1366
|
* Create/暂停确认/Created 弹窗)。
|
|
1367
1367
|
*/
|
|
1368
|
-
function Icon$
|
|
1368
|
+
function Icon$3({ d, size = 18 }) {
|
|
1369
1369
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1370
1370
|
viewBox: "0 0 24 24",
|
|
1371
1371
|
width: size,
|
|
@@ -1379,7 +1379,7 @@ window.__ModuleLoader__.load({
|
|
|
1379
1379
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
1380
1380
|
});
|
|
1381
1381
|
}
|
|
1382
|
-
var I$
|
|
1382
|
+
var I$3 = {
|
|
1383
1383
|
api: "M4 4h16v16H4zM9 9h6v6H9zM12 9V7M12 17v-2M9 12H7M17 12h-2",
|
|
1384
1384
|
key: "M14 7a4 4 0 1 1-1.4 7.6L9 18H6v-3l4.4-4.4A4 4 0 0 1 14 7zM18 7h.01",
|
|
1385
1385
|
visibility: "M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
|
|
@@ -1528,7 +1528,7 @@ window.__ModuleLoader__.load({
|
|
|
1528
1528
|
className: "dshr-cardHead",
|
|
1529
1529
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1530
1530
|
className: "dshr-cardIcon",
|
|
1531
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1531
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, { d: I$3.api })
|
|
1532
1532
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1533
1533
|
className: "dshr-cardTitle",
|
|
1534
1534
|
children: "API Endpoint"
|
|
@@ -1552,8 +1552,8 @@ window.__ModuleLoader__.load({
|
|
|
1552
1552
|
className: "dshr-iconBtn",
|
|
1553
1553
|
title: "复制",
|
|
1554
1554
|
onClick: () => void copyText(endpointBase, "__endpoint"),
|
|
1555
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1556
|
-
d: copiedKeyId === "__endpoint" ? I$
|
|
1555
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1556
|
+
d: copiedKeyId === "__endpoint" ? I$3.check : I$3.copy,
|
|
1557
1557
|
size: 16
|
|
1558
1558
|
})
|
|
1559
1559
|
})
|
|
@@ -1573,7 +1573,7 @@ window.__ModuleLoader__.load({
|
|
|
1573
1573
|
children: [
|
|
1574
1574
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1575
1575
|
className: "dshr-cardIcon",
|
|
1576
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1576
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, { d: I$3.key })
|
|
1577
1577
|
}),
|
|
1578
1578
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1579
1579
|
className: "dshr-cardTitle",
|
|
@@ -1583,8 +1583,8 @@ window.__ModuleLoader__.load({
|
|
|
1583
1583
|
type: "button",
|
|
1584
1584
|
className: "dshr-primaryButton dshr-cardAction",
|
|
1585
1585
|
onClick: openCreate,
|
|
1586
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1587
|
-
d: I$
|
|
1586
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1587
|
+
d: I$3.add,
|
|
1588
1588
|
size: 14
|
|
1589
1589
|
}), "创建 Key"]
|
|
1590
1590
|
})
|
|
@@ -1619,8 +1619,8 @@ window.__ModuleLoader__.load({
|
|
|
1619
1619
|
children: [
|
|
1620
1620
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1621
1621
|
className: "dshr-keyEmptyIcon",
|
|
1622
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1623
|
-
d: I$
|
|
1622
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1623
|
+
d: I$3.key,
|
|
1624
1624
|
size: 30
|
|
1625
1625
|
})
|
|
1626
1626
|
}),
|
|
@@ -1636,8 +1636,8 @@ window.__ModuleLoader__.load({
|
|
|
1636
1636
|
type: "button",
|
|
1637
1637
|
className: "dshr-primaryButton",
|
|
1638
1638
|
onClick: openCreate,
|
|
1639
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1640
|
-
d: I$
|
|
1639
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1640
|
+
d: I$3.add,
|
|
1641
1641
|
size: 14
|
|
1642
1642
|
}), "创建 Key"]
|
|
1643
1643
|
})
|
|
@@ -1659,8 +1659,8 @@ window.__ModuleLoader__.load({
|
|
|
1659
1659
|
className: "dshr-iconBtn dshr-iconBtn-sm dshr-keyDelete",
|
|
1660
1660
|
title: "删除",
|
|
1661
1661
|
onClick: () => setDeleteTarget(key),
|
|
1662
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1663
|
-
d: I$
|
|
1662
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1663
|
+
d: I$3.delete,
|
|
1664
1664
|
size: 14
|
|
1665
1665
|
})
|
|
1666
1666
|
})]
|
|
@@ -1677,8 +1677,8 @@ window.__ModuleLoader__.load({
|
|
|
1677
1677
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
1678
1678
|
title: visibleKeys.has(key.id) ? "隐藏" : "显示",
|
|
1679
1679
|
onClick: () => toggleVisible(key.id),
|
|
1680
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1681
|
-
d: visibleKeys.has(key.id) ? I$
|
|
1680
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1681
|
+
d: visibleKeys.has(key.id) ? I$3.visibilityOff : I$3.visibility,
|
|
1682
1682
|
size: 14
|
|
1683
1683
|
})
|
|
1684
1684
|
}),
|
|
@@ -1687,8 +1687,8 @@ window.__ModuleLoader__.load({
|
|
|
1687
1687
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
1688
1688
|
title: "复制",
|
|
1689
1689
|
onClick: () => void copyText(key.key, key.id),
|
|
1690
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$
|
|
1691
|
-
d: copiedKeyId === key.id ? I$
|
|
1690
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
|
|
1691
|
+
d: copiedKeyId === key.id ? I$3.check : I$3.copy,
|
|
1692
1692
|
size: 14
|
|
1693
1693
|
})
|
|
1694
1694
|
})
|
|
@@ -1865,7 +1865,7 @@ window.__ModuleLoader__.load({
|
|
|
1865
1865
|
* 组合 = 命名的一组模型(完整名 alias/id),请求 model 传组合名时,
|
|
1866
1866
|
* 按策略(回退/轮询)命中其中一个。可创建/编辑/删除。
|
|
1867
1867
|
*/
|
|
1868
|
-
function Icon({ d, size = 18 }) {
|
|
1868
|
+
function Icon$2({ d, size = 18 }) {
|
|
1869
1869
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1870
1870
|
viewBox: "0 0 24 24",
|
|
1871
1871
|
width: size,
|
|
@@ -1879,7 +1879,7 @@ window.__ModuleLoader__.load({
|
|
|
1879
1879
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
1880
1880
|
});
|
|
1881
1881
|
}
|
|
1882
|
-
var I = {
|
|
1882
|
+
var I$2 = {
|
|
1883
1883
|
layers: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5M3 17l9 5 9-5",
|
|
1884
1884
|
add: "M12 5v14M5 12h14",
|
|
1885
1885
|
copy: "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
|
|
@@ -1890,13 +1890,22 @@ window.__ModuleLoader__.load({
|
|
|
1890
1890
|
search: "M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM21 21l-4.35-4.35"
|
|
1891
1891
|
};
|
|
1892
1892
|
function CombosTab({ combos, aliases, onRefresh }) {
|
|
1893
|
-
/**
|
|
1894
|
-
*
|
|
1895
|
-
*
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1893
|
+
/**
|
|
1894
|
+
* 存储值 → 展示全名 alias/model。
|
|
1895
|
+
* 存储格式 = `supplierId,modelId`:精准,同名模型不会串台。
|
|
1896
|
+
* 兼容旧数据的裸 modelId:查不到归属就显示裸名——不能兜底套第一个供应商的
|
|
1897
|
+
* 前缀,那样前缀是假的。
|
|
1898
|
+
*/
|
|
1899
|
+
const displayName = (stored) => {
|
|
1900
|
+
const comma = stored.indexOf(",");
|
|
1901
|
+
if (comma > 0) {
|
|
1902
|
+
const sid = stored.slice(0, comma);
|
|
1903
|
+
const mid = stored.slice(comma + 1);
|
|
1904
|
+
const alias = groups.find((x) => x.supplier.id === sid)?.supplier.alias ?? aliases.find((a) => a.id === sid)?.alias ?? "";
|
|
1905
|
+
return alias === "" ? mid : `${alias}/${mid}`;
|
|
1906
|
+
}
|
|
1907
|
+
const hit = groups.find((g) => g.models.some((m) => m.id === stored && m.enabled)) ?? groups.find((g) => g.models.some((m) => m.id === stored));
|
|
1908
|
+
return hit === void 0 ? stored : `${hit.supplier.alias}/${stored}`;
|
|
1900
1909
|
};
|
|
1901
1910
|
const [showForm, setShowForm] = (0, react.useState)(false);
|
|
1902
1911
|
const [editing, setEditing] = (0, react.useState)(null);
|
|
@@ -1971,8 +1980,10 @@ window.__ModuleLoader__.load({
|
|
|
1971
1980
|
...patch
|
|
1972
1981
|
}));
|
|
1973
1982
|
};
|
|
1974
|
-
|
|
1975
|
-
|
|
1983
|
+
/** 勾选:存 `supplierId,modelId`(精准归属,展示时才拼 alias)。 */
|
|
1984
|
+
const toggleModel = (supplierId, id) => {
|
|
1985
|
+
const stored = `${supplierId},${id}`;
|
|
1986
|
+
patchForm({ models: form.models.includes(stored) ? form.models.filter((m) => m !== stored) : [...form.models, stored] });
|
|
1976
1987
|
};
|
|
1977
1988
|
const handleDragStart = (index) => {
|
|
1978
1989
|
setDragIndex(index);
|
|
@@ -2090,8 +2101,8 @@ window.__ModuleLoader__.load({
|
|
|
2090
2101
|
children: [
|
|
2091
2102
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2092
2103
|
className: "dshr-cardIcon",
|
|
2093
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2094
|
-
d: I.layers,
|
|
2104
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2105
|
+
d: I$2.layers,
|
|
2095
2106
|
size: 16
|
|
2096
2107
|
})
|
|
2097
2108
|
}),
|
|
@@ -2107,8 +2118,8 @@ window.__ModuleLoader__.load({
|
|
|
2107
2118
|
type: "button",
|
|
2108
2119
|
className: "dshr-primaryButton dshr-cardAction",
|
|
2109
2120
|
onClick: openCreate,
|
|
2110
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2111
|
-
d: I.add,
|
|
2121
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2122
|
+
d: I$2.add,
|
|
2112
2123
|
size: 14
|
|
2113
2124
|
}), "创建组合"]
|
|
2114
2125
|
})
|
|
@@ -2134,8 +2145,8 @@ window.__ModuleLoader__.load({
|
|
|
2134
2145
|
children: [
|
|
2135
2146
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2136
2147
|
className: "dshr-keyEmptyIcon",
|
|
2137
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2138
|
-
d: I.layers,
|
|
2148
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2149
|
+
d: I$2.layers,
|
|
2139
2150
|
size: 30
|
|
2140
2151
|
})
|
|
2141
2152
|
}),
|
|
@@ -2151,8 +2162,8 @@ window.__ModuleLoader__.load({
|
|
|
2151
2162
|
type: "button",
|
|
2152
2163
|
className: "dshr-primaryButton",
|
|
2153
2164
|
onClick: openCreate,
|
|
2154
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2155
|
-
d: I.add,
|
|
2165
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2166
|
+
d: I$2.add,
|
|
2156
2167
|
size: 14
|
|
2157
2168
|
}), "创建组合"]
|
|
2158
2169
|
})
|
|
@@ -2166,8 +2177,8 @@ window.__ModuleLoader__.load({
|
|
|
2166
2177
|
children: [
|
|
2167
2178
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2168
2179
|
className: "dshr-cardIcon",
|
|
2169
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2170
|
-
d: I.layers,
|
|
2180
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2181
|
+
d: I$2.layers,
|
|
2171
2182
|
size: 16
|
|
2172
2183
|
})
|
|
2173
2184
|
}),
|
|
@@ -2200,8 +2211,8 @@ window.__ModuleLoader__.load({
|
|
|
2200
2211
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
2201
2212
|
title: "复制组合名",
|
|
2202
2213
|
onClick: () => void copyName(combo.name, combo.id),
|
|
2203
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2204
|
-
d: copied === combo.id ? I.check : I.copy,
|
|
2214
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2215
|
+
d: copied === combo.id ? I$2.check : I$2.copy,
|
|
2205
2216
|
size: 15
|
|
2206
2217
|
})
|
|
2207
2218
|
}),
|
|
@@ -2210,8 +2221,8 @@ window.__ModuleLoader__.load({
|
|
|
2210
2221
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
2211
2222
|
title: "编辑",
|
|
2212
2223
|
onClick: () => openEdit(combo),
|
|
2213
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2214
|
-
d: I.edit,
|
|
2224
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2225
|
+
d: I$2.edit,
|
|
2215
2226
|
size: 15
|
|
2216
2227
|
})
|
|
2217
2228
|
}),
|
|
@@ -2220,8 +2231,8 @@ window.__ModuleLoader__.load({
|
|
|
2220
2231
|
className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboOpBtn-danger",
|
|
2221
2232
|
title: "删除",
|
|
2222
2233
|
onClick: () => setDeleteTarget(combo),
|
|
2223
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2224
|
-
d: I.delete,
|
|
2234
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2235
|
+
d: I$2.delete,
|
|
2225
2236
|
size: 15
|
|
2226
2237
|
})
|
|
2227
2238
|
})
|
|
@@ -2242,7 +2253,7 @@ window.__ModuleLoader__.load({
|
|
|
2242
2253
|
children: i + 1
|
|
2243
2254
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2244
2255
|
className: "dshr-comboChipSupplier",
|
|
2245
|
-
children:
|
|
2256
|
+
children: displayName(model)
|
|
2246
2257
|
})]
|
|
2247
2258
|
}, `${model}-${i}`)), combo.models.length > 4 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2248
2259
|
className: "dshr-comboMore",
|
|
@@ -2317,8 +2328,8 @@ window.__ModuleLoader__.load({
|
|
|
2317
2328
|
]
|
|
2318
2329
|
}), form.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2319
2330
|
className: "dshr-comboEmptyStepsBox",
|
|
2320
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2321
|
-
d: I.layers,
|
|
2331
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2332
|
+
d: I$2.layers,
|
|
2322
2333
|
size: 20
|
|
2323
2334
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2324
2335
|
className: "dshr-muted",
|
|
@@ -2340,8 +2351,8 @@ window.__ModuleLoader__.load({
|
|
|
2340
2351
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2341
2352
|
className: "dshr-comboGrip",
|
|
2342
2353
|
title: "拖拽排序",
|
|
2343
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2344
|
-
d: I.grip,
|
|
2354
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2355
|
+
d: I$2.grip,
|
|
2345
2356
|
size: 14
|
|
2346
2357
|
})
|
|
2347
2358
|
}),
|
|
@@ -2351,15 +2362,15 @@ window.__ModuleLoader__.load({
|
|
|
2351
2362
|
}),
|
|
2352
2363
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2353
2364
|
className: "dshr-comboSelectedModel",
|
|
2354
|
-
children:
|
|
2365
|
+
children: displayName(model)
|
|
2355
2366
|
}),
|
|
2356
2367
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2357
2368
|
type: "button",
|
|
2358
2369
|
className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove",
|
|
2359
2370
|
title: "移除",
|
|
2360
2371
|
onClick: () => removeModel(index),
|
|
2361
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2362
|
-
d: I.delete,
|
|
2372
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2373
|
+
d: I$2.delete,
|
|
2363
2374
|
size: 14
|
|
2364
2375
|
})
|
|
2365
2376
|
})
|
|
@@ -2379,8 +2390,8 @@ window.__ModuleLoader__.load({
|
|
|
2379
2390
|
className: "dshr-modelSearch dshr-comboModelSearch",
|
|
2380
2391
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2381
2392
|
className: "dshr-modelSearchIcon",
|
|
2382
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2383
|
-
d: I.search,
|
|
2393
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
|
|
2394
|
+
d: I$2.search,
|
|
2384
2395
|
size: 13
|
|
2385
2396
|
})
|
|
2386
2397
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -2399,7 +2410,7 @@ window.__ModuleLoader__.load({
|
|
|
2399
2410
|
className: "dshr-muted dshr-comboPickEmpty",
|
|
2400
2411
|
children: "没有可添加的模型"
|
|
2401
2412
|
}) : groups.map((g) => {
|
|
2402
|
-
const items = g.models.filter((m) => !form.models.includes(m.id) && (modelQuery === "" || m.id.toLowerCase().includes(modelQuery.toLowerCase())));
|
|
2413
|
+
const items = g.models.filter((m) => !form.models.includes(`${g.supplier.id},${m.id}`) && (modelQuery === "" || m.id.toLowerCase().includes(modelQuery.toLowerCase())));
|
|
2403
2414
|
if (items.length === 0) return null;
|
|
2404
2415
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2405
2416
|
className: "dshr-comboPickGroup",
|
|
@@ -2413,7 +2424,7 @@ window.__ModuleLoader__.load({
|
|
|
2413
2424
|
}), items.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2414
2425
|
type: "button",
|
|
2415
2426
|
className: "dshr-comboPickItem",
|
|
2416
|
-
onClick: () => toggleModel(m.id),
|
|
2427
|
+
onClick: () => toggleModel(g.supplier.id, m.id),
|
|
2417
2428
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2418
2429
|
className: "dshr-comboPickName",
|
|
2419
2430
|
children: m.id
|
|
@@ -2484,16 +2495,820 @@ window.__ModuleLoader__.load({
|
|
|
2484
2495
|
});
|
|
2485
2496
|
}
|
|
2486
2497
|
//#endregion
|
|
2498
|
+
//#region src/client/CheckinCard.tsx
|
|
2499
|
+
/**
|
|
2500
|
+
* 概览的「签到」汇总卡:一键签到所有支持签到的供应商 + 显示今天点过没。
|
|
2501
|
+
*
|
|
2502
|
+
* **「今日已签到」的口径(天花板,刻意不撒谎)**:这里只知道「今天在这个浏览器
|
|
2503
|
+
* 点过这个按钮没」,存在 localStorage,**不知道上游是不是真的签上了**。真凭据是
|
|
2504
|
+
* 上游的 `checked_in`,但那得逐个链接查(traywork 侧实测:积分不可信、
|
|
2505
|
+
* 只有 checked_in 算数),而当前契约里没有「查签到状态」这个能力。
|
|
2506
|
+
* 所以卡片文案是「今日已点」而不是「已签到」,悬停说明里写清这一点。
|
|
2507
|
+
*
|
|
2508
|
+
* 要真状态的升级路径:给 supplier 契约加 `checkinStatus?()`,核心加一条
|
|
2509
|
+
* `/suppliers/:id/checkin/status` 汇总路由,这里改成读它即可(卡片不用改)。
|
|
2510
|
+
*/
|
|
2511
|
+
function Icon$1({ d, size = 14 }) {
|
|
2512
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2513
|
+
viewBox: "0 0 24 24",
|
|
2514
|
+
width: size,
|
|
2515
|
+
height: size,
|
|
2516
|
+
fill: "none",
|
|
2517
|
+
stroke: "currentColor",
|
|
2518
|
+
strokeWidth: "2",
|
|
2519
|
+
strokeLinecap: "round",
|
|
2520
|
+
strokeLinejoin: "round",
|
|
2521
|
+
"aria-hidden": "true",
|
|
2522
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
var I$1 = {
|
|
2526
|
+
check: "M4 12.5l5 5L20 6.5",
|
|
2527
|
+
loading: "M12 3a9 9 0 1 0 9 9"
|
|
2528
|
+
};
|
|
2529
|
+
/** localStorage key:值 = 本地日期 YYYY-MM-DD。 */
|
|
2530
|
+
var CHECKIN_STAMP_KEY = "dsh-router:checkin-stamp";
|
|
2531
|
+
/** 本地日切(跟用量统计的 localDateKey 同口径,别用 UTC)。 */
|
|
2532
|
+
function todayKey(ts = Date.now()) {
|
|
2533
|
+
const d = new Date(ts);
|
|
2534
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2535
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
2536
|
+
}
|
|
2537
|
+
function readStamp() {
|
|
2538
|
+
try {
|
|
2539
|
+
return localStorage.getItem(CHECKIN_STAMP_KEY);
|
|
2540
|
+
} catch {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
function writeStamp() {
|
|
2545
|
+
try {
|
|
2546
|
+
localStorage.setItem(CHECKIN_STAMP_KEY, todayKey());
|
|
2547
|
+
} catch {}
|
|
2548
|
+
}
|
|
2549
|
+
/** 可签到的供应商(能力 checkinNow)。 */
|
|
2550
|
+
function checkinable(health) {
|
|
2551
|
+
return (health?.suppliers ?? []).filter((s) => (s.capabilities ?? []).includes("checkinNow")).map((s) => ({
|
|
2552
|
+
id: s.id,
|
|
2553
|
+
name: s.name
|
|
2554
|
+
}));
|
|
2555
|
+
}
|
|
2556
|
+
function CheckinCard({ health, onRefresh }) {
|
|
2557
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2558
|
+
const [stamp, setStamp] = (0, react.useState)(null);
|
|
2559
|
+
const [result, setResult] = (0, react.useState)("");
|
|
2560
|
+
const [error, setError] = (0, react.useState)("");
|
|
2561
|
+
(0, react.useEffect)(() => {
|
|
2562
|
+
setStamp(readStamp());
|
|
2563
|
+
}, []);
|
|
2564
|
+
const targets = checkinable(health);
|
|
2565
|
+
const clickedToday = stamp === todayKey();
|
|
2566
|
+
const run = async () => {
|
|
2567
|
+
if (busy || targets.length === 0) return;
|
|
2568
|
+
setBusy(true);
|
|
2569
|
+
setError("");
|
|
2570
|
+
setResult("");
|
|
2571
|
+
let okCount = 0;
|
|
2572
|
+
let alreadyCount = 0;
|
|
2573
|
+
let totalCount = 0;
|
|
2574
|
+
const failures = [];
|
|
2575
|
+
for (const t of targets) try {
|
|
2576
|
+
const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${t.id}/checkin`, {
|
|
2577
|
+
method: "POST",
|
|
2578
|
+
cache: "no-store"
|
|
2579
|
+
})).json();
|
|
2580
|
+
if (data.error !== void 0) {
|
|
2581
|
+
failures.push(`${t.name}:${data.error}`);
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
okCount += data.succeeded;
|
|
2585
|
+
alreadyCount += data.already;
|
|
2586
|
+
totalCount += data.total;
|
|
2587
|
+
if (!data.ok) {
|
|
2588
|
+
const first = (data.results ?? []).find((r) => r.status !== "ok" && r.status !== "already");
|
|
2589
|
+
failures.push(`${t.name}:${first?.message ?? "有链接签到失败"}`);
|
|
2590
|
+
}
|
|
2591
|
+
} catch (err) {
|
|
2592
|
+
failures.push(`${t.name}:${err.message}`);
|
|
2593
|
+
}
|
|
2594
|
+
writeStamp();
|
|
2595
|
+
setStamp(todayKey());
|
|
2596
|
+
const parts = [`${okCount}/${totalCount} 成功`];
|
|
2597
|
+
if (alreadyCount > 0) parts.push(`${alreadyCount} 今日已签`);
|
|
2598
|
+
setResult(parts.join(" · "));
|
|
2599
|
+
if (failures.length > 0) setError(failures.join(";"));
|
|
2600
|
+
onRefresh();
|
|
2601
|
+
setBusy(false);
|
|
2602
|
+
};
|
|
2603
|
+
/**
|
|
2604
|
+
* 第三行文案:所有状态/结果都压在这一行里,行数恒定。
|
|
2605
|
+
* 优先级:失败 > 本次结果 > 今日已点 > 默认供应商数。
|
|
2606
|
+
* 失败时截断显示(全量挂 title),不换行。
|
|
2607
|
+
*/
|
|
2608
|
+
const hintText = () => {
|
|
2609
|
+
if (targets.length === 0) return "没有支持签到的供应商";
|
|
2610
|
+
if (busy) return "签到中…";
|
|
2611
|
+
if (error !== "") return `失败 · ${error}`;
|
|
2612
|
+
if (result !== "") return result;
|
|
2613
|
+
return clickedToday ? `今日已点 · ${targets.length} 个供应商` : `${targets.length} 个供应商`;
|
|
2614
|
+
};
|
|
2615
|
+
/** 悬停说明:成功/失败都在这儿补全,正文只放得下一行 */
|
|
2616
|
+
const hintTitle = () => {
|
|
2617
|
+
if (targets.length === 0) return "没有供应商实现了 checkinNow 能力";
|
|
2618
|
+
const lines = [`对 ${targets.length} 个支持签到的供应商逐个签到:${targets.map((t) => t.name).join("、")}`];
|
|
2619
|
+
if (clickedToday) lines.push("今天在这个浏览器点过签到(不代表上游一定签上了)");
|
|
2620
|
+
if (result !== "") lines.push(`上次结果:${result}`);
|
|
2621
|
+
if (error !== "") lines.push(`失败:${error}`);
|
|
2622
|
+
return lines.join("\n");
|
|
2623
|
+
};
|
|
2624
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2625
|
+
className: `dshr-statCard dshr-statCard-checkin${clickedToday ? " dshr-statCard-done" : ""}`,
|
|
2626
|
+
title: hintTitle(),
|
|
2627
|
+
children: [
|
|
2628
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2629
|
+
className: "dshr-statLabel",
|
|
2630
|
+
children: "签到"
|
|
2631
|
+
}),
|
|
2632
|
+
targets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2633
|
+
className: "dshr-statValue",
|
|
2634
|
+
children: "—"
|
|
2635
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2636
|
+
type: "button",
|
|
2637
|
+
className: `dshr-checkinButton${busy ? " dshr-checkinButton-busy" : ""}`,
|
|
2638
|
+
onClick: () => {
|
|
2639
|
+
run();
|
|
2640
|
+
},
|
|
2641
|
+
disabled: busy,
|
|
2642
|
+
title: hintTitle(),
|
|
2643
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2644
|
+
className: busy ? "dshr-spin" : void 0,
|
|
2645
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
2646
|
+
d: busy ? I$1.loading : I$1.check,
|
|
2647
|
+
size: 14
|
|
2648
|
+
})
|
|
2649
|
+
}), busy ? "签到中…" : "一键签到"]
|
|
2650
|
+
}),
|
|
2651
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2652
|
+
className: `dshr-statHint${error !== "" ? " dshr-statHint-err" : ""}`,
|
|
2653
|
+
title: error !== "" ? error : void 0,
|
|
2654
|
+
children: hintText()
|
|
2655
|
+
})
|
|
2656
|
+
]
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/client/StatsTab.tsx
|
|
2661
|
+
/**
|
|
2662
|
+
* 概览 —— 仿 9router `Dashboard → Usage → Overview` 的统计看板。
|
|
2663
|
+
*
|
|
2664
|
+
* 布局/交互跟其它 tab 一致(同一套 `.dshr-card` / `.dshr-tabBody`):
|
|
2665
|
+
* 周期切换 → 汇总卡 → 趋势折线图 → Top 榜 → 最近请求。
|
|
2666
|
+
*
|
|
2667
|
+
* 两个 9router 有而我们刻意不做的(天花板,非遗漏):
|
|
2668
|
+
* - 成本估算:9router 有 pricingRepo 全表,dsh-router 没有价格数据,
|
|
2669
|
+
* 硬造一个只会显示假钱。宁可不显示。
|
|
2670
|
+
* - 实时推送:9router 有 `/api/usage/stream` 的 SSE 看板。这里刷新才更新
|
|
2671
|
+
* (面板本身有刷新按钮)。要实时就加一条 SSE 路由,看板不用改。
|
|
2672
|
+
*
|
|
2673
|
+
* token 口径见 `router/usage-tokens.ts`:上游不发 usage 时按字符数估算,
|
|
2674
|
+
* 估算过的字段标 `~`,鼠标悬停会说明。
|
|
2675
|
+
*/
|
|
2676
|
+
function Icon({ d, size = 18 }) {
|
|
2677
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2678
|
+
viewBox: "0 0 24 24",
|
|
2679
|
+
width: size,
|
|
2680
|
+
height: size,
|
|
2681
|
+
fill: "none",
|
|
2682
|
+
stroke: "currentColor",
|
|
2683
|
+
strokeWidth: "1.8",
|
|
2684
|
+
strokeLinecap: "round",
|
|
2685
|
+
strokeLinejoin: "round",
|
|
2686
|
+
"aria-hidden": "true",
|
|
2687
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
var I = {
|
|
2691
|
+
chart: "M3 3v18h18M7 15l4-5 3 3 5-7",
|
|
2692
|
+
bolt: "M13 2L4 14h6l-1 8 9-12h-6z",
|
|
2693
|
+
refresh: "M21 12a9 9 0 1 1-3-6.7M21 4v5h-5",
|
|
2694
|
+
empty: "M4 4h16v16H4zM8 12h8M12 8v8"
|
|
2695
|
+
};
|
|
2696
|
+
/**
|
|
2697
|
+
* 周期选项(9router 的 PERIODS)。
|
|
2698
|
+
* `value` 是 `/stats?period=` 的参数值,别为了显示去动它;
|
|
2699
|
+
* 显示文案统一中文(跟「今日」一致)。
|
|
2700
|
+
*/
|
|
2701
|
+
var PERIODS = [
|
|
2702
|
+
{
|
|
2703
|
+
value: "today",
|
|
2704
|
+
label: "今日"
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
value: "24h",
|
|
2708
|
+
label: "24 小时"
|
|
2709
|
+
},
|
|
2710
|
+
{
|
|
2711
|
+
value: "7d",
|
|
2712
|
+
label: "7 天"
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
value: "30d",
|
|
2716
|
+
label: "30 天"
|
|
2717
|
+
}
|
|
2718
|
+
];
|
|
2719
|
+
/** 「最近请求」显示条数(后端给了 20 条,面板只看最近的这些)。 */
|
|
2720
|
+
var RECENT_LIMIT = 10;
|
|
2721
|
+
/** 千分位。 */
|
|
2722
|
+
function fmt(n) {
|
|
2723
|
+
return new Intl.NumberFormat().format(n ?? 0);
|
|
2724
|
+
}
|
|
2725
|
+
/** 大数缩写:1.2K / 3.4M(卡片和柱图上放不下完整数字时用)。 */
|
|
2726
|
+
function fmtShort(n) {
|
|
2727
|
+
const v = n ?? 0;
|
|
2728
|
+
if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
|
|
2729
|
+
if (v >= 1e4) return `${(v / 1e3).toFixed(1)}K`;
|
|
2730
|
+
return fmt(v);
|
|
2731
|
+
}
|
|
2732
|
+
function fmtDuration(ms) {
|
|
2733
|
+
const v = ms ?? 0;
|
|
2734
|
+
if (v <= 0) return "—";
|
|
2735
|
+
if (v < 1e3) return `${v}ms`;
|
|
2736
|
+
return `${(v / 1e3).toFixed(1)}s`;
|
|
2737
|
+
}
|
|
2738
|
+
function timeAgo(ts) {
|
|
2739
|
+
const diff = Math.max(0, Math.floor((Date.now() - ts) / 1e3));
|
|
2740
|
+
if (diff < 60) return `${diff}秒前`;
|
|
2741
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
|
|
2742
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
|
|
2743
|
+
return `${Math.floor(diff / 86400)}天前`;
|
|
2744
|
+
}
|
|
2745
|
+
function clockTime(ts) {
|
|
2746
|
+
const d = new Date(ts);
|
|
2747
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2748
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
2749
|
+
}
|
|
2750
|
+
function StatCard({ label, value, hint, tone = "default", title }) {
|
|
2751
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2752
|
+
className: `dshr-statCard dshr-statCard-${tone}`,
|
|
2753
|
+
title,
|
|
2754
|
+
children: [
|
|
2755
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2756
|
+
className: "dshr-statLabel",
|
|
2757
|
+
children: label
|
|
2758
|
+
}),
|
|
2759
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2760
|
+
className: "dshr-statValue",
|
|
2761
|
+
children: value
|
|
2762
|
+
}),
|
|
2763
|
+
hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2764
|
+
className: "dshr-statHint",
|
|
2765
|
+
children: hint
|
|
2766
|
+
})
|
|
2767
|
+
]
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
/**
|
|
2771
|
+
* 视口坐标固定,SVG 用 `preserveAspectRatio="none"` 横向拉满容器:
|
|
2772
|
+
* 线宽靠 `vector-effect="non-scaling-stroke"` 抵消拉伸,数值点/悬停辅助线
|
|
2773
|
+
* 用 HTML 绝对定位叠在 SVG 上(否则圆点会被拉成椭圆)。
|
|
2774
|
+
*
|
|
2775
|
+
* 交互:指针(鼠标/触摸/笔)取最近桶,键盘 ←/→/Home/End 逐格走,Esc 取消。
|
|
2776
|
+
* 天花板:只画一条 tokens 折线(桶里没有输入/输出分项),要分线得先改
|
|
2777
|
+
* `/stats/chart` 的桶结构。
|
|
2778
|
+
*/
|
|
2779
|
+
var TREND_W = 640;
|
|
2780
|
+
var TREND_H = 150;
|
|
2781
|
+
var TREND_PAD_X = 12;
|
|
2782
|
+
var TREND_PAD_T = 12;
|
|
2783
|
+
var TREND_PAD_B = 8;
|
|
2784
|
+
function LineChart({ buckets, busy }) {
|
|
2785
|
+
const [hover, setHover] = (0, react.useState)(null);
|
|
2786
|
+
const n = buckets.length;
|
|
2787
|
+
const max = Math.max(1, ...buckets.map((b) => b.tokens));
|
|
2788
|
+
const hasData = buckets.some((b) => b.requests > 0 || b.tokens > 0);
|
|
2789
|
+
const active = hover === null ? void 0 : buckets[hover];
|
|
2790
|
+
const innerW = TREND_W - TREND_PAD_X * 2;
|
|
2791
|
+
const innerH = TREND_H - TREND_PAD_T - TREND_PAD_B;
|
|
2792
|
+
const xOf = (i) => TREND_PAD_X + (n <= 1 ? innerW / 2 : i * innerW / (n - 1));
|
|
2793
|
+
const yOf = (v) => TREND_PAD_T + (1 - v / max) * innerH;
|
|
2794
|
+
const pctX = (i) => `${xOf(i) / TREND_W * 100}%`;
|
|
2795
|
+
const pctY = (v) => `${yOf(v) / TREND_H * 100}%`;
|
|
2796
|
+
const pts = buckets.map((b, i) => `${xOf(i)},${yOf(b.tokens)}`);
|
|
2797
|
+
const linePath = pts.length > 0 ? `M${pts.join("L")}` : "";
|
|
2798
|
+
const areaPath = pts.length > 0 ? `${linePath}L${xOf(n - 1)},${TREND_H - TREND_PAD_B}L${xOf(0)},${TREND_H - TREND_PAD_B}Z` : "";
|
|
2799
|
+
const labelStep = Math.max(1, Math.ceil(n / 8));
|
|
2800
|
+
const showLabel = (i) => i === n - 1 || i % labelStep === 0 && n - 1 - i >= labelStep / 2;
|
|
2801
|
+
/** 取离指针最近的桶:比给每桶铺一个透明 rect 省 DOM,30 天也只有一层。 */
|
|
2802
|
+
const pick = (e) => {
|
|
2803
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
2804
|
+
const x = (e.clientX - rect.left) / rect.width * TREND_W;
|
|
2805
|
+
const i = n <= 1 ? 0 : Math.round((x - TREND_PAD_X) / innerW * (n - 1));
|
|
2806
|
+
setHover(Math.min(n - 1, Math.max(0, i)));
|
|
2807
|
+
};
|
|
2808
|
+
/** 键盘逐格走:函数式更新,连按不会读到过期的 hover。 */
|
|
2809
|
+
const step = (delta) => {
|
|
2810
|
+
setHover((cur) => n === 0 ? null : Math.min(n - 1, Math.max(0, (cur ?? n - 1) + delta)));
|
|
2811
|
+
};
|
|
2812
|
+
const onKeyDown = (e) => {
|
|
2813
|
+
const act = {
|
|
2814
|
+
ArrowLeft: () => step(-1),
|
|
2815
|
+
ArrowRight: () => step(1),
|
|
2816
|
+
Home: () => setHover(0),
|
|
2817
|
+
End: () => setHover(n - 1),
|
|
2818
|
+
Escape: () => setHover(null)
|
|
2819
|
+
}[e.key];
|
|
2820
|
+
if (act === void 0) return;
|
|
2821
|
+
e.preventDefault();
|
|
2822
|
+
act();
|
|
2823
|
+
};
|
|
2824
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2825
|
+
className: "dshr-trendWrap",
|
|
2826
|
+
children: [busy && n > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2827
|
+
className: "dshr-trendBusy",
|
|
2828
|
+
"aria-hidden": "true"
|
|
2829
|
+
}), !hasData ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2830
|
+
className: "dshr-trendEmpty",
|
|
2831
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2832
|
+
className: "dshr-trendEmptyIcon",
|
|
2833
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
2834
|
+
d: I.empty,
|
|
2835
|
+
size: 26
|
|
2836
|
+
})
|
|
2837
|
+
}), "这个周期还没有请求"]
|
|
2838
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2840
|
+
className: "dshr-linePlot",
|
|
2841
|
+
role: "img",
|
|
2842
|
+
"aria-label": `Token 趋势:${n} 个时段,峰值 ${fmtShort(max)} tokens`,
|
|
2843
|
+
tabIndex: 0,
|
|
2844
|
+
onPointerMove: pick,
|
|
2845
|
+
onPointerDown: pick,
|
|
2846
|
+
onPointerLeave: (e) => {
|
|
2847
|
+
if (e.pointerType === "mouse") setHover(null);
|
|
2848
|
+
},
|
|
2849
|
+
onBlur: () => setHover(null),
|
|
2850
|
+
onKeyDown,
|
|
2851
|
+
children: [
|
|
2852
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2853
|
+
className: "dshr-lineSvg",
|
|
2854
|
+
viewBox: `0 0 ${TREND_W} ${TREND_H}`,
|
|
2855
|
+
preserveAspectRatio: "none",
|
|
2856
|
+
"aria-hidden": "true",
|
|
2857
|
+
children: [
|
|
2858
|
+
[
|
|
2859
|
+
0,
|
|
2860
|
+
.5,
|
|
2861
|
+
1
|
|
2862
|
+
].map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
2863
|
+
className: "dshr-lineGrid",
|
|
2864
|
+
x1: 0,
|
|
2865
|
+
x2: TREND_W,
|
|
2866
|
+
y1: yOf(max * f),
|
|
2867
|
+
y2: yOf(max * f),
|
|
2868
|
+
vectorEffect: "non-scaling-stroke"
|
|
2869
|
+
}, f)),
|
|
2870
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2871
|
+
className: "dshr-lineArea",
|
|
2872
|
+
d: areaPath
|
|
2873
|
+
}),
|
|
2874
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2875
|
+
className: "dshr-linePath",
|
|
2876
|
+
d: linePath,
|
|
2877
|
+
vectorEffect: "non-scaling-stroke"
|
|
2878
|
+
})
|
|
2879
|
+
]
|
|
2880
|
+
}),
|
|
2881
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2882
|
+
className: "dshr-linePeak",
|
|
2883
|
+
children: ["峰值 ", fmtShort(max)]
|
|
2884
|
+
}),
|
|
2885
|
+
n <= 24 && buckets.map((b, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2886
|
+
className: "dshr-linePoint",
|
|
2887
|
+
style: {
|
|
2888
|
+
left: pctX(i),
|
|
2889
|
+
top: pctY(b.tokens)
|
|
2890
|
+
}
|
|
2891
|
+
}, i)),
|
|
2892
|
+
hover !== null && active !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2893
|
+
className: "dshr-lineGuide",
|
|
2894
|
+
style: { left: pctX(hover) }
|
|
2895
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2896
|
+
className: "dshr-lineDot",
|
|
2897
|
+
style: {
|
|
2898
|
+
left: pctX(hover),
|
|
2899
|
+
top: pctY(active.tokens)
|
|
2900
|
+
}
|
|
2901
|
+
})] })
|
|
2902
|
+
]
|
|
2903
|
+
}),
|
|
2904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2905
|
+
className: "dshr-lineLabels",
|
|
2906
|
+
children: buckets.map((b, i) => showLabel(i) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2907
|
+
className: `dshr-lineLabel${hover === i ? " dshr-lineLabel-on" : ""}`,
|
|
2908
|
+
style: { left: pctX(i) },
|
|
2909
|
+
children: b.label
|
|
2910
|
+
}, i))
|
|
2911
|
+
}),
|
|
2912
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2913
|
+
className: "dshr-trendReadout",
|
|
2914
|
+
title: active === void 0 ? void 0 : `${fmt(active.requests)} 请求 · ${fmt(active.tokens)} tokens`,
|
|
2915
|
+
children: active === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2916
|
+
className: "dshr-muted",
|
|
2917
|
+
children: "悬停或按 ←/→ 看每个时段"
|
|
2918
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: active.label }),
|
|
2920
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2921
|
+
className: "dshr-muted",
|
|
2922
|
+
children: [
|
|
2923
|
+
" · ",
|
|
2924
|
+
fmtShort(active.requests),
|
|
2925
|
+
" 请求"
|
|
2926
|
+
]
|
|
2927
|
+
}),
|
|
2928
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2929
|
+
className: "dshr-muted",
|
|
2930
|
+
children: [
|
|
2931
|
+
" · ",
|
|
2932
|
+
fmtShort(active.tokens),
|
|
2933
|
+
" tokens"
|
|
2934
|
+
]
|
|
2935
|
+
})
|
|
2936
|
+
] })
|
|
2937
|
+
})
|
|
2938
|
+
] })]
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
function RankTable({ title, rows, empty }) {
|
|
2942
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2943
|
+
className: "dshr-card",
|
|
2944
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2945
|
+
className: "dshr-cardHead",
|
|
2946
|
+
children: [
|
|
2947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2948
|
+
className: "dshr-cardIcon",
|
|
2949
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.chart })
|
|
2950
|
+
}),
|
|
2951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2952
|
+
className: "dshr-cardTitle",
|
|
2953
|
+
children: title
|
|
2954
|
+
}),
|
|
2955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2956
|
+
className: "dshr-cardMeta",
|
|
2957
|
+
children: rows.length > 0 ? `Top ${rows.length}` : ""
|
|
2958
|
+
})
|
|
2959
|
+
]
|
|
2960
|
+
}), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2961
|
+
className: "dshr-empty",
|
|
2962
|
+
children: empty
|
|
2963
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2964
|
+
className: "dshr-rankList",
|
|
2965
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2966
|
+
className: "dshr-rankRow dshr-rankHead",
|
|
2967
|
+
children: [
|
|
2968
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2969
|
+
className: "dshr-rankName",
|
|
2970
|
+
children: "名称"
|
|
2971
|
+
}),
|
|
2972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2973
|
+
className: "dshr-rankNum",
|
|
2974
|
+
children: "请求"
|
|
2975
|
+
}),
|
|
2976
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2977
|
+
className: "dshr-rankNum",
|
|
2978
|
+
children: "输入"
|
|
2979
|
+
}),
|
|
2980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2981
|
+
className: "dshr-rankNum",
|
|
2982
|
+
children: "输出"
|
|
2983
|
+
}),
|
|
2984
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2985
|
+
className: "dshr-rankNum",
|
|
2986
|
+
children: "最近"
|
|
2987
|
+
})
|
|
2988
|
+
]
|
|
2989
|
+
}), rows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2990
|
+
className: "dshr-rankRow",
|
|
2991
|
+
children: [
|
|
2992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2993
|
+
className: "dshr-rankName dshr-mono",
|
|
2994
|
+
title: r.name,
|
|
2995
|
+
children: r.name
|
|
2996
|
+
}),
|
|
2997
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2998
|
+
className: "dshr-rankNum",
|
|
2999
|
+
children: [fmt(r.requests), r.failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3000
|
+
className: "dshr-rankFail",
|
|
3001
|
+
title: `${r.failed} 次失败`,
|
|
3002
|
+
children: [
|
|
3003
|
+
" (",
|
|
3004
|
+
r.failed,
|
|
3005
|
+
")"
|
|
3006
|
+
]
|
|
3007
|
+
})]
|
|
3008
|
+
}),
|
|
3009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3010
|
+
className: "dshr-rankNum",
|
|
3011
|
+
children: fmtShort(r.promptTokens)
|
|
3012
|
+
}),
|
|
3013
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3014
|
+
className: "dshr-rankNum",
|
|
3015
|
+
children: fmtShort(r.completionTokens)
|
|
3016
|
+
}),
|
|
3017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3018
|
+
className: "dshr-rankNum dshr-muted",
|
|
3019
|
+
children: r.lastTs > 0 ? timeAgo(r.lastTs) : "—"
|
|
3020
|
+
})
|
|
3021
|
+
]
|
|
3022
|
+
}, r.name))]
|
|
3023
|
+
})]
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
function RecentTable({ rows }) {
|
|
3027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3028
|
+
className: "dshr-card",
|
|
3029
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3030
|
+
className: "dshr-cardHead",
|
|
3031
|
+
children: [
|
|
3032
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3033
|
+
className: "dshr-cardIcon",
|
|
3034
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.bolt })
|
|
3035
|
+
}),
|
|
3036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3037
|
+
className: "dshr-cardTitle",
|
|
3038
|
+
children: "最近请求"
|
|
3039
|
+
}),
|
|
3040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3041
|
+
className: "dshr-cardMeta",
|
|
3042
|
+
children: [
|
|
3043
|
+
"最近 ",
|
|
3044
|
+
rows.length,
|
|
3045
|
+
" 条"
|
|
3046
|
+
]
|
|
3047
|
+
})
|
|
3048
|
+
]
|
|
3049
|
+
}), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3050
|
+
className: "dshr-empty",
|
|
3051
|
+
children: "还没有请求记录"
|
|
3052
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3053
|
+
className: "dshr-recentList",
|
|
3054
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3055
|
+
className: "dshr-recentRow dshr-rankHead",
|
|
3056
|
+
children: [
|
|
3057
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3058
|
+
className: "dshr-recentTime",
|
|
3059
|
+
children: "时间"
|
|
3060
|
+
}),
|
|
3061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3062
|
+
className: "dshr-recentModel",
|
|
3063
|
+
children: "模型"
|
|
3064
|
+
}),
|
|
3065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3066
|
+
className: "dshr-recentSupplier",
|
|
3067
|
+
children: "供应商"
|
|
3068
|
+
}),
|
|
3069
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3070
|
+
className: "dshr-rankNum",
|
|
3071
|
+
children: "In / Out"
|
|
3072
|
+
}),
|
|
3073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3074
|
+
className: "dshr-rankNum",
|
|
3075
|
+
children: "耗时"
|
|
3076
|
+
})
|
|
3077
|
+
]
|
|
3078
|
+
}), rows.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3079
|
+
className: `dshr-recentRow${r.ok ? "" : " dshr-recentRow-fail"}`,
|
|
3080
|
+
children: [
|
|
3081
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3082
|
+
className: "dshr-recentTime",
|
|
3083
|
+
title: new Date(r.ts).toLocaleString(),
|
|
3084
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `dshr-dot ${r.ok ? "dshr-dot-ok" : "dshr-dot-fail"}` }), clockTime(r.ts)]
|
|
3085
|
+
}),
|
|
3086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3087
|
+
className: "dshr-recentModel dshr-mono",
|
|
3088
|
+
title: `请求 ${r.requested}${r.error === void 0 ? "" : ` · ${r.error}`}`,
|
|
3089
|
+
children: r.model === "" ? r.requested : r.model
|
|
3090
|
+
}),
|
|
3091
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3092
|
+
className: "dshr-recentSupplier",
|
|
3093
|
+
children: r.supplier === "" ? "—" : r.supplier
|
|
3094
|
+
}),
|
|
3095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3096
|
+
className: "dshr-rankNum",
|
|
3097
|
+
children: [
|
|
3098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3099
|
+
className: "dshr-tokenIn",
|
|
3100
|
+
children: [fmtShort(r.promptTokens), r.inputEstimated ? "~" : ""]
|
|
3101
|
+
}),
|
|
3102
|
+
" / ",
|
|
3103
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3104
|
+
className: "dshr-tokenOut",
|
|
3105
|
+
children: [fmtShort(r.completionTokens), r.outputEstimated ? "~" : ""]
|
|
3106
|
+
})
|
|
3107
|
+
]
|
|
3108
|
+
}),
|
|
3109
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3110
|
+
className: "dshr-rankNum dshr-muted",
|
|
3111
|
+
children: fmtDuration(r.durationMs)
|
|
3112
|
+
})
|
|
3113
|
+
]
|
|
3114
|
+
}, `${r.ts}-${r.model}-${i}`))]
|
|
3115
|
+
})]
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
function StatsTab({ onRefresh, refreshing, active, health }) {
|
|
3119
|
+
const [period, setPeriod] = (0, react.useState)("today");
|
|
3120
|
+
const [stats, setStats] = (0, react.useState)(null);
|
|
3121
|
+
const [chart, setChart] = (0, react.useState)([]);
|
|
3122
|
+
const [loading, setLoading] = (0, react.useState)(true);
|
|
3123
|
+
const [error, setError] = (0, react.useState)("");
|
|
3124
|
+
const mounted = (0, react.useRef)(true);
|
|
3125
|
+
(0, react.useEffect)(() => {
|
|
3126
|
+
mounted.current = true;
|
|
3127
|
+
return () => {
|
|
3128
|
+
mounted.current = false;
|
|
3129
|
+
};
|
|
3130
|
+
}, []);
|
|
3131
|
+
const load = async (p) => {
|
|
3132
|
+
try {
|
|
3133
|
+
const [sRes, cRes] = await Promise.all([fetch(`${ROUTER_API_BASE}/stats?period=${p}`, { cache: "no-store" }), fetch(`${ROUTER_API_BASE}/stats/chart?period=${p}`, { cache: "no-store" })]);
|
|
3134
|
+
const s = await sRes.json();
|
|
3135
|
+
const c = await cRes.json();
|
|
3136
|
+
if (!mounted.current) return;
|
|
3137
|
+
setStats(s.ok ? s : null);
|
|
3138
|
+
setChart(c.ok ? c.chart ?? [] : []);
|
|
3139
|
+
setError(s.ok ? c.ok ? "" : c.error ?? "" : s.error ?? "加载统计失败");
|
|
3140
|
+
} catch (err) {
|
|
3141
|
+
if (mounted.current) setError(err.message);
|
|
3142
|
+
} finally {
|
|
3143
|
+
if (mounted.current) setLoading(false);
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
3146
|
+
(0, react.useEffect)(() => {
|
|
3147
|
+
if (!active) return;
|
|
3148
|
+
setLoading(true);
|
|
3149
|
+
load(period);
|
|
3150
|
+
}, [period, active]);
|
|
3151
|
+
const s = stats;
|
|
3152
|
+
const successRate = s !== null && s.requests !== void 0 && s.requests > 0 ? Math.round((s.okCount ?? 0) / s.requests * 100) : null;
|
|
3153
|
+
const estimated = s !== null && ((s.estimatedInputs ?? 0) > 0 || (s.estimatedOutputs ?? 0) > 0);
|
|
3154
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3155
|
+
className: "dshr-tabBody",
|
|
3156
|
+
children: [
|
|
3157
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3158
|
+
className: "dshr-statsBar",
|
|
3159
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3160
|
+
className: "dshr-periods",
|
|
3161
|
+
role: "tablist",
|
|
3162
|
+
"aria-label": "统计周期",
|
|
3163
|
+
children: PERIODS.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3164
|
+
type: "button",
|
|
3165
|
+
role: "tab",
|
|
3166
|
+
"aria-selected": period === p.value,
|
|
3167
|
+
className: `dshr-period${period === p.value ? " dshr-period-on" : ""}`,
|
|
3168
|
+
onClick: () => setPeriod(p.value),
|
|
3169
|
+
children: p.label
|
|
3170
|
+
}, p.value))
|
|
3171
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3172
|
+
className: "dshr-statsBarRight",
|
|
3173
|
+
children: [s?.lifetime !== void 0 && s.lifetime > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3174
|
+
className: "dshr-muted dshr-lifetime",
|
|
3175
|
+
title: "累计请求数(不受周期影响)",
|
|
3176
|
+
children: ["累计 ", fmt(s.lifetime)]
|
|
3177
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3178
|
+
type: "button",
|
|
3179
|
+
className: "dshr-iconBtn",
|
|
3180
|
+
onClick: () => {
|
|
3181
|
+
load(period);
|
|
3182
|
+
onRefresh();
|
|
3183
|
+
},
|
|
3184
|
+
disabled: refreshing,
|
|
3185
|
+
title: refreshing ? "刷新中…" : "刷新统计",
|
|
3186
|
+
"aria-label": refreshing ? "刷新中" : "刷新统计",
|
|
3187
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3188
|
+
className: refreshing ? "dshr-spin" : void 0,
|
|
3189
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
3190
|
+
d: I.refresh,
|
|
3191
|
+
size: 16
|
|
3192
|
+
})
|
|
3193
|
+
})
|
|
3194
|
+
})]
|
|
3195
|
+
})]
|
|
3196
|
+
}),
|
|
3197
|
+
error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3198
|
+
className: "dshr-alert",
|
|
3199
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "统计加载失败" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
|
|
3200
|
+
}),
|
|
3201
|
+
loading && s === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3202
|
+
className: "dshr-empty",
|
|
3203
|
+
children: "加载中…"
|
|
3204
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3206
|
+
className: "dshr-statGrid",
|
|
3207
|
+
children: [
|
|
3208
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
|
|
3209
|
+
label: "总请求",
|
|
3210
|
+
value: fmt(s?.requests),
|
|
3211
|
+
hint: successRate === null ? void 0 : `成功率 ${successRate}%`,
|
|
3212
|
+
tone: "default"
|
|
3213
|
+
}),
|
|
3214
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
|
|
3215
|
+
label: "输入 Tokens",
|
|
3216
|
+
value: fmtShort(s?.promptTokens),
|
|
3217
|
+
hint: (s?.estimatedInputs ?? 0) > 0 ? "~ 含估算" : void 0,
|
|
3218
|
+
tone: "primary",
|
|
3219
|
+
title: "包含缓存命中的输入"
|
|
3220
|
+
}),
|
|
3221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
|
|
3222
|
+
label: "输出 Tokens",
|
|
3223
|
+
value: fmtShort(s?.completionTokens),
|
|
3224
|
+
hint: (s?.estimatedOutputs ?? 0) > 0 ? "~ 含估算" : void 0,
|
|
3225
|
+
tone: "ok"
|
|
3226
|
+
}),
|
|
3227
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
|
|
3228
|
+
label: "缓存 Tokens",
|
|
3229
|
+
value: fmtShort(s?.cachedTokens),
|
|
3230
|
+
hint: "命中缓存的输入部分",
|
|
3231
|
+
tone: "default"
|
|
3232
|
+
}),
|
|
3233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
|
|
3234
|
+
label: "平均耗时",
|
|
3235
|
+
value: fmtDuration(s?.avgDurationMs),
|
|
3236
|
+
hint: (s?.avgTtfbMs ?? 0) > 0 ? `首字节 ${fmtDuration(s?.avgTtfbMs)}` : void 0,
|
|
3237
|
+
tone: "warn"
|
|
3238
|
+
}),
|
|
3239
|
+
health !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckinCard, {
|
|
3240
|
+
health,
|
|
3241
|
+
onRefresh
|
|
3242
|
+
})
|
|
3243
|
+
]
|
|
3244
|
+
}),
|
|
3245
|
+
estimated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3246
|
+
className: "dshr-estimateNote",
|
|
3247
|
+
children: [
|
|
3248
|
+
"带 ",
|
|
3249
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "~" }),
|
|
3250
|
+
" 的数字为估算:部分上游不返回用量,按 ~4 字符/token 推算。请求数与耗时不受影响。"
|
|
3251
|
+
]
|
|
3252
|
+
}),
|
|
3253
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3254
|
+
className: "dshr-card",
|
|
3255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3256
|
+
className: "dshr-cardHead",
|
|
3257
|
+
children: [
|
|
3258
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3259
|
+
className: "dshr-cardIcon",
|
|
3260
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.chart })
|
|
3261
|
+
}),
|
|
3262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3263
|
+
className: "dshr-cardTitle",
|
|
3264
|
+
children: "Token 趋势"
|
|
3265
|
+
}),
|
|
3266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3267
|
+
className: "dshr-cardMeta",
|
|
3268
|
+
children: PERIODS.find((p) => p.value === period)?.label
|
|
3269
|
+
})
|
|
3270
|
+
]
|
|
3271
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineChart, {
|
|
3272
|
+
buckets: chart,
|
|
3273
|
+
busy: loading
|
|
3274
|
+
})]
|
|
3275
|
+
}),
|
|
3276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3277
|
+
className: "dshr-rankPair",
|
|
3278
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankTable, {
|
|
3279
|
+
title: "按供应商",
|
|
3280
|
+
rows: s?.bySupplier ?? [],
|
|
3281
|
+
empty: "这个周期还没有请求"
|
|
3282
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankTable, {
|
|
3283
|
+
title: "按模型",
|
|
3284
|
+
rows: s?.byModel ?? [],
|
|
3285
|
+
empty: "这个周期还没有请求"
|
|
3286
|
+
})]
|
|
3287
|
+
}),
|
|
3288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecentTable, { rows: (s?.recent ?? []).slice(0, RECENT_LIMIT) })
|
|
3289
|
+
] })
|
|
3290
|
+
]
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
//#endregion
|
|
2487
3294
|
//#region src/client/RouterView.tsx
|
|
2488
3295
|
/**
|
|
2489
3296
|
* 路由系统 (Routing System) panel — a simplified 9router.
|
|
2490
3297
|
*
|
|
2491
|
-
*
|
|
2492
|
-
* dashboard: 供应商 (supplier cards → detail: accounts +
|
|
2493
|
-
* 组合 (combo fallback chains),
|
|
3298
|
+
* Four tabs fed by the host half (/router/api/*), mirroring 9router's
|
|
3299
|
+
* dashboard: 概览 (usage), 供应商 (supplier cards → detail: accounts +
|
|
3300
|
+
* add-account + models), 组合 (combo fallback chains),
|
|
3301
|
+
* 端点与密钥 (endpoint URL + API keys + auth).
|
|
2494
3302
|
*
|
|
2495
|
-
*
|
|
2496
|
-
*
|
|
3303
|
+
* **挂载在「设置」里**(`settings.section` 座位),所以布局规则跟中心栏劫持时期不同:
|
|
3304
|
+
* - 没有 masthead / 返回会话按钮 —— 设置面板自带标题栏和关闭
|
|
3305
|
+
* - **tab 条固定、只有内容区滚**:根节点限高 `height:100%`,内容区
|
|
3306
|
+
* `flex:1 + min-height:0 + overflow-y:auto` 自己滚。
|
|
3307
|
+
* 根节点**必须限高** —— 不限高内容会把外壳撑开,退化成整页滚、
|
|
3308
|
+
* tab 条跟着滚上去
|
|
3309
|
+
* - 根节点外面套了一层 `display: contents`(宿主 slot renderer 给的),
|
|
3310
|
+
* 不过实测 `height:100%` 仍能穿过它解析到外壳高度
|
|
3311
|
+
* - 内容宽度实测 564px(面板 612 − 左右 padding 24),卡片 grid 按这个宽度排
|
|
2497
3312
|
*/
|
|
2498
3313
|
async function getJson(path) {
|
|
2499
3314
|
const response = await fetch(`${ROUTER_API_BASE}${path}`, { cache: "no-store" });
|
|
@@ -2501,7 +3316,7 @@ window.__ModuleLoader__.load({
|
|
|
2501
3316
|
return response.json();
|
|
2502
3317
|
}
|
|
2503
3318
|
function RouterView({ onBack }) {
|
|
2504
|
-
const [tab, setTab] = (0, react.useState)("
|
|
3319
|
+
const [tab, setTab] = (0, react.useState)("overview");
|
|
2505
3320
|
const [detailSupplier, setDetailSupplier] = (0, react.useState)(null);
|
|
2506
3321
|
const [snapshot, setSnapshot] = (0, react.useState)({
|
|
2507
3322
|
health: null,
|
|
@@ -2587,109 +3402,107 @@ window.__ModuleLoader__.load({
|
|
|
2587
3402
|
});
|
|
2588
3403
|
};
|
|
2589
3404
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2590
|
-
className: "dshr-
|
|
2591
|
-
children: [
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
3405
|
+
className: "dshr-settings",
|
|
3406
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
3407
|
+
className: "dshr-tabs",
|
|
3408
|
+
role: "tablist",
|
|
3409
|
+
"aria-label": "路由分区",
|
|
3410
|
+
children: [
|
|
3411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3412
|
+
type: "button",
|
|
3413
|
+
role: "tab",
|
|
3414
|
+
"aria-selected": tab === "overview",
|
|
3415
|
+
className: `dshr-tab${tab === "overview" ? " dshr-tab-on" : ""}`,
|
|
3416
|
+
onClick: () => setTab("overview"),
|
|
3417
|
+
children: "概览"
|
|
3418
|
+
}),
|
|
3419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3420
|
+
type: "button",
|
|
3421
|
+
role: "tab",
|
|
3422
|
+
"aria-selected": tab === "suppliers",
|
|
3423
|
+
className: `dshr-tab${tab === "suppliers" ? " dshr-tab-on" : ""}`,
|
|
3424
|
+
onClick: () => setTab("suppliers"),
|
|
3425
|
+
children: ["供应商 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3426
|
+
className: "dshr-navCount",
|
|
3427
|
+
children: suppliers.length
|
|
3428
|
+
})]
|
|
3429
|
+
}),
|
|
3430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3431
|
+
type: "button",
|
|
3432
|
+
role: "tab",
|
|
3433
|
+
"aria-selected": tab === "combos",
|
|
3434
|
+
className: `dshr-tab${tab === "combos" ? " dshr-tab-on" : ""}`,
|
|
3435
|
+
onClick: () => setTab("combos"),
|
|
3436
|
+
children: ["组合 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3437
|
+
className: "dshr-navCount",
|
|
3438
|
+
children: combos.length
|
|
3439
|
+
})]
|
|
3440
|
+
}),
|
|
3441
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3442
|
+
type: "button",
|
|
3443
|
+
role: "tab",
|
|
3444
|
+
"aria-selected": tab === "endpoint",
|
|
3445
|
+
className: `dshr-tab${tab === "endpoint" ? " dshr-tab-on" : ""}`,
|
|
3446
|
+
onClick: () => setTab("endpoint"),
|
|
3447
|
+
children: "端点与密钥"
|
|
3448
|
+
}),
|
|
3449
|
+
onBack !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2595
3450
|
type: "button",
|
|
2596
|
-
className: "dshr-
|
|
3451
|
+
className: "dshr-tabBack",
|
|
2597
3452
|
onClick: onBack,
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
className: "dshr-nav",
|
|
2621
|
-
children: [
|
|
2622
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2623
|
-
type: "button",
|
|
2624
|
-
"aria-current": tab === "suppliers" ? "page" : void 0,
|
|
2625
|
-
onClick: () => setTab("suppliers"),
|
|
2626
|
-
children: ["供应商 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2627
|
-
className: "dshr-navCount",
|
|
2628
|
-
children: suppliers.length
|
|
2629
|
-
})]
|
|
2630
|
-
}),
|
|
2631
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2632
|
-
type: "button",
|
|
2633
|
-
"aria-current": tab === "combos" ? "page" : void 0,
|
|
2634
|
-
onClick: () => setTab("combos"),
|
|
2635
|
-
children: ["组合 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2636
|
-
className: "dshr-navCount",
|
|
2637
|
-
children: combos.length
|
|
2638
|
-
})]
|
|
2639
|
-
}),
|
|
2640
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2641
|
-
type: "button",
|
|
2642
|
-
"aria-current": tab === "endpoint" ? "page" : void 0,
|
|
2643
|
-
onClick: () => setTab("endpoint"),
|
|
2644
|
-
children: "端点与密钥"
|
|
2645
|
-
})
|
|
2646
|
-
]
|
|
2647
|
-
}),
|
|
2648
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2649
|
-
className: "dshr-content",
|
|
2650
|
-
children: [
|
|
2651
|
-
tab === "suppliers" && (detailSupplier !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SupplierDetail, {
|
|
2652
|
-
supplier: detailSupplier,
|
|
2653
|
-
accounts,
|
|
2654
|
-
statusLoading: snapshot.status === null,
|
|
2655
|
-
onBack: () => setDetailSupplier(null),
|
|
2656
|
-
onRefresh: () => void refresh()
|
|
2657
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2658
|
-
className: "dshr-tabBody",
|
|
2659
|
-
children: [
|
|
2660
|
-
suppliers.length === 0 && snapshot.health !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2661
|
-
className: "dshr-empty",
|
|
2662
|
-
children: "暂无供应商"
|
|
2663
|
-
}),
|
|
2664
|
-
renderSupplierGroup("内置", suppliers.filter((s) => s.source !== "external")),
|
|
2665
|
-
renderSupplierGroup("插件", suppliers.filter((s) => s.source === "external")),
|
|
2666
|
-
suppliersError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2667
|
-
className: "dshr-empty",
|
|
2668
|
-
children: suppliersError
|
|
2669
|
-
})
|
|
2670
|
-
]
|
|
2671
|
-
})),
|
|
2672
|
-
tab === "combos" && (snapshot.combos === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2673
|
-
className: "dshr-tabBody",
|
|
2674
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3453
|
+
children: "返回会话"
|
|
3454
|
+
})
|
|
3455
|
+
]
|
|
3456
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3457
|
+
className: "dshr-settingsBody",
|
|
3458
|
+
children: [
|
|
3459
|
+
tab === "overview" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTab, {
|
|
3460
|
+
active: true,
|
|
3461
|
+
refreshing,
|
|
3462
|
+
health: snapshot.health,
|
|
3463
|
+
onRefresh: () => void refresh()
|
|
3464
|
+
}),
|
|
3465
|
+
tab === "suppliers" && (detailSupplier !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SupplierDetail, {
|
|
3466
|
+
supplier: detailSupplier,
|
|
3467
|
+
accounts,
|
|
3468
|
+
statusLoading: snapshot.status === null,
|
|
3469
|
+
onBack: () => setDetailSupplier(null),
|
|
3470
|
+
onRefresh: () => void refresh()
|
|
3471
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3472
|
+
className: "dshr-tabBody",
|
|
3473
|
+
children: [
|
|
3474
|
+
suppliers.length === 0 && snapshot.health !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2675
3475
|
className: "dshr-empty",
|
|
2676
|
-
children: "
|
|
2677
|
-
})
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
3476
|
+
children: "暂无供应商"
|
|
3477
|
+
}),
|
|
3478
|
+
renderSupplierGroup("内置", suppliers.filter((s) => s.source !== "external")),
|
|
3479
|
+
renderSupplierGroup("插件", suppliers.filter((s) => s.source === "external")),
|
|
3480
|
+
suppliersError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2681
3481
|
className: "dshr-empty",
|
|
2682
|
-
children:
|
|
3482
|
+
children: suppliersError
|
|
2683
3483
|
})
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3484
|
+
]
|
|
3485
|
+
})),
|
|
3486
|
+
tab === "combos" && (snapshot.combos === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3487
|
+
className: "dshr-tabBody",
|
|
3488
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3489
|
+
className: "dshr-empty",
|
|
3490
|
+
children: "加载中…"
|
|
3491
|
+
})
|
|
3492
|
+
}) : combosError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3493
|
+
className: "dshr-tabBody",
|
|
3494
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3495
|
+
className: "dshr-empty",
|
|
3496
|
+
children: combosError
|
|
3497
|
+
})
|
|
3498
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CombosTab, {
|
|
3499
|
+
combos,
|
|
3500
|
+
aliases: snapshot.combos?.aliases ?? [],
|
|
3501
|
+
onRefresh: () => void refresh()
|
|
3502
|
+
})),
|
|
3503
|
+
tab === "endpoint" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EndpointTab, {})
|
|
3504
|
+
]
|
|
3505
|
+
})]
|
|
2693
3506
|
});
|
|
2694
3507
|
}
|
|
2695
3508
|
//#endregion
|
|
@@ -2789,9 +3602,9 @@ window.__ModuleLoader__.load({
|
|
|
2789
3602
|
let root;
|
|
2790
3603
|
let placed = false;
|
|
2791
3604
|
const syncLabel = () => {
|
|
2792
|
-
if (entry.getAttribute("aria-label") !== "
|
|
2793
|
-
if (entry.title !== "
|
|
2794
|
-
if (label.textContent !== "
|
|
3605
|
+
if (entry.getAttribute("aria-label") !== "路由") entry.setAttribute("aria-label", ROUTER_TITLE);
|
|
3606
|
+
if (entry.title !== "路由") entry.title = ROUTER_TITLE;
|
|
3607
|
+
if (label.textContent !== "路由") label.textContent = ROUTER_TITLE;
|
|
2795
3608
|
};
|
|
2796
3609
|
const rootObserver = new MutationObserver(() => {
|
|
2797
3610
|
if (root === void 0 || !root.isConnected) {
|
|
@@ -2926,13 +3739,6 @@ window.__ModuleLoader__.load({
|
|
|
2926
3739
|
}
|
|
2927
3740
|
function mountPanel(controller) {
|
|
2928
3741
|
function Panel() {
|
|
2929
|
-
(0, react.useEffect)(() => {
|
|
2930
|
-
const original = document.title;
|
|
2931
|
-
document.title = ROUTER_TITLE;
|
|
2932
|
-
return () => {
|
|
2933
|
-
document.title = original;
|
|
2934
|
-
};
|
|
2935
|
-
}, []);
|
|
2936
3742
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, { onBack: () => controller.close() });
|
|
2937
3743
|
}
|
|
2938
3744
|
let root;
|
|
@@ -3010,17 +3816,117 @@ window.__ModuleLoader__.load({
|
|
|
3010
3816
|
};
|
|
3011
3817
|
}
|
|
3012
3818
|
//#endregion
|
|
3819
|
+
//#region src/client/settings-section.tsx
|
|
3820
|
+
/**
|
|
3821
|
+
* 「设置 → 路由」页 —— 走 DSH 官方 `settings.section` 座位。
|
|
3822
|
+
*
|
|
3823
|
+
* 容器几何(浏览器实测,不是估的)决定了这里的写法:
|
|
3824
|
+
* - 内容宽 564px(面板 612 − 左右 padding 24)
|
|
3825
|
+
* - 外壳 `.VOzbGW_options` 是 `overflow-y:auto` 且高度确定,所以**根节点
|
|
3826
|
+
* 限高**(`height:100%`)+ 内容区自己滚,才能做到「tab 条固定、只有
|
|
3827
|
+
* 内容区滚」。不限高就会退化成整页滚,tab 条跟着滚上去
|
|
3828
|
+
* - 根节点外面套了一层 `display: contents`(宿主 slot renderer 给的),
|
|
3829
|
+
* 但实测 `height:100%` 能穿过它解析到外壳高度
|
|
3830
|
+
* - 横向会溢出:老布局是按整个中心栏宽做的,卡片 grid 必须按 564px 排
|
|
3831
|
+
*
|
|
3832
|
+
* 组件本身无状态、无 props:数据全在 RouterView 里自己 fetch。
|
|
3833
|
+
*/
|
|
3834
|
+
/** 设置页入口组件。 */
|
|
3835
|
+
function RouterSettingsSection() {
|
|
3836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, {});
|
|
3837
|
+
}
|
|
3838
|
+
//#endregion
|
|
3839
|
+
//#region src/client/settings-nav-icon.ts
|
|
3840
|
+
/**
|
|
3841
|
+
* 给设置导航里「路由」这一行换掉宿主的默认齿轮。
|
|
3842
|
+
*
|
|
3843
|
+
* 为什么需要这个:DSH 0.1.x 的 `settings.section` 注册**只认 id / order / label
|
|
3844
|
+
* 三个字段**,图标是宿主内部按「内置 id 白名单」选的,插件传不了。所以外部
|
|
3845
|
+
* section 一律拿到通用齿轮(跟「通用设置」「远程访问」同一个)。
|
|
3846
|
+
* 在契约长出 icon 字段之前,只能注册后认领自己的那一行。
|
|
3847
|
+
*
|
|
3848
|
+
* 做法跟 dsh-better-sidebar 的 settings-nav-icon.ts 一致:给自己的行打一个
|
|
3849
|
+
* 属性标记,再由 CSS 用 mask 渲染图标(见 router.css 的
|
|
3850
|
+
* `[data-dsh-router-settings-nav]`)。用 mask + currentColor 是为了跟随
|
|
3851
|
+
* 宿主导航的 hover/active 配色,也保持 16px 的图标节奏。
|
|
3852
|
+
*
|
|
3853
|
+
* 匹配方式:按行的**可见文案**认领(label 可能本地化),且只认 `nav button`,
|
|
3854
|
+
* 碰到同名的别人家 section 不会误伤(我们同时校验它确实在设置面板里)。
|
|
3855
|
+
* 卸载时断开观察并摘掉标记,HMR / 插件热卸载安全。
|
|
3856
|
+
*/
|
|
3857
|
+
/** 打在设置导航按钮上的标记属性。 */
|
|
3858
|
+
var SETTINGS_NAV_MARKER = "data-dsh-router-settings-nav";
|
|
3859
|
+
/** 设置面板的导航按钮选择器(宿主结构:dialog 里的 nav)。 */
|
|
3860
|
+
var NAV_BUTTON_SELECTOR = "[role=\"dialog\"] nav button";
|
|
3861
|
+
/**
|
|
3862
|
+
* 持续把标记打在文案等于 `label` 的那一行的身上。
|
|
3863
|
+
* @param label 当前本地化的 section 标题(跟注册时的 label 一致)
|
|
3864
|
+
* @returns 清理函数:断开观察 + 摘掉所有本插件打的标记
|
|
3865
|
+
*/
|
|
3866
|
+
function registerSettingsNavIcon(label) {
|
|
3867
|
+
let disposed = false;
|
|
3868
|
+
const wanted = label.trim();
|
|
3869
|
+
const sync = () => {
|
|
3870
|
+
if (disposed || wanted === "") return;
|
|
3871
|
+
for (const button of document.querySelectorAll(NAV_BUTTON_SELECTOR)) if (button.textContent?.trim() === wanted) button.setAttribute(SETTINGS_NAV_MARKER, "");
|
|
3872
|
+
else button.removeAttribute(SETTINGS_NAV_MARKER);
|
|
3873
|
+
};
|
|
3874
|
+
sync();
|
|
3875
|
+
const observer = new MutationObserver(sync);
|
|
3876
|
+
observer.observe(document.body, {
|
|
3877
|
+
childList: true,
|
|
3878
|
+
subtree: true,
|
|
3879
|
+
characterData: true
|
|
3880
|
+
});
|
|
3881
|
+
return () => {
|
|
3882
|
+
disposed = true;
|
|
3883
|
+
observer.disconnect();
|
|
3884
|
+
for (const el of document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`)) el.removeAttribute(SETTINGS_NAV_MARKER);
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
//#endregion
|
|
3013
3888
|
//#region src/client/index.tsx
|
|
3014
3889
|
/**
|
|
3015
|
-
* Client half of dsh-router
|
|
3016
|
-
*
|
|
3017
|
-
*
|
|
3890
|
+
* Client half of dsh-router.
|
|
3891
|
+
*
|
|
3892
|
+
* 挂载点是「设置 → 路由」(官方 `settings.section` 座位),不再是侧边栏
|
|
3893
|
+
* DOM 注入 + 中心栏劫持。`slots` / `locale` 由宿主运行时提供(profile 的
|
|
3894
|
+
* node_modules 里 @deepseek-ai 是空的:这些包不入 node_modules,按
|
|
3895
|
+
* package.json 的 dsh.client.inject 声明即可)。
|
|
3896
|
+
*
|
|
3897
|
+
* 拿不到 slots 时(老宿主)回退到原来的中心栏面板,不至于整个插件失效。
|
|
3018
3898
|
*/
|
|
3019
|
-
/**
|
|
3020
|
-
|
|
3021
|
-
|
|
3899
|
+
/**
|
|
3900
|
+
* 设置导航里这一页的显示名。同时是注册 label 和「认领图标」时匹配的文案,
|
|
3901
|
+
* 两处必须一致 —— 认领逻辑按可见文案找自己那一行(见 settings-nav-icon.ts)。
|
|
3902
|
+
*/
|
|
3903
|
+
var SECTION_LABEL = "路由";
|
|
3904
|
+
/**
|
|
3905
|
+
* 必须声明:Cordis 的 ctx 是 Proxy,未声明就访问 `ctx.slots` 会直接抛
|
|
3906
|
+
* `cannot get property "slots" without inject`(实测错误,不是理论风险)——
|
|
3907
|
+
* 连 `ctx.slots === undefined` 这种防御性判断都跑不到,getter 先炸。
|
|
3908
|
+
*/
|
|
3909
|
+
var inject = ["slots"];
|
|
3910
|
+
/** 挂载「设置 → 路由」页;拿不到 slots 时返回 undefined 表示跳过。 */
|
|
3911
|
+
function mountSettingsSection(ctx) {
|
|
3912
|
+
const slots = ctx.slots;
|
|
3913
|
+
if (slots === void 0) return void 0;
|
|
3914
|
+
return slots.inject("settings.section", () => slots.register({
|
|
3915
|
+
name: "settings.section",
|
|
3916
|
+
id: "dsh-router",
|
|
3917
|
+
order: 10,
|
|
3918
|
+
label: SECTION_LABEL
|
|
3919
|
+
}, RouterSettingsSection));
|
|
3920
|
+
}
|
|
3022
3921
|
function apply(ctx) {
|
|
3023
|
-
|
|
3922
|
+
const disposeSettings = mountSettingsSection(ctx);
|
|
3923
|
+
if (disposeSettings === void 0) {
|
|
3924
|
+
console.warn("[dsh-router] 宿主未提供 slots,回退到侧边栏面板");
|
|
3925
|
+
ctx.effect(() => mountRouterWorkspace(), "dsh-router: routing system workspace");
|
|
3926
|
+
return;
|
|
3927
|
+
}
|
|
3928
|
+
ctx.effect(() => disposeSettings, "dsh-router: settings section");
|
|
3929
|
+
ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
|
|
3024
3930
|
}
|
|
3025
3931
|
//#endregion
|
|
3026
3932
|
exports.apply = apply;
|