dsh-router-core 0.3.21 → 0.4.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 +238 -63
- package/cordis.patch.yml +22 -0
- package/docs/ext.md +28 -5
- package/docs/pool-sticky-block.md +158 -0
- package/docs/suppliers.md +28 -5
- package/lib/client-registry.js +1300 -549
- package/lib/client.js +1300 -549
- package/lib/ext-test/index.js +47 -0
- package/lib/ext-test/locale/en.json +6 -0
- package/lib/ext-test/locale/zh.json +6 -0
- package/lib/ext-test/package.json +5 -0
- package/lib/http-state-C1vb3gW2.js +48 -0
- package/lib/index.js +1169 -254
- package/lib/suppliers/{nvidia.js → nvidia/index.js} +14 -6
- package/lib/suppliers/nvidia/locale/en.json +6 -0
- package/lib/suppliers/nvidia/locale/zh.json +6 -0
- package/lib/suppliers/nvidia/package.json +5 -0
- package/lib/suppliers/{opencode.js → opencode/index.js} +14 -6
- package/lib/suppliers/opencode/locale/en.json +6 -0
- package/lib/suppliers/opencode/locale/zh.json +6 -0
- package/lib/suppliers/opencode/package.json +5 -0
- package/lib/suppliers/{openrouter.js → openrouter/index.js} +14 -6
- package/lib/suppliers/openrouter/locale/en.json +6 -0
- package/lib/suppliers/openrouter/locale/zh.json +6 -0
- package/lib/suppliers/openrouter/package.json +5 -0
- package/lib/types/ext/contract.d.ts +9 -7
- package/lib/types/ext-test/index.d.ts +21 -0
- package/lib/types/ext-test/plugin.d.ts +23 -0
- package/lib/types/host-version.d.ts +15 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/llm/adapter.d.ts +35 -4
- package/lib/types/router/account-pool.d.ts +53 -18
- package/lib/types/router/index.d.ts +65 -15
- package/lib/types/router/prefix-affinity.d.ts +18 -1
- package/lib/types/router/types.d.ts +7 -4
- package/lib/types/router/usage-store.d.ts +12 -0
- package/lib/types/shared.d.ts +25 -21
- package/lib/types/supplier-config.d.ts +15 -3
- package/lib/types/suppliers/contract.d.ts +37 -4
- package/lib/types/suppliers/http-state.d.ts +23 -0
- package/lib/types/suppliers/loader.d.ts +6 -4
- package/lib/types/suppliers/nvidia/index.d.ts +18 -0
- package/lib/types/suppliers/nvidia/plugin.d.ts +13 -0
- package/lib/types/suppliers/opencode/index.d.ts +18 -0
- package/lib/types/suppliers/opencode/plugin.d.ts +7 -0
- package/lib/types/suppliers/openrouter/index.d.ts +18 -0
- package/lib/types/suppliers/openrouter/plugin.d.ts +11 -0
- package/lib/types/suppliers/probe.d.ts +160 -0
- package/lib/types/suppliers/registry.d.ts +3 -2
- package/package.json +50 -16
- package/src/client/CombosTab.tsx +8 -53
- package/src/client/ExtDetail.tsx +32 -196
- package/src/client/ExtTab.tsx +25 -42
- package/src/client/ExtTestPanel.tsx +392 -0
- package/src/client/LastHitDock.tsx +141 -0
- package/src/client/RouterComponentsSection.tsx +241 -0
- package/src/client/RouterView.tsx +12 -5
- package/src/client/StatsTab.tsx +8 -6
- package/src/client/SupplierDetail.tsx +2 -34
- package/src/client/comp-button-scope.test.ts +58 -0
- package/src/client/comp-panel-layout.test.ts +58 -0
- package/src/client/env.d.ts +38 -1
- package/src/client/ext-panels.test.ts +44 -0
- package/src/client/ext-panels.ts +49 -0
- package/src/client/index.tsx +86 -0
- package/src/client/last-hit-store.ts +89 -0
- package/src/client/model-choice.test.ts +41 -0
- package/src/client/model-choice.ts +37 -0
- package/src/client/model-hint-copy.test.ts +7 -2
- package/src/client/model-hint-copy.ts +2 -2
- package/src/client/router-components.test.ts +114 -0
- package/src/client/router-components.ts +104 -0
- package/src/client/router.css +2592 -1635
- package/src/client/supplier-visibility.test.ts +57 -0
- package/src/client/supplier-visibility.ts +63 -0
- package/src/ext/contract.ts +9 -7
- package/src/ext-test/ext-test.test.ts +60 -0
- package/src/ext-test/index.ts +35 -0
- package/src/ext-test/locale/en.json +6 -0
- package/src/ext-test/locale/zh.json +6 -0
- package/src/ext-test/plugin.ts +34 -0
- package/src/ext-test/row.json +5 -0
- package/src/host-version.test.ts +61 -0
- package/src/host-version.ts +101 -0
- package/src/index.ts +165 -68
- package/src/llm/adapter.test.ts +175 -4
- package/src/llm/adapter.ts +109 -16
- package/src/router/account-pool.test.ts +189 -83
- package/src/router/account-pool.ts +120 -31
- package/src/router/disconnect-leak.test.ts +7 -12
- package/src/router/first-byte-budget.test.ts +160 -0
- package/src/router/index.ts +276 -62
- package/src/router/models-cache.test.ts +54 -9
- package/src/router/precise-dispatch.test.ts +6 -6
- package/src/router/prefix-affinity.ts +22 -1
- package/src/router/supplier-switch.test.ts +220 -0
- package/src/router/types.ts +7 -5
- package/src/router/usage-store.ts +16 -0
- package/src/router/usage.test.ts +1 -1
- package/src/shared.ts +25 -24
- package/src/supplier-config.ts +33 -13
- package/src/suppliers/contract.ts +56 -4
- package/src/suppliers/loader.test.ts +38 -4
- package/src/suppliers/loader.ts +33 -18
- package/src/suppliers/nvidia/index.ts +27 -0
- package/src/suppliers/nvidia/locale/en.json +6 -0
- package/src/suppliers/nvidia/locale/zh.json +6 -0
- package/src/suppliers/nvidia/row.json +5 -0
- package/src/suppliers/opencode/index.ts +27 -0
- package/src/suppliers/opencode/locale/en.json +6 -0
- package/src/suppliers/opencode/locale/zh.json +6 -0
- package/src/suppliers/opencode/row.json +5 -0
- package/src/suppliers/openrouter/index.ts +27 -0
- package/src/suppliers/openrouter/locale/en.json +6 -0
- package/src/suppliers/openrouter/locale/zh.json +6 -0
- package/src/suppliers/openrouter/row.json +5 -0
- package/src/suppliers/probe.test.ts +266 -0
- package/src/suppliers/probe.ts +475 -0
- package/src/suppliers/registry.ts +103 -23
- package/src/types.d.ts +2 -1
package/lib/client-registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function() { try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); 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-toggle:disabled {\n cursor: default;\n opacity: .45;\n}\n\n/* 扩展页:未就绪问题描述(红字) */\n.dshr-ext-error {\n color: var(--rs-danger);\n font-size: 11px;\n margin-top: 4px;\n}\n\n/* watch 进程行动作区(复制 PID / kill 命令,只读) */\n.dshr-procOps {\n display: inline-flex;\n align-items: center;\n justify-content: flex-end;\n gap: 4px;\n white-space: nowrap;\n}\n\n/* 扩展供应商式小卡片:未就绪红字(描述只在详情页,卡片不放) */\n.dshr-extCardError {\n color: var(--rs-danger);\n font-size: 11px;\n}\n\n/* 扩展卡 logo(如 rtk 白底 logo):黑底 + 内边距,白色 logo 在黑底上显形 */\n.dshr-extLogo {\n background: #000;\n padding: 3px;\n box-sizing: border-box;\n object-fit: contain;\n}\n\n/* 扩展详情头部右侧的开关(顶到标题行右边,同供应商详情的编辑按钮位) */\n.dshr-extDetailToggle {\n margin-left: auto;\n}\n\n/* 扩展卡片内容区:文字左对齐,左侧留白与标题行的图标位对齐 */\n.dshr-extBody {\n padding: 10px 14px 12px 48px;\n text-align: left;\n}\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/* 改名:与删除一致,hover 行才显示;编辑中保持可见(否则保存按钮被藏住) */\n.dshr-linkEdit {\n color: var(--rs-muted);\n opacity: 0;\n transition: opacity .12s ease, color .12s ease, background .12s ease;\n}\n.dshr-linkRow:hover .dshr-linkEdit { opacity: 1; }\n.dshr-linkEdit:focus-visible { opacity: 1; }\n.dshr-linkEdit:hover {\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n}\n.dshr-linkEdit.dshr-linkEdit-active { opacity: 1; }\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*/")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); }})();
|
|
1
|
+
(function() { try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); 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(\n --ds-font-family-code,\n \"SFMono-Regular\",\n Consolas,\n monospace\n );\n --rs-sans: var(\n --dsw-font-family,\n -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif\n );\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 {\n box-sizing: border-box;\n}\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 0.12s ease;\n}\n\n.dshr-tab:hover {\n color: var(--rs-text);\n}\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 {\n color: var(--rs-text);\n background: var(--rs-hover);\n}\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\"] {\n position: relative;\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(\n [data-dsh-ssh-active]\n ):not([data-dsh-mnemon-active])\n [data-dsh-router-view] {\n display: block;\n}\n\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not(\n [data-dsh-ssh-active]\n ):not([data-dsh-mnemon-active])\n [data-pane=\"conversation\"]\n > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not(\n [data-dsh-ssh-active]\n ):not([data-dsh-mnemon-active])\n [class*=\"centerCol\"]\n > :not([data-dsh-router-view]),\nhtml[data-dsh-router-active]:not([data-dsh-taskboard-active]):not(\n [data-dsh-ssh-active]\n ):not([data-dsh-mnemon-active])\n .dshDesktopConversationSurface\n > :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 {\n box-sizing: border-box;\n}\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 {\n color: inherit;\n font: inherit;\n}\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 {\n color: var(--rs-text);\n background: var(--rs-hover);\n}\n.dshr-iconButton:disabled {\n opacity: 0.5;\n cursor: default;\n}\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 {\n background: var(--rs-hover);\n}\n.dshr-backButton svg {\n display: block;\n}\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 {\n color: var(--rs-danger);\n font-size: 12px;\n}\n.dshr-alert span {\n color: var(--rs-muted);\n font-size: 12px;\n}\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 {\n color: var(--rs-text);\n}\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/* 两行空状态(标题 + 说明)。`.dshr-empty` 本身是横排的单行 flex,直接套两行\n div 会左右排开,所以另给一个纵向堆叠的变体。 */\n.dshr-emptyStack {\n flex-direction: column;\n gap: 6px;\n text-align: center;\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 {\n background: var(--rs-hover);\n}\n\n.dshr-mono {\n font-family: var(--rs-mono);\n font-size: 11px;\n}\n.dshr-num {\n text-align: right;\n}\n.dshr-muted {\n color: var(--rs-faint);\n}\n.dshr-reason {\n color: var(--rs-muted);\n font-size: 11px;\n}\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 {\n color: var(--rs-success);\n background: color-mix(in srgb, var(--rs-success) 12%, transparent);\n}\n.dshr-badge-warn {\n color: var(--rs-warn);\n background: color-mix(in srgb, var(--rs-warn) 14%, transparent);\n}\n.dshr-badge-danger {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 12%, transparent);\n}\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 {\n border-color: var(--rs-line-strong);\n}\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 {\n color: var(--rs-text);\n background: var(--rs-hover);\n}\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 0.12s ease;\n}\n\n.dshr-card-click:hover {\n border-color: var(--rs-accent);\n}\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: 0.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 0.12s ease;\n}\n\n.dshr-supplierCard:hover {\n border-color: var(--rs-accent);\n}\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: 0.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 0.15s ease;\n}\n\n.dshr-toggle-on {\n background: var(--rs-ok);\n}\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 0.15s ease;\n}\n\n.dshr-toggle-on .dshr-toggleKnob {\n transform: translateX(14px);\n}\n\n/* 开关禁用态:不可用(未就绪)且未开启时,灰显 + 不可点 */\n.dshr-toggle:disabled {\n cursor: default;\n opacity: 0.45;\n}\n\n/* 扩展页:未就绪问题描述(红字) */\n.dshr-ext-error {\n color: var(--rs-danger);\n font-size: 11px;\n margin-top: 4px;\n}\n\n/* 扩展供应商式小卡片:未就绪红字(描述只在详情页,卡片不放) */\n.dshr-extCardError {\n color: var(--rs-danger);\n font-size: 11px;\n}\n\n/* 扩展卡片内容区:文字左对齐,左侧留白与标题行的图标位对齐 */\n.dshr-extBody {\n padding: 10px 14px 12px 48px;\n text-align: left;\n}\n\n/* 操作按钮组 */\n.dshr-ops {\n display: flex;\n gap: 6px;\n white-space: nowrap;\n}\n\n.dshr-danger {\n color: var(--rs-danger);\n}\n.dshr-danger:hover {\n color: var(--rs-danger);\n border-color: var(--rs-danger);\n}\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 0.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:\n 0 20px 50px rgb(0 0 0 / 0.35),\n 0 0 0 1px rgb(0 0 0 / 0.05);\n animation: dshrModalIn 0.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 {\n color: var(--rs-text);\n background: var(--rs-hover);\n}\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 {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n@keyframes dshrModalIn {\n from {\n opacity: 0;\n transform: translateY(8px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: none;\n }\n}\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 0.12s ease;\n}\n\n.dshr-settings .dshr-primaryButton:hover,\n.dshr-shell .dshr-primaryButton:hover {\n filter: brightness(1.08);\n}\n.dshr-settings .dshr-primaryButton:disabled,\n.dshr-shell .dshr-primaryButton:disabled {\n opacity: 0.55;\n cursor: default;\n}\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 0.12s ease;\n}\n\n.dshr-settings .dshr-dangerButton:hover,\n.dshr-shell .dshr-dangerButton:hover {\n filter: brightness(1.1);\n}\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(\n to bottom,\n color-mix(in srgb, var(--rs-layer-1) 62%, transparent),\n transparent\n );\n}\n\n/* Brand 标题更突出 */\n.dshr-brandTitle {\n letter-spacing: 0.01em;\n}\n\n/* 卡片:阴影 + hover 微抬升 */\n.dshr-card {\n box-shadow: 0 1px 2px rgb(0 0 0 / 0.04);\n transition:\n box-shadow 0.15s ease,\n transform 0.15s ease,\n border-color 0.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 / 0.1);\n}\n\n/* Tab:悬停背景 + 计数徽标配色 */\n.dshr-nav button {\n transition:\n color 0.12s ease,\n background 0.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\n color-mix(in srgb, currentColor 12%, transparent);\n}\n\n/* 表格行悬停 + 表头层级 */\n.dshr-table tbody tr {\n transition: background 0.1s ease;\n}\n.dshr-table th {\n letter-spacing: 0.02em;\n}\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 {\n transition:\n background 0.15s ease,\n box-shadow 0.15s ease;\n}\n.dshr-toggle:hover {\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--rs-accent) 18%, transparent);\n}\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:\n color 0.12s ease,\n background 0.12s ease;\n}\n\n.dshr-iconBtn:hover {\n color: var(--rs-accent);\n background: var(--rs-hover);\n}\n\n.dshr-iconBtn svg {\n display: block;\n}\n\n.dshr-iconBtn-sm {\n width: 26px;\n height: 26px;\n}\n\n/* 禁用态必须看得出来:纯图标按钮没有文字可读(不像「刷新中…」),\n 只靠 hover 反馈会让人以为还能点。 */\n.dshr-iconBtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n.dshr-iconBtn:disabled:hover {\n color: var(--rs-muted);\n background: transparent;\n}\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 0.12s ease;\n}\n\n.dshr-keyRow:last-child {\n border-bottom: none;\n}\n.dshr-keyRow-paused {\n opacity: 0.6;\n}\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 {\n color: var(--rs-warn);\n}\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:\n opacity 0.12s ease,\n color 0.12s ease,\n background 0.12s ease;\n}\n\n.dshr-keyRow:hover .dshr-keyDelete {\n opacity: 1;\n}\n.dshr-keyDelete:focus-visible {\n opacity: 1;\n}\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 {\n padding: 0;\n}\n\n.dshr-createdRow .dshr-input {\n color: var(--rs-text);\n}\n\n.dshr-fullButton {\n width: 100%;\n}\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 0.12s ease;\n}\n\n.dshr-backLink:hover {\n color: var(--rs-accent);\n}\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: -0.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 {\n text-align: center;\n}\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 0.1s ease;\n}\n\n.dshr-linkRow:last-child {\n border-bottom: none;\n}\n.dshr-linkRow:hover {\n background: var(--rs-hover);\n}\n.dshr-linkRow[draggable=\"true\"] {\n cursor: grab;\n}\n.dshr-linkRow[draggable=\"true\"]:active {\n cursor: grabbing;\n}\n.dshr-linkRowDragging {\n opacity: 0.45;\n background: var(--rs-hover);\n}\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 {\n cursor: grabbing;\n}\n.dshr-linkGrip svg {\n opacity: 0.65;\n}\n.dshr-linkRow:hover .dshr-linkGrip svg {\n opacity: 1;\n}\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 {\n color: var(--rs-faint);\n}\n.dshr-linkCredits {\n color: var(--rs-muted);\n}\n.dshr-linkErr {\n color: var(--rs-warn);\n}\n.dshr-linkCooling {\n color: var(--rs-warn);\n}\n.dshr-linkDot {\n color: var(--rs-faint);\n opacity: 0.6;\n}\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:\n opacity 0.12s ease,\n color 0.12s ease,\n background 0.12s ease;\n}\n\n.dshr-linkRow:hover .dshr-linkDelete {\n opacity: 1;\n}\n.dshr-linkDelete:focus-visible {\n opacity: 1;\n}\n.dshr-linkDelete:hover {\n color: var(--rs-danger);\n background: color-mix(in srgb, var(--rs-danger) 14%, transparent);\n}\n\n/* 改名:与删除一致,hover 行才显示;编辑中保持可见(否则保存按钮被藏住) */\n.dshr-linkEdit {\n color: var(--rs-muted);\n opacity: 0;\n transition:\n opacity 0.12s ease,\n color 0.12s ease,\n background 0.12s ease;\n}\n.dshr-linkRow:hover .dshr-linkEdit {\n opacity: 1;\n}\n.dshr-linkEdit:focus-visible {\n opacity: 1;\n}\n.dshr-linkEdit:hover {\n color: var(--rs-accent);\n background: color-mix(in srgb, var(--rs-accent) 14%, transparent);\n}\n.dshr-linkEdit.dshr-linkEdit-active {\n opacity: 1;\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:\n border-color 0.12s ease,\n background 0.12s ease,\n color 0.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 {\n opacity: 0.6;\n cursor: default;\n}\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: 0.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-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:\n linear-gradient(45deg, transparent 50%, var(--rs-muted) 50%),\n linear-gradient(135deg, var(--rs-muted) 50%, transparent 50%);\n background-position:\n calc(100% - 12px) 55%,\n calc(100% - 7px) 55%;\n background-size: 5px 5px;\n background-repeat: no-repeat;\n}\n.dshr-select:focus {\n outline: none;\n 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:\n border-color 0.12s ease,\n background 0.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 {\n border-color: var(--rs-ok);\n}\n.dshr-modelChipWrap.dshr-modelChip-err {\n border-color: var(--rs-danger);\n}\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 {\n color: var(--rs-ok);\n}\n.dshr-modelChipWrap.dshr-modelChip-err .dshr-modelChipStatus {\n color: var(--rs-danger);\n}\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 0.12s ease;\n}\n\n.dshr-modelChipWrap:hover .dshr-modelChipOps,\n.dshr-modelChipOps:focus-within {\n opacity: 1;\n}\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:\n border-color 0.12s ease,\n background 0.12s ease,\n color 0.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 {\n opacity: 0.6;\n cursor: default;\n}\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: 0.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 0.8s linear infinite;\n}\n\n@keyframes dshrSpin {\n to {\n transform: rotate(360deg);\n }\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:\n color 0.12s ease,\n background 0.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 / 0.18);\n font-size: 12px;\n font-weight: 500;\n white-space: nowrap;\n animation: dshrToastIn 0.18s ease;\n}\n\n@keyframes dshrToastIn {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(8px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n/* ============ 组合(9router combos 布局交互) ============ */\n.dshr-comboIntro {\n overflow: visible;\n}\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 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dshr-comboCard {\n display: flex;\n flex-direction: column;\n}\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 {\n color: var(--rs-danger);\n}\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 {\n font-style: italic;\n font-size: 12px;\n}\n\n.dshr-comboMore {\n font-size: 10px;\n color: var(--rs-muted);\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: 380px;\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 {\n cursor: grabbing;\n}\n\n.dshr-comboStepRow-dragging {\n opacity: 0.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 {\n flex: 1;\n min-width: 0;\n}\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:\n border-color 0.12s ease,\n background 0.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 {\n margin-top: 12px;\n}\n\n.dshr-comboModelSearch {\n margin-bottom: 6px;\n}\n\n.dshr-comboPickList {\n display: flex;\n flex-direction: column;\n gap: 2px;\n max-height: 300px;\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 0.12s ease;\n}\n\n.dshr-comboPickItem:hover {\n background: var(--rs-hover);\n}\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 {\n margin-bottom: 6px;\n}\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: 460px;\n}\n\n.dshr-comboFormRight .dshr-comboPickList {\n flex: 1;\n min-height: 0;\n}\n\n/* 组合编辑弹窗:加宽加高(body 更高,内部列表同步放大) */\n.dshr-modal-wide {\n width: min(760px, 100%);\n max-height: min(90vh, 820px);\n}\n\n.dshr-modal-wide .dshr-modalBody {\n min-height: 560px;\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:\n color 0.12s ease,\n background 0.12s ease,\n filter 0.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 / 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 / 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: 0.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: -0.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 {\n color: var(--rs-accent);\n}\n.dshr-statCard-ok .dshr-statValue {\n color: var(--rs-success);\n}\n.dshr-statCard-warn .dshr-statValue {\n color: var(--rs-warn);\n}\n.dshr-statCard-danger .dshr-statValue {\n color: var(--rs-danger);\n}\n\n/* 签到卡:跟统计卡同宽同高,但里面是按钮不是数字。\n gap 沿用卡片的 3px(不覆写)—— 之前改成 6px 让这一行比别的高 6px,\n 两行卡片因此不齐(92 vs 98px)。\n 今日已点过 → 边框转成功色,一眼看出「今天点过了」 */\n.dshr-statCard-checkin {\n gap: 3px;\n}\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:\n color 0.12s ease,\n background 0.12s ease,\n border-color 0.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 {\n opacity: 0.6;\n cursor: default;\n}\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 {\n border-bottom: none;\n}\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: 0.03em;\n text-transform: uppercase;\n}\n\n.dshr-rankRow:not(.dshr-rankHead):hover {\n background: var(--rs-hover);\n}\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 {\n color: var(--rs-danger);\n}\n\n.dshr-tokenIn {\n color: var(--rs-accent);\n}\n.dshr-tokenOut {\n color: var(--rs-success);\n}\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-align: center;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* 最近请求的 In/Out 与耗时列:**固定宽度**(不是 min-width)——\n rankNum 的 min-width 会让表头(纯文本)与数据行(更长)算出的列宽不同,\n 两行的后一列就对不齐。固定宽度让表头与每一行严格同宽。 */\n.dshr-recentTokens {\n flex: none;\n width: 80px;\n overflow: hidden;\n text-align: right;\n text-overflow: ellipsis;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.dshr-recentDuration {\n flex: none;\n width: 40px;\n overflow: hidden;\n text-align: right;\n text-overflow: ellipsis;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.dshr-recentRow-fail .dshr-recentModel {\n color: var(--rs-danger);\n}\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 {\n background: var(--rs-success);\n}\n.dshr-dot-fail {\n background: var(--rs-danger);\n}\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>\")\n 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>\")\n center / contain no-repeat;\n}\n\n/* ============================================================\n last-hit —— 输入框底部 dock 里的「最近命中」徽章\n (LastHitDock.tsx 注册到官方 conversation.composer.dock,\n 即 .uV2eYG_dock 操作行里、原生上下文环旁边;scope: session)\n\n 与同排控件对齐:28px 高胶囊、hover 用 interactive-bg-hover、\n 图标 14px 用 label-tertiary;弹卡对齐原生 panel 规格\n (--dsw-specific-menu / --dsw-elevation-prominent / 12px 圆角 / 264px)。\n ============================================================ */\n\n.dshr-lastHit {\n position: relative;\n display: inline-flex;\n flex: none;\n align-items: center;\n}\n\n.dshr-lastHit-btn {\n position: relative;\n display: inline-flex;\n height: 28px;\n flex: none;\n align-items: center;\n gap: 6px;\n color: var(--dsw-alias-label-secondary);\n background: 0 0;\n border: none;\n border-radius: 999px;\n padding: 0 8px;\n font-size: 12px;\n line-height: 1;\n cursor: pointer;\n max-width: 220px;\n}\n\n.dshr-lastHit-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n/* 图标:与隔壁上下文环的描边同色(它用 label-tertiary 画进度弧),\n 尺寸 14px 也对齐该环的 svg —— 两个控件并排时看着是一套。 */\n.dshr-lastHit-btn svg {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dshr-lastHit-label {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-lastHit-fail {\n color: var(--dsw-alias-label-error, #e5534b);\n}\n\n.dshr-lastHit-fail svg {\n color: var(--dsw-alias-label-error, #e5534b);\n}\n\n.dshr-lastHit-card {\n position: absolute;\n right: 0;\n bottom: calc(100% + 8px);\n z-index: 100;\n box-sizing: border-box;\n width: 264px;\n border: 0;\n border-radius: 12px;\n padding: 12px;\n color: var(--dsw-alias-label-secondary);\n /* --dsw-specific-menu 是**半透明**色,原生 panel 靠 backdrop-filter 做毛玻璃;\n 缺了它只剩半透明、看着像没背景。这两条必须成对出现。 */\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-primary, #fff));\n backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));\n -webkit-backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%));\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent, 0 6px 24px rgba(0, 0, 0, 0.18));\n font-size: 12px;\n line-height: 20px;\n cursor: default;\n}\n\n/* 头部:照宿主聊天气泡的 .bRhRbq_title 规格\n (label-primary / space-between / gap16 / margin-bottom8 / font-weight500)\n 左标签带 14px 图标,右值 tabular-nums,下接一条 0.5px 分隔线。 */\n.dshr-lastHit-title {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.dshr-lastHit-titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dshr-lastHit-titleLabel svg {\n width: 14px;\n height: 14px;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dshr-lastHit-titleValue {\n overflow: hidden;\n font-variant-numeric: tabular-nums;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.dshr-lastHit-titleRule {\n border-top: .5px solid var(--dsw-alias-border-l2);\n margin-bottom: 10px;\n}\n\n.dshr-lastHit-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 2px 0;\n}\n\n.dshr-lastHit-key {\n flex: none;\n color: var(--dsw-alias-label-secondary);\n}\n\n.dshr-lastHit-val {\n overflow: hidden;\n color: var(--dsw-alias-label-primary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* ============================================================\n dsh-router — 官方插件页「路由组件」一节\n ------------------------------------------------------------\n 尺寸与配色照抄宿主 plugin-manager 的 detailSection/rows/row\n (14px 500 小标题、40px 圆角图标行、12px 行距),让这一节看起来\n 就是原生列表的一部分。\n\n 注意:--rs-* 变量只定义在 .dshr-settings / .dshr-shell 上,而这一节\n 渲染在**插件页**里(两者的 DOM 祖先不同),所以这里直接用宿主的\n --dsw-alias-* 原始变量,不走中间层。\n ============================================================ */\n\n.dshr-comp {\n display: flex;\n flex-direction: column;\n gap: 28px;\n min-width: 0;\n}\n\n.dshr-compError {\n margin: 0;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compGroup {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n}\n\n.dshr-compHead {\n display: flex;\n align-items: baseline;\n gap: 10px;\n}\n\n.dshr-compTitle {\n margin: 0;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.dshr-compCount {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compHint {\n margin: -4px 0 0;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compRows {\n display: flex;\n flex-direction: column;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.dshr-compRow {\n display: flex;\n align-items: center;\n gap: 16px;\n min-width: 0;\n border-bottom: 0.5px solid var(--dsw-alias-border-l2);\n padding: 12px 2px;\n}\n\n.dshr-compRow:last-child {\n border-bottom: 0;\n}\n\n.dshr-compRowIcon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: 10px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n}\n\n.dshr-compRowIcon img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.dshr-compRowMain {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.dshr-compRowName {\n color: var(--dsw-alias-label-primary);\n font-size: 13.5px;\n font-weight: 500;\n line-height: 20px;\n overflow-wrap: anywhere;\n}\n\n.dshr-compRowState {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11.5px;\n line-height: 16px;\n overflow-wrap: anywhere;\n}\n\n/* ============================================================\n dsh-router — 插件自检面板(设置 → 路由 → 扩展 → 插件自检)\n ------------------------------------------------------------\n 这一页渲染在**宿主的插件页**里,DOM 祖先既不是 .dshr-settings 也不是\n .dshr-shell,所以文件顶部那套 `--rs-*` 主题变量在这里**不存在**(未定义的\n var() 会让整条声明失效:边框/文字色退成继承值)。\n\n 所以这里**不用**设置页那套组件样式,直接按宿主页面的视觉语言写,用宿主的\n `--dsw-alias-*` token:几何对齐它(14px 小标题、12px 次要文字、0.5px 描边、\n 12px 圆角),配色也从同一套 token 取 —— 于是这块面板看起来就是原生的一部分。\n 主按钮不用在这里画:交给宿主的 Button 原语(见 env.d.ts),配色由原语自己的\n token 家族决定,不必猜「主色底上配什么字色」。\n ============================================================ */\n\n.dshr-comp {\n gap: 16px;\n}\n\n.dshr-compIntro {\n margin: 0;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n line-height: 20px;\n}\n\n/* 三栏收进同一块面板,读起来是「一组控件」而不是三个飘着的输入框。\n 窄了换行(详情区宽度随窗口变,写死三列会在窄窗口挤成一团)。 */\n.dshr-compForm {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 14px 16px;\n padding: 14px 16px;\n border: 0.5px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.dshr-compField {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n}\n\n.dshr-compFieldHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n.dshr-compFieldLabel {\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n\n/* 标题右侧的用法要点。语义放这里比塞进顶部那段长文字更容易被读到 ——\n 「模型只列可用的」「连接留空 = 池内任选」都是用这一栏时才关心的。 */\n.dshr-compFieldNote {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n text-align: right;\n}\n\n/* 36px / 12px 圆角,与宿主 Button 原语的 md 尺寸对齐。 */\n.dshr-compSelect {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 11px;\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background-color: var(--dsw-alias-bg-layer-2);\n background-image:\n linear-gradient(45deg, transparent 50%, var(--dsw-alias-label-tertiary) 50%),\n linear-gradient(135deg, var(--dsw-alias-label-tertiary) 50%, transparent 50%);\n background-position:\n calc(100% - 17px) 16px,\n calc(100% - 12px) 16px;\n background-size: 5px 5px;\n background-repeat: no-repeat;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n text-overflow: ellipsis;\n cursor: pointer;\n}\n\n.dshr-compSelect:hover:not(:disabled) {\n border-color: var(--dsw-alias-border-l3);\n}\n\n.dshr-compSelect:focus {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent);\n}\n\n.dshr-compSelect:disabled {\n border-color: var(--dsw-alias-border-l1);\n background-color: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-tertiary);\n cursor: not-allowed;\n}\n\n/* 动作行:把按钮与它旁边的提示排在一条基线上。两个动作行(访问测试 / 契约体检)\n 共用 `dshr-compBtnRow` —— 那条抬特异性的字色规则就挂在它上面(见下)。 */\n.dshr-compActions {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dshr-compHint {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n/* 结果区:成功/失败/请求失败 三种语气。底下垫一层极淡的状态色,比只有一行\n 彩色文字更容易扫到 —— 自检的结果是这一页唯一重要的信息。 */\n.dshr-compNotice {\n display: flex;\n gap: 10px;\n padding: 12px 14px;\n border: 0.5px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.dshr-compNotice[data-tone=\"ok\"] {\n border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 34%, transparent);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent);\n}\n\n.dshr-compNotice[data-tone=\"fail\"],\n.dshr-compNotice[data-tone=\"network\"] {\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 34%, transparent);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);\n}\n\n.dshr-compNoticeIcon {\n flex: none;\n width: 16px;\n height: 16px;\n margin-top: 2px;\n color: var(--dsw-alias-state-success-secondary);\n}\n\n.dshr-compNotice[data-tone=\"fail\"] .dshr-compNoticeIcon,\n.dshr-compNotice[data-tone=\"network\"] .dshr-compNoticeIcon {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.dshr-compNoticeBody {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.dshr-compNoticeTitle {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.dshr-compNoticeText {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n line-height: 19px;\n /* 上游可能回一整段 HTML/JSON,限定宽度换行,别把布局撑破 */\n overflow-wrap: anywhere;\n}\n\n/* 回显测的是哪三样。一条「通过」如果不带对象,就是没法归因的一串绿字。 */\n.dshr-compNoticeMeta {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n overflow-wrap: anywhere;\n}\n\n/* 宿主 Button 原语的 primary 想把字色设成 --dsw-alias-label-primary-foreground,\n 但上面那条兜底 `.dshr-settings button { color: inherit; font: inherit }`\n 特异性 (1,1) **高于**原语的 `.primary` (1,0) —— 赢的是它。于是按钮变成\n 「#0f1115 的底 + 继承来的 #0f1115 的字」,黑底黑字、按钮看着是空的。\n\n 按本文件既有的抬特异性办法修(见文件里 .dshr-tab-on / .dshr-period 那几处注释:\n 凡是受这条兜底影响的规则,都必须带挂载容器抬到 (2,0) 以上)。这里\n `.dshr-comp .dshr-compBtnRow button` 是 (2,1),不依赖源码顺序,以后有人挪块也不会翻车。\n\n 作用域挂在**动作行的共用类 `dshr-compBtnRow`** 上,不是某个具体容器:上一版这条\n 只写了 `.dshr-compActions`,随后新增的「契约体检」按钮在另一个容器里,于是又变回\n 黑底黑字。现在两个动作行共用这个类,再加按钮只需沿用它。\n\n 修的是**把原语的意图还回去**,不是自己另挑一个颜色:字色仍由原语那个 token 决定。 */\n.dshr-comp .dshr-compBtnRow button {\n color: var(--dsw-alias-label-primary-foreground);\n font-size: 14px;\n line-height: 22px;\n}\n\n/* --- 契约体检区(插件自检面板的第二块) ---\n 状态靠 data-state 上色,和「结果卡」那套语气一致:过了是绿、失败是红、\n 没实现是灰、不执行是中性(它是**故意**不跑的,不是问题)。 */\n\n.dshr-compSection {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin-top: 4px;\n padding-top: 16px;\n border-top: 0.5px solid var(--dsw-alias-border-l1);\n}\n\n.dshr-compHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n.dshr-compSectionTitle {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.dshr-compSectionNote {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compProbeRow {\n display: flex;\n align-items: flex-end;\n gap: 12px;\n}\n\n.dshr-compProbeRow .dshr-compField {\n flex: 1;\n}\n\n.dshr-compProbeBtn {\n flex: none;\n}\n\n.dshr-compReport {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px 14px;\n border: 0.5px solid var(--dsw-alias-border-l1);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.dshr-compReportHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.dshr-compReportName {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n}\n\n.dshr-compReportSum {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compMembers {\n display: flex;\n flex-direction: column;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.dshr-compMember {\n display: grid;\n grid-template-columns: 16px minmax(0, auto) minmax(0, 1fr);\n align-items: baseline;\n gap: 10px;\n padding: 5px 0;\n border-bottom: 0.5px solid var(--dsw-alias-border-l1);\n}\n\n.dshr-compMember:last-child {\n border-bottom: 0;\n}\n\n.dshr-compMemberState {\n flex: none;\n text-align: center;\n font-size: 12px;\n line-height: 18px;\n}\n\n.dshr-compMember[data-state=\"ok\"] .dshr-compMemberState {\n color: var(--dsw-alias-state-success-secondary);\n}\n\n.dshr-compMember[data-state=\"fail\"] .dshr-compMemberState {\n color: var(--dsw-alias-state-error-primary);\n}\n\n.dshr-compMember[data-state=\"absent\"] .dshr-compMemberState,\n.dshr-compMember[data-state=\"skipped\"] .dshr-compMemberState {\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dshr-compMemberName {\n color: var(--dsw-alias-label-primary);\n font-size: 12.5px;\n line-height: 19px;\n white-space: nowrap;\n}\n\n.dshr-compMemberKey {\n margin-left: 6px;\n color: var(--dsw-alias-label-tertiary);\n font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n font-size: 11px;\n}\n\n.dshr-compMemberDetail {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n overflow-wrap: anywhere;\n}\n\n\n/*$vite$:1*/")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); }})();
|
|
2
2
|
window.__ModuleLoader__.load({
|
|
3
3
|
id: "dsh-external/dsh-router",
|
|
4
4
|
factory: (require) => {
|
|
@@ -8,6 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react_dom_client = require("react-dom/client");
|
|
9
9
|
let react = require("react");
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
11
12
|
//#region src/shared.ts
|
|
12
13
|
/** Active-state attribute on <html> that shows the panel. */
|
|
13
14
|
var ROUTER_ACTIVE_ATTR = "data-dsh-router-active";
|
|
@@ -159,7 +160,6 @@ window.__ModuleLoader__.load({
|
|
|
159
160
|
const [modelQuery, setModelQuery] = (0, react.useState)("");
|
|
160
161
|
const [fetchingModels, setFetchingModels] = (0, react.useState)(false);
|
|
161
162
|
const [removeModelTarget, setRemoveModelTarget] = (0, react.useState)(null);
|
|
162
|
-
const [poolStrategy, setPoolStrategy] = (0, react.useState)("fallback");
|
|
163
163
|
const [poolOrder, setPoolOrder] = (0, react.useState)([]);
|
|
164
164
|
const [checkingIn, setCheckingIn] = (0, react.useState)(false);
|
|
165
165
|
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
@@ -197,10 +197,8 @@ window.__ModuleLoader__.load({
|
|
|
197
197
|
}, [loadModels]);
|
|
198
198
|
const loadPoolConfig = (0, react.useCallback)(async () => {
|
|
199
199
|
try {
|
|
200
|
-
const
|
|
201
|
-
const [strategyData, orderData] = await Promise.all([strategyRes.json(), orderRes.json()]);
|
|
200
|
+
const orderData = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/order`, { cache: "no-store" })).json();
|
|
202
201
|
if (mounted.current) {
|
|
203
|
-
if (strategyData.ok && strategyData.strategy) setPoolStrategy(strategyData.strategy);
|
|
204
202
|
if (orderData.ok && Array.isArray(orderData.order)) setPoolOrder(orderData.order);
|
|
205
203
|
}
|
|
206
204
|
} catch {}
|
|
@@ -573,14 +571,6 @@ window.__ModuleLoader__.load({
|
|
|
573
571
|
})).json();
|
|
574
572
|
if (data.ok) {
|
|
575
573
|
setAlias(aliasDraft.trim());
|
|
576
|
-
try {
|
|
577
|
-
await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, {
|
|
578
|
-
method: "POST",
|
|
579
|
-
headers: { "Content-Type": "application/json" },
|
|
580
|
-
body: JSON.stringify({ strategy: poolStrategy }),
|
|
581
|
-
cache: "no-store"
|
|
582
|
-
});
|
|
583
|
-
} catch {}
|
|
584
574
|
setShowAlias(false);
|
|
585
575
|
} else setAliasError(data.error ?? "保存失败");
|
|
586
576
|
} catch (err) {
|
|
@@ -648,7 +638,6 @@ window.__ModuleLoader__.load({
|
|
|
648
638
|
children: [
|
|
649
639
|
supplierAccounts.length,
|
|
650
640
|
" 链接",
|
|
651
|
-
canPool && ` · ${poolStrategy === "round-robin" ? "轮询" : "回退"}`,
|
|
652
641
|
` · 前缀 ${alias}`
|
|
653
642
|
]
|
|
654
643
|
})]
|
|
@@ -1162,28 +1151,6 @@ window.__ModuleLoader__.load({
|
|
|
1162
1151
|
onChange: (e) => setAliasDraft(e.target.value)
|
|
1163
1152
|
})
|
|
1164
1153
|
] }),
|
|
1165
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1166
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1167
|
-
className: "dshr-fieldLabel",
|
|
1168
|
-
children: "连接池策略"
|
|
1169
|
-
}),
|
|
1170
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1171
|
-
className: "dshr-muted",
|
|
1172
|
-
children: "回退 = 按连接池顺序取第一个健康链接;轮询 = 按会话前缀粘住同一链接(前缀亲和,多会话不打架),拿不到会话信息时退回块轮转。"
|
|
1173
|
-
}),
|
|
1174
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1175
|
-
className: "dshr-select dshr-input",
|
|
1176
|
-
value: poolStrategy,
|
|
1177
|
-
onChange: (e) => setPoolStrategy(e.target.value),
|
|
1178
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1179
|
-
value: "fallback",
|
|
1180
|
-
children: "回退"
|
|
1181
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1182
|
-
value: "round-robin",
|
|
1183
|
-
children: "轮询"
|
|
1184
|
-
})]
|
|
1185
|
-
})
|
|
1186
|
-
] }),
|
|
1187
1154
|
aliasError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1188
1155
|
className: "dshr-alert",
|
|
1189
1156
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: aliasError })]
|
|
@@ -1913,7 +1880,7 @@ window.__ModuleLoader__.load({
|
|
|
1913
1880
|
/**
|
|
1914
1881
|
* 组合 — 布局交互贴近 9router combos 页。
|
|
1915
1882
|
* 组合 = 命名的一组模型(完整名 alias/id),请求 model 传组合名时,
|
|
1916
|
-
*
|
|
1883
|
+
* 作为降级链按序尝试命中其中一个。可创建/编辑/删除。
|
|
1917
1884
|
*/
|
|
1918
1885
|
function Icon$4({ d, size = 18 }) {
|
|
1919
1886
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -1961,7 +1928,6 @@ window.__ModuleLoader__.load({
|
|
|
1961
1928
|
const [editing, setEditing] = (0, react.useState)(null);
|
|
1962
1929
|
const [form, setForm] = (0, react.useState)({
|
|
1963
1930
|
name: "",
|
|
1964
|
-
strategy: "fallback",
|
|
1965
1931
|
models: []
|
|
1966
1932
|
});
|
|
1967
1933
|
const [formError, setFormError] = (0, react.useState)("");
|
|
@@ -2003,7 +1969,6 @@ window.__ModuleLoader__.load({
|
|
|
2003
1969
|
const openCreate = () => {
|
|
2004
1970
|
setForm({
|
|
2005
1971
|
name: "",
|
|
2006
|
-
strategy: "fallback",
|
|
2007
1972
|
models: []
|
|
2008
1973
|
});
|
|
2009
1974
|
setModelQuery("");
|
|
@@ -2015,7 +1980,6 @@ window.__ModuleLoader__.load({
|
|
|
2015
1980
|
const openEdit = (combo) => {
|
|
2016
1981
|
setForm({
|
|
2017
1982
|
name: combo.name,
|
|
2018
|
-
strategy: combo.strategy,
|
|
2019
1983
|
models: [...combo.models]
|
|
2020
1984
|
});
|
|
2021
1985
|
setModelQuery("");
|
|
@@ -2082,11 +2046,9 @@ window.__ModuleLoader__.load({
|
|
|
2082
2046
|
body: JSON.stringify(isEdit ? {
|
|
2083
2047
|
id: editing.id,
|
|
2084
2048
|
name: form.name.trim(),
|
|
2085
|
-
strategy: form.strategy,
|
|
2086
2049
|
models: form.models
|
|
2087
2050
|
} : {
|
|
2088
2051
|
name: form.name.trim(),
|
|
2089
|
-
strategy: form.strategy,
|
|
2090
2052
|
models: form.models
|
|
2091
2053
|
}),
|
|
2092
2054
|
cache: "no-store"
|
|
@@ -2122,25 +2084,6 @@ window.__ModuleLoader__.load({
|
|
|
2122
2084
|
window.setTimeout(() => setCopied(null), 1500);
|
|
2123
2085
|
} catch {}
|
|
2124
2086
|
};
|
|
2125
|
-
const updateStrategy = async (id, strategy) => {
|
|
2126
|
-
const combo = combos.find((c) => c.id === id);
|
|
2127
|
-
if (!combo) return;
|
|
2128
|
-
const data = await (await fetch(`${ROUTER_API_BASE}/combos/update`, {
|
|
2129
|
-
method: "POST",
|
|
2130
|
-
headers: { "Content-Type": "application/json" },
|
|
2131
|
-
body: JSON.stringify({
|
|
2132
|
-
id,
|
|
2133
|
-
name: combo.name,
|
|
2134
|
-
strategy,
|
|
2135
|
-
models: combo.models
|
|
2136
|
-
}),
|
|
2137
|
-
cache: "no-store"
|
|
2138
|
-
})).json();
|
|
2139
|
-
if (data.ok) {
|
|
2140
|
-
onRefresh();
|
|
2141
|
-
showToast(`策略已切换为${strategy === "round-robin" ? "轮询" : "回退"}`);
|
|
2142
|
-
} else showToast(data.error ?? "更新失败");
|
|
2143
|
-
};
|
|
2144
2087
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2145
2088
|
className: "dshr-tabBody",
|
|
2146
2089
|
children: [
|
|
@@ -2162,7 +2105,7 @@ window.__ModuleLoader__.load({
|
|
|
2162
2105
|
}),
|
|
2163
2106
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2164
2107
|
className: "dshr-muted dshr-comboIntroDesc",
|
|
2165
|
-
children: "
|
|
2108
|
+
children: "请求传组合名,在一组模型中按序降级命中"
|
|
2166
2109
|
}),
|
|
2167
2110
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2168
2111
|
type: "button",
|
|
@@ -2176,18 +2119,10 @@ window.__ModuleLoader__.load({
|
|
|
2176
2119
|
]
|
|
2177
2120
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2178
2121
|
className: "dshr-comboIntroBody",
|
|
2179
|
-
children: [
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
}),
|
|
2184
|
-
" 按顺序尝试,前面失败时用下一个",
|
|
2185
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2186
|
-
className: "dshr-comboHeaderKey dshr-comboHeaderKey-2",
|
|
2187
|
-
children: "轮询"
|
|
2188
|
-
}),
|
|
2189
|
-
" 在模型间轮转分配请求"
|
|
2190
|
-
]
|
|
2122
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2123
|
+
className: "dshr-comboHeaderKey",
|
|
2124
|
+
children: "降级链"
|
|
2125
|
+
}), " 按顺序尝试,前面失败时用下一个"]
|
|
2191
2126
|
})]
|
|
2192
2127
|
}),
|
|
2193
2128
|
combos.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2243,19 +2178,6 @@ window.__ModuleLoader__.load({
|
|
|
2243
2178
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2244
2179
|
className: "dshr-comboOps",
|
|
2245
2180
|
children: [
|
|
2246
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2247
|
-
className: "dshr-input dshr-comboStrategySelect",
|
|
2248
|
-
value: combo.strategy,
|
|
2249
|
-
title: "组合策略",
|
|
2250
|
-
onChange: (e) => void updateStrategy(combo.id, e.target.value),
|
|
2251
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2252
|
-
value: "fallback",
|
|
2253
|
-
children: "回退"
|
|
2254
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2255
|
-
value: "round-robin",
|
|
2256
|
-
children: "轮询"
|
|
2257
|
-
})]
|
|
2258
|
-
}),
|
|
2259
2181
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2260
2182
|
type: "button",
|
|
2261
2183
|
className: "dshr-iconBtn dshr-iconBtn-sm",
|
|
@@ -2315,7 +2237,7 @@ window.__ModuleLoader__.load({
|
|
|
2315
2237
|
})] })
|
|
2316
2238
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2317
2239
|
className: "dshr-muted dshr-reason",
|
|
2318
|
-
children:
|
|
2240
|
+
children: "按此顺序尝试模型;当前模型不可用时换下一个。"
|
|
2319
2241
|
})]
|
|
2320
2242
|
})]
|
|
2321
2243
|
}, combo.id))
|
|
@@ -2336,98 +2258,80 @@ window.__ModuleLoader__.load({
|
|
|
2336
2258
|
}),
|
|
2337
2259
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2338
2260
|
className: "dshr-comboFormLeft",
|
|
2339
|
-
children: [
|
|
2340
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
2341
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2342
|
-
className: "dshr-formLabel",
|
|
2343
|
-
children: "组合名"
|
|
2344
|
-
}),
|
|
2345
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2346
|
-
className: "dshr-input dshr-mono",
|
|
2347
|
-
placeholder: "my-combo",
|
|
2348
|
-
value: form.name,
|
|
2349
|
-
onChange: (e) => patchForm({ name: e.target.value }),
|
|
2350
|
-
autoFocus: true
|
|
2351
|
-
}),
|
|
2352
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2353
|
-
className: "dshr-formHint",
|
|
2354
|
-
children: "只能含字母、数字、-、_ 和 ."
|
|
2355
|
-
})
|
|
2356
|
-
] }),
|
|
2357
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2358
|
-
className: "dshr-formLabel",
|
|
2359
|
-
children: "策略"
|
|
2360
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2361
|
-
className: "dshr-input",
|
|
2362
|
-
value: form.strategy,
|
|
2363
|
-
onChange: (e) => patchForm({ strategy: e.target.value }),
|
|
2364
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2365
|
-
value: "fallback",
|
|
2366
|
-
children: "回退 — 按顺序尝试,失败换下一个"
|
|
2367
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2368
|
-
value: "round-robin",
|
|
2369
|
-
children: "轮询 — 在模型间轮转分配"
|
|
2370
|
-
})]
|
|
2371
|
-
})] }),
|
|
2372
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2261
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
2262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2373
2263
|
className: "dshr-formLabel",
|
|
2264
|
+
children: "组合名"
|
|
2265
|
+
}),
|
|
2266
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2267
|
+
className: "dshr-input dshr-mono",
|
|
2268
|
+
placeholder: "my-combo",
|
|
2269
|
+
value: form.name,
|
|
2270
|
+
onChange: (e) => patchForm({ name: e.target.value }),
|
|
2271
|
+
autoFocus: true
|
|
2272
|
+
}),
|
|
2273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2274
|
+
className: "dshr-formHint",
|
|
2275
|
+
children: "只能含字母、数字、-、_ 和 ."
|
|
2276
|
+
})
|
|
2277
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2278
|
+
className: "dshr-formLabel",
|
|
2279
|
+
children: [
|
|
2280
|
+
"模型(已选 ",
|
|
2281
|
+
form.models.length,
|
|
2282
|
+
")"
|
|
2283
|
+
]
|
|
2284
|
+
}), form.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2285
|
+
className: "dshr-comboEmptyStepsBox",
|
|
2286
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2287
|
+
d: I$4.layers,
|
|
2288
|
+
size: 20
|
|
2289
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2290
|
+
className: "dshr-muted",
|
|
2291
|
+
children: "还没有模型"
|
|
2292
|
+
})]
|
|
2293
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2294
|
+
className: "dshr-comboStepList",
|
|
2295
|
+
children: form.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2296
|
+
className: `dshr-comboStepRow${dragIndex === index ? " dshr-comboStepRow-dragging" : ""}${dragOverIndex === index && dragIndex !== null && dragIndex !== index ? " dshr-comboStepRow-over" : ""}`,
|
|
2297
|
+
draggable: true,
|
|
2298
|
+
onDragStart: () => handleDragStart(index),
|
|
2299
|
+
onDragOver: (e) => handleDragOver(e, index),
|
|
2300
|
+
onDrop: () => handleDrop(index),
|
|
2301
|
+
onDragEnd: () => {
|
|
2302
|
+
setDragIndex(null);
|
|
2303
|
+
setDragOverIndex(null);
|
|
2304
|
+
},
|
|
2374
2305
|
children: [
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2382
|
-
d: I$4.layers,
|
|
2383
|
-
size: 20
|
|
2384
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2385
|
-
className: "dshr-muted",
|
|
2386
|
-
children: "还没有模型"
|
|
2387
|
-
})]
|
|
2388
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2389
|
-
className: "dshr-comboStepList",
|
|
2390
|
-
children: form.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2391
|
-
className: `dshr-comboStepRow${dragIndex === index ? " dshr-comboStepRow-dragging" : ""}${dragOverIndex === index && dragIndex !== null && dragIndex !== index ? " dshr-comboStepRow-over" : ""}`,
|
|
2392
|
-
draggable: true,
|
|
2393
|
-
onDragStart: () => handleDragStart(index),
|
|
2394
|
-
onDragOver: (e) => handleDragOver(e, index),
|
|
2395
|
-
onDrop: () => handleDrop(index),
|
|
2396
|
-
onDragEnd: () => {
|
|
2397
|
-
setDragIndex(null);
|
|
2398
|
-
setDragOverIndex(null);
|
|
2399
|
-
},
|
|
2400
|
-
children: [
|
|
2401
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2402
|
-
className: "dshr-comboGrip",
|
|
2403
|
-
title: "拖拽排序",
|
|
2404
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2405
|
-
d: I$4.grip,
|
|
2406
|
-
size: 14
|
|
2407
|
-
})
|
|
2408
|
-
}),
|
|
2409
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2410
|
-
className: "dshr-comboStepNum",
|
|
2411
|
-
children: index + 1
|
|
2412
|
-
}),
|
|
2413
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2414
|
-
className: "dshr-comboSelectedModel",
|
|
2415
|
-
children: displayName(model)
|
|
2416
|
-
}),
|
|
2417
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2418
|
-
type: "button",
|
|
2419
|
-
className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove",
|
|
2420
|
-
title: "移除",
|
|
2421
|
-
onClick: () => removeModel(index),
|
|
2422
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2423
|
-
d: I$4.delete,
|
|
2424
|
-
size: 14
|
|
2425
|
-
})
|
|
2306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2307
|
+
className: "dshr-comboGrip",
|
|
2308
|
+
title: "拖拽排序",
|
|
2309
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2310
|
+
d: I$4.grip,
|
|
2311
|
+
size: 14
|
|
2426
2312
|
})
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2313
|
+
}),
|
|
2314
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2315
|
+
className: "dshr-comboStepNum",
|
|
2316
|
+
children: index + 1
|
|
2317
|
+
}),
|
|
2318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2319
|
+
className: "dshr-comboSelectedModel",
|
|
2320
|
+
children: displayName(model)
|
|
2321
|
+
}),
|
|
2322
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2323
|
+
type: "button",
|
|
2324
|
+
className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove",
|
|
2325
|
+
title: "移除",
|
|
2326
|
+
onClick: () => removeModel(index),
|
|
2327
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
2328
|
+
d: I$4.delete,
|
|
2329
|
+
size: 14
|
|
2330
|
+
})
|
|
2331
|
+
})
|
|
2332
|
+
]
|
|
2333
|
+
}, `${model}-${index}`))
|
|
2334
|
+
})] })]
|
|
2431
2335
|
}),
|
|
2432
2336
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2433
2337
|
className: "dshr-comboFormRight",
|
|
@@ -2545,6 +2449,40 @@ window.__ModuleLoader__.load({
|
|
|
2545
2449
|
});
|
|
2546
2450
|
}
|
|
2547
2451
|
//#endregion
|
|
2452
|
+
//#region src/client/supplier-visibility.ts
|
|
2453
|
+
/** 开关在插件页的固定位置 —— 空状态要把用户指过去,不然就断在这里了。 */
|
|
2454
|
+
var SWITCH_HOME = "设置 → 插件 → dsh-router-core";
|
|
2455
|
+
/**
|
|
2456
|
+
* 按「开着与否 + 来源」把供应商分成面板要渲染的两组,并给出空状态该说什么。
|
|
2457
|
+
*
|
|
2458
|
+
* `enabled` 缺省按**开着**读(老核心没有这个字段);只有明确的 `false` 才算关。
|
|
2459
|
+
*/
|
|
2460
|
+
function supplierVisibility(suppliers) {
|
|
2461
|
+
const on = suppliers.filter((s) => s.enabled !== false);
|
|
2462
|
+
const builtin = on.filter((s) => s.source !== "external");
|
|
2463
|
+
const external = on.filter((s) => s.source === "external");
|
|
2464
|
+
const hidden = suppliers.length - on.length;
|
|
2465
|
+
if (on.length > 0) return {
|
|
2466
|
+
builtin,
|
|
2467
|
+
external,
|
|
2468
|
+
hidden
|
|
2469
|
+
};
|
|
2470
|
+
return {
|
|
2471
|
+
builtin,
|
|
2472
|
+
external,
|
|
2473
|
+
hidden,
|
|
2474
|
+
empty: hidden > 0 ? {
|
|
2475
|
+
reason: "disabled",
|
|
2476
|
+
title: "供应商都已关闭",
|
|
2477
|
+
desc: `开关在 ${SWITCH_HOME} 的「路由组件」一节;打开后会重新参与路由`
|
|
2478
|
+
} : {
|
|
2479
|
+
reason: "none",
|
|
2480
|
+
title: "暂无供应商",
|
|
2481
|
+
desc: ""
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
//#endregion
|
|
2548
2486
|
//#region src/client/CheckinCard.tsx
|
|
2549
2487
|
/**
|
|
2550
2488
|
* 概览的「签到」汇总卡:一键签到所有支持签到的供应商 + 显示今天点过没。
|
|
@@ -3115,14 +3053,14 @@ window.__ModuleLoader__.load({
|
|
|
3115
3053
|
}),
|
|
3116
3054
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3117
3055
|
className: "dshr-recentSupplier",
|
|
3118
|
-
children: "
|
|
3056
|
+
children: "连接"
|
|
3119
3057
|
}),
|
|
3120
3058
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3121
|
-
className: "dshr-
|
|
3059
|
+
className: "dshr-recentTokens",
|
|
3122
3060
|
children: "In / Out"
|
|
3123
3061
|
}),
|
|
3124
3062
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3125
|
-
className: "dshr-
|
|
3063
|
+
className: "dshr-recentDuration",
|
|
3126
3064
|
children: "耗时"
|
|
3127
3065
|
})
|
|
3128
3066
|
]
|
|
@@ -3141,10 +3079,11 @@ window.__ModuleLoader__.load({
|
|
|
3141
3079
|
}),
|
|
3142
3080
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3143
3081
|
className: "dshr-recentSupplier",
|
|
3144
|
-
|
|
3082
|
+
title: r.uid ?? "",
|
|
3083
|
+
children: r.connection === void 0 || r.connection === "" ? "—" : r.connection
|
|
3145
3084
|
}),
|
|
3146
3085
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3147
|
-
className: "dshr-
|
|
3086
|
+
className: "dshr-recentTokens",
|
|
3148
3087
|
children: [
|
|
3149
3088
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3150
3089
|
className: "dshr-tokenIn",
|
|
@@ -3158,7 +3097,7 @@ window.__ModuleLoader__.load({
|
|
|
3158
3097
|
]
|
|
3159
3098
|
}),
|
|
3160
3099
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3161
|
-
className: "dshr-
|
|
3100
|
+
className: "dshr-recentDuration dshr-muted",
|
|
3162
3101
|
children: fmtDuration(r.durationMs)
|
|
3163
3102
|
})
|
|
3164
3103
|
]
|
|
@@ -3344,17 +3283,29 @@ window.__ModuleLoader__.load({
|
|
|
3344
3283
|
});
|
|
3345
3284
|
}
|
|
3346
3285
|
//#endregion
|
|
3347
|
-
//#region src/client/
|
|
3286
|
+
//#region src/client/ext-panels.ts
|
|
3287
|
+
/** 扩展 id → 它的详情面板。 */
|
|
3288
|
+
var panels = /* @__PURE__ */ new Map();
|
|
3348
3289
|
/**
|
|
3349
|
-
*
|
|
3290
|
+
* 给某个扩展登记详情面板。同 id 后登记的**顶掉**先登记的(热重载/重复 apply
|
|
3291
|
+
* 时的常见情形);返回清理函数。
|
|
3350
3292
|
*
|
|
3351
|
-
*
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
* 「复制 kill 命令」两个剪贴板动作方便用户去终端处理。
|
|
3355
|
-
* 进程数据走后端 `/router/api/ext/:id/processes`;`ps` 瞬时失败时保留上一帧,别刷成
|
|
3356
|
-
* 整屏错误。
|
|
3293
|
+
* @param extId - 扩展 id(`router.ext` 表里的键)
|
|
3294
|
+
* @param panel - 详情面板组件。**不收 props**:面板的渲染点只有详情页一处
|
|
3295
|
+
* (`ExtDetail`),没有第二种调用形状。
|
|
3357
3296
|
*/
|
|
3297
|
+
function registerExtPanel(extId, panel) {
|
|
3298
|
+
panels.set(extId, panel);
|
|
3299
|
+
return () => {
|
|
3300
|
+
if (panels.get(extId) === panel) panels.delete(extId);
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
/** 取某个扩展的详情面板;没登记返回 undefined(调用方据此走通用只读页)。 */
|
|
3304
|
+
function extPanel(extId) {
|
|
3305
|
+
return panels.get(extId);
|
|
3306
|
+
}
|
|
3307
|
+
//#endregion
|
|
3308
|
+
//#region src/client/ExtDetail.tsx
|
|
3358
3309
|
function Icon$1({ d, size = 18 }) {
|
|
3359
3310
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3360
3311
|
viewBox: "0 0 24 24",
|
|
@@ -3371,298 +3322,83 @@ window.__ModuleLoader__.load({
|
|
|
3371
3322
|
}
|
|
3372
3323
|
var I$1 = {
|
|
3373
3324
|
back: "M19 12H5M12 19l-7-7 7-7",
|
|
3374
|
-
|
|
3375
|
-
node: "M8 5.5L12 3l4 2.5v5L12 13l-4-2.5v-5zM4 9v5l4 2.5M12 13v5l4-2.5M20 9v5l-4 2.5",
|
|
3376
|
-
bolt: "M13 2L4 14h6l-1 8 9-12h-6z",
|
|
3377
|
-
copy: "M8 8h11v11H8zM5 14H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
|
|
3378
|
-
check: "M4 12.5l5 5L20 6.5"
|
|
3325
|
+
bolt: "M13 2L4 14h6l-1 8 9-12h-6z"
|
|
3379
3326
|
};
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3383
|
-
type: "button",
|
|
3384
|
-
className: "dshr-iconButton",
|
|
3385
|
-
onClick,
|
|
3386
|
-
disabled: busy,
|
|
3387
|
-
title: "刷新进程",
|
|
3388
|
-
"aria-label": "刷新进程",
|
|
3389
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3390
|
-
className: busy ? "dshr-spin" : void 0,
|
|
3391
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3392
|
-
d: I$1.refresh,
|
|
3393
|
-
size: 16
|
|
3394
|
-
})
|
|
3395
|
-
})
|
|
3396
|
-
});
|
|
3397
|
-
}
|
|
3398
|
-
function fmtPct(v) {
|
|
3399
|
-
return `${(v ?? 0).toFixed(1)}%`;
|
|
3400
|
-
}
|
|
3401
|
-
function ExtDetail({ item, onBack, onToggle }) {
|
|
3327
|
+
function ExtDetail({ item, onBack }) {
|
|
3328
|
+
const Custom = extPanel(item.id);
|
|
3402
3329
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3403
3330
|
className: "dshr-tabBody",
|
|
3404
|
-
children: [
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
className: "dshr-providerName",
|
|
3433
|
-
children: item.name
|
|
3434
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3435
|
-
className: "dshr-providerCount",
|
|
3436
|
-
children: [
|
|
3437
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3438
|
-
className: "dshr-mono",
|
|
3439
|
-
children: item.id
|
|
3440
|
-
}),
|
|
3441
|
-
item.enabled ? " · 已启用" : " · 未启用",
|
|
3442
|
-
item.ready === false ? " · 未就绪" : ""
|
|
3443
|
-
]
|
|
3444
|
-
})]
|
|
3445
|
-
}),
|
|
3446
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3447
|
-
type: "button",
|
|
3448
|
-
className: `dshr-toggle dshr-extDetailToggle ${item.enabled ? "dshr-toggle-on" : ""}`,
|
|
3449
|
-
role: "switch",
|
|
3450
|
-
"aria-checked": !!item.enabled,
|
|
3451
|
-
disabled: item.ready === false && !item.enabled,
|
|
3452
|
-
onClick: () => void onToggle(item, !item.enabled),
|
|
3453
|
-
title: item.ready === false && !item.enabled ? "当前不可用,无法开启" : item.enabled ? `关闭 ${item.name}` : `开启 ${item.name}`,
|
|
3454
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshr-toggleKnob" })
|
|
3455
|
-
})
|
|
3456
|
-
]
|
|
3457
|
-
})]
|
|
3458
|
-
}),
|
|
3459
|
-
item.description !== void 0 && item.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
3460
|
-
className: "dshr-card",
|
|
3461
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3462
|
-
className: "dshr-muted",
|
|
3463
|
-
style: { padding: "12px 14px" },
|
|
3464
|
-
children: item.description
|
|
3465
|
-
})
|
|
3466
|
-
}),
|
|
3467
|
-
item.ready === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3468
|
-
className: "dshr-alert",
|
|
3469
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "未就绪" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.detail !== void 0 && item.detail !== "" ? item.detail : "当前不可用" })]
|
|
3470
|
-
}),
|
|
3471
|
-
item.id === "watch" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WatchPanel, {}) : null
|
|
3472
|
-
]
|
|
3473
|
-
});
|
|
3474
|
-
}
|
|
3475
|
-
/** watch 的进程监控面板:轮询采样(只读)+ 复制 PID / 复制 kill 命令。 */
|
|
3476
|
-
function WatchPanel() {
|
|
3477
|
-
const [processes, setProcesses] = (0, react.useState)([]);
|
|
3478
|
-
const [error, setError] = (0, react.useState)("");
|
|
3479
|
-
const [loading, setLoading] = (0, react.useState)(true);
|
|
3480
|
-
const [copied, setCopied] = (0, react.useState)(null);
|
|
3481
|
-
const alive = (0, react.useRef)(true);
|
|
3482
|
-
const now = (0, react.useRef)(Date.now());
|
|
3483
|
-
(0, react.useEffect)(() => {
|
|
3484
|
-
alive.current = true;
|
|
3485
|
-
return () => {
|
|
3486
|
-
alive.current = false;
|
|
3487
|
-
};
|
|
3488
|
-
}, []);
|
|
3489
|
-
const load = async () => {
|
|
3490
|
-
try {
|
|
3491
|
-
const res = await fetch(`${ROUTER_API_BASE}/ext/watch/processes`, { cache: "no-store" });
|
|
3492
|
-
if (!res.ok) {
|
|
3493
|
-
let reason = "";
|
|
3494
|
-
try {
|
|
3495
|
-
const err = await res.json();
|
|
3496
|
-
if (err.error) reason = ` — ${err.error}`;
|
|
3497
|
-
} catch {}
|
|
3498
|
-
if (alive.current) setError(`HTTP ${res.status}${reason}`);
|
|
3499
|
-
return;
|
|
3500
|
-
}
|
|
3501
|
-
let data;
|
|
3502
|
-
try {
|
|
3503
|
-
data = await res.json();
|
|
3504
|
-
} catch {
|
|
3505
|
-
if (alive.current) setError("响应不是 JSON");
|
|
3506
|
-
return;
|
|
3507
|
-
}
|
|
3508
|
-
if (!alive.current) return;
|
|
3509
|
-
setError(data.ok ? "" : data.error ?? "加载进程失败");
|
|
3510
|
-
if (data.ok && data.processes !== void 0) setProcesses(data.processes);
|
|
3511
|
-
} catch (err) {
|
|
3512
|
-
if (alive.current) setError(err.message);
|
|
3513
|
-
} finally {
|
|
3514
|
-
if (alive.current) setLoading(false);
|
|
3515
|
-
}
|
|
3516
|
-
};
|
|
3517
|
-
(0, react.useEffect)(() => {
|
|
3518
|
-
load();
|
|
3519
|
-
const timer = setInterval(() => {
|
|
3520
|
-
load();
|
|
3521
|
-
}, 3e3);
|
|
3522
|
-
return () => clearInterval(timer);
|
|
3523
|
-
}, []);
|
|
3524
|
-
const copy = (pid, text) => {
|
|
3525
|
-
const stamp = Date.now();
|
|
3526
|
-
now.current = stamp;
|
|
3527
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
3528
|
-
if (alive.current && now.current === stamp) {
|
|
3529
|
-
setCopied({
|
|
3530
|
-
pid,
|
|
3531
|
-
text
|
|
3532
|
-
});
|
|
3533
|
-
setTimeout(() => {
|
|
3534
|
-
if (alive.current) setCopied((c) => c?.pid === pid ? null : c);
|
|
3535
|
-
}, 1200);
|
|
3536
|
-
}
|
|
3537
|
-
}).catch(() => {
|
|
3538
|
-
if (alive.current) setError("复制失败");
|
|
3539
|
-
});
|
|
3540
|
-
};
|
|
3541
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3542
|
-
className: "dshr-card",
|
|
3543
|
-
children: [
|
|
3544
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3545
|
-
className: "dshr-cardHead",
|
|
3546
|
-
children: [
|
|
3547
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3548
|
-
className: "dshr-cardIcon",
|
|
3549
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, { d: I$1.node })
|
|
3550
|
-
}),
|
|
3551
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3552
|
-
className: "dshr-cardTitle",
|
|
3553
|
-
children: "运行进程"
|
|
3554
|
-
}),
|
|
3555
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3556
|
-
className: "dshr-cardMeta",
|
|
3557
|
-
children: "agent 启动 · 只读 · 3s 采样"
|
|
3558
|
-
}),
|
|
3559
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RefreshBtn, {
|
|
3560
|
-
onClick: () => void load(),
|
|
3561
|
-
busy: loading
|
|
3331
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3332
|
+
className: "dshr-providerHead",
|
|
3333
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3334
|
+
type: "button",
|
|
3335
|
+
className: "dshr-backLink",
|
|
3336
|
+
onClick: onBack,
|
|
3337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3338
|
+
d: I$1.back,
|
|
3339
|
+
size: 16
|
|
3340
|
+
}), "返回"]
|
|
3341
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3342
|
+
className: "dshr-providerTitleRow",
|
|
3343
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3344
|
+
className: "dshr-providerIcon",
|
|
3345
|
+
style: {
|
|
3346
|
+
color: "var(--rs-faint)",
|
|
3347
|
+
background: "var(--rs-layer-2)"
|
|
3348
|
+
},
|
|
3349
|
+
children: item.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3350
|
+
className: "dshr-providerImg",
|
|
3351
|
+
src: item.icon,
|
|
3352
|
+
alt: "",
|
|
3353
|
+
onError: (e) => {
|
|
3354
|
+
e.currentTarget.style.display = "none";
|
|
3355
|
+
}
|
|
3356
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3357
|
+
d: I$1.bolt,
|
|
3358
|
+
size: 22
|
|
3562
3359
|
})
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
]
|
|
3576
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3577
|
-
className: "dshr-rankList",
|
|
3578
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3579
|
-
className: "dshr-rankRow dshr-rankHead",
|
|
3580
|
-
children: [
|
|
3581
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3582
|
-
className: "dshr-rankName",
|
|
3583
|
-
children: "PID / 命令"
|
|
3584
|
-
}),
|
|
3585
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3586
|
-
className: "dshr-rankNum",
|
|
3587
|
-
children: "CPU"
|
|
3588
|
-
}),
|
|
3589
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3590
|
-
className: "dshr-rankNum",
|
|
3591
|
-
children: "内存"
|
|
3592
|
-
}),
|
|
3593
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshr-rankNum" })
|
|
3594
|
-
]
|
|
3595
|
-
}), processes.map((p) => {
|
|
3596
|
-
const done = copied?.pid === p.pid;
|
|
3597
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3598
|
-
className: "dshr-rankRow",
|
|
3599
|
-
children: [
|
|
3600
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3601
|
-
className: "dshr-rankName dshr-mono",
|
|
3602
|
-
title: p.command,
|
|
3603
|
-
children: [
|
|
3604
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3605
|
-
className: "dshr-muted",
|
|
3606
|
-
children: p.pid
|
|
3607
|
-
}),
|
|
3608
|
-
" ",
|
|
3609
|
-
p.command
|
|
3610
|
-
]
|
|
3611
|
-
}),
|
|
3612
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3613
|
-
className: "dshr-rankNum",
|
|
3614
|
-
children: fmtPct(p.cpu)
|
|
3615
|
-
}),
|
|
3616
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3617
|
-
className: "dshr-rankNum",
|
|
3618
|
-
children: fmtPct(p.mem)
|
|
3619
|
-
}),
|
|
3620
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3621
|
-
className: "dshr-rankNum dshr-procOps",
|
|
3622
|
-
children: done ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3623
|
-
className: "dshr-muted",
|
|
3624
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3625
|
-
d: I$1.check,
|
|
3626
|
-
size: 13
|
|
3627
|
-
}), " 已复制"]
|
|
3628
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3629
|
-
type: "button",
|
|
3630
|
-
className: "dshr-miniButton",
|
|
3631
|
-
onClick: () => copy(p.pid, String(p.pid)),
|
|
3632
|
-
title: `复制 PID ${p.pid}`,
|
|
3633
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3634
|
-
d: I$1.copy,
|
|
3635
|
-
size: 13
|
|
3636
|
-
}), " PID"]
|
|
3637
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3638
|
-
type: "button",
|
|
3639
|
-
className: "dshr-miniButton",
|
|
3640
|
-
onClick: () => copy(p.pid, `kill -TERM ${p.pid}`),
|
|
3641
|
-
title: `复制 kill -TERM ${p.pid}`,
|
|
3642
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3643
|
-
d: I$1.copy,
|
|
3644
|
-
size: 13
|
|
3645
|
-
}), " kill"]
|
|
3646
|
-
})] })
|
|
3647
|
-
})
|
|
3648
|
-
]
|
|
3649
|
-
}, p.pid);
|
|
3360
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3361
|
+
className: "dshr-providerMeta",
|
|
3362
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
3363
|
+
className: "dshr-providerName",
|
|
3364
|
+
children: item.name
|
|
3365
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3366
|
+
className: "dshr-providerCount",
|
|
3367
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3368
|
+
className: "dshr-mono",
|
|
3369
|
+
children: item.id
|
|
3370
|
+
}), item.ready === false ? " · 未就绪" : ""]
|
|
3371
|
+
})]
|
|
3650
3372
|
})]
|
|
3373
|
+
})]
|
|
3374
|
+
}), Custom !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Custom, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [item.description !== void 0 && item.description !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
3375
|
+
className: "dshr-card",
|
|
3376
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3377
|
+
className: "dshr-muted",
|
|
3378
|
+
style: { padding: "12px 14px" },
|
|
3379
|
+
children: item.description
|
|
3651
3380
|
})
|
|
3652
|
-
|
|
3381
|
+
}), item.ready === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3382
|
+
className: "dshr-alert",
|
|
3383
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "未就绪" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.detail !== void 0 && item.detail !== "" ? item.detail : "当前不可用" })]
|
|
3384
|
+
})] })]
|
|
3653
3385
|
});
|
|
3654
3386
|
}
|
|
3655
3387
|
//#endregion
|
|
3656
3388
|
//#region src/client/ExtTab.tsx
|
|
3657
3389
|
/**
|
|
3658
|
-
* 扩展 (Ext) 页 ——
|
|
3390
|
+
* 扩展 (Ext) 页 —— 列出**已启用**的扩展插件,只读。
|
|
3391
|
+
*
|
|
3392
|
+
* 数据来自 `/router/api/ext`:核心把自己存的开关(`ext.json`)+ 扩展器报的
|
|
3393
|
+
* 运行时事实(`ready` / `detail`)合并后给面板。扩展器只实现具体能力(如 RTK)。
|
|
3659
3394
|
*
|
|
3660
|
-
*
|
|
3661
|
-
*
|
|
3662
|
-
*
|
|
3395
|
+
* 两个刻意的形状(与「开关在官方插件页」这一决定配套):
|
|
3396
|
+
* 1. **只列已启用的** —— 关闭的扩展连卡片都不出现。这里是「正在生效的东西」的
|
|
3397
|
+
* 展台,不是配置页;要去改开关,去设置 → 插件 → dsh-router-core 详情页的
|
|
3398
|
+
* 「路由组件」一节。
|
|
3399
|
+
* 2. **本页不发 PATCH** —— 启停只有一个入口,避免两处状态各说各话。
|
|
3663
3400
|
*
|
|
3664
|
-
*
|
|
3665
|
-
* 未装任何扩展 → 空状态提示。
|
|
3401
|
+
* 未装任何扩展,或装了但都没启用 → 同一条空状态提示。
|
|
3666
3402
|
*/
|
|
3667
3403
|
function Icon({ d, size = 18 }) {
|
|
3668
3404
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -3679,8 +3415,8 @@ window.__ModuleLoader__.load({
|
|
|
3679
3415
|
});
|
|
3680
3416
|
}
|
|
3681
3417
|
var I = {
|
|
3682
|
-
bolt: "M13 2L4 14h6l-1 8 9-12h-
|
|
3683
|
-
puzzle: "M4 7h3a2 2 0 0 1 4
|
|
3418
|
+
bolt: "M13 2L4 14h6l-1 8 9-12h-6z",
|
|
3419
|
+
puzzle: "M4 7h3a2 2 0 0 1 4 0h5a2 2 0 0 1 2 1v4a2 2 0 0 0 0 4v4H8v-3a2 2 0 0 1 0-4H4V7z"
|
|
3684
3420
|
};
|
|
3685
3421
|
function ExtTab() {
|
|
3686
3422
|
const [items, setItems] = (0, react.useState)([]);
|
|
@@ -3710,45 +3446,11 @@ window.__ModuleLoader__.load({
|
|
|
3710
3446
|
}
|
|
3711
3447
|
}
|
|
3712
3448
|
};
|
|
3713
|
-
const
|
|
3714
|
-
|
|
3715
|
-
...i,
|
|
3716
|
-
enabled: value
|
|
3717
|
-
} : i));
|
|
3718
|
-
setError("");
|
|
3719
|
-
try {
|
|
3720
|
-
const data = await (await fetch(`${ROUTER_API_BASE}/ext`, {
|
|
3721
|
-
method: "PATCH",
|
|
3722
|
-
headers: { "Content-Type": "application/json" },
|
|
3723
|
-
body: JSON.stringify({
|
|
3724
|
-
id: item.id,
|
|
3725
|
-
enabled: value
|
|
3726
|
-
}),
|
|
3727
|
-
cache: "no-store"
|
|
3728
|
-
})).json();
|
|
3729
|
-
if (mounted.current && data.enhancers) setItems(data.enhancers);
|
|
3730
|
-
else if (mounted.current && !data.ok) {
|
|
3731
|
-
setError(data.error ?? "切换失败");
|
|
3732
|
-
setItems((prev) => prev.map((i) => i.id === item.id ? {
|
|
3733
|
-
...i,
|
|
3734
|
-
enabled: !value
|
|
3735
|
-
} : i));
|
|
3736
|
-
}
|
|
3737
|
-
} catch (err) {
|
|
3738
|
-
if (mounted.current) {
|
|
3739
|
-
setError(err.message);
|
|
3740
|
-
setItems((prev) => prev.map((i) => i.id === item.id ? {
|
|
3741
|
-
...i,
|
|
3742
|
-
enabled: !value
|
|
3743
|
-
} : i));
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
};
|
|
3747
|
-
const selected = selectedId !== null ? items.find((i) => i.id === selectedId) ?? null : null;
|
|
3449
|
+
const shown = items.filter((item) => item.enabled === true);
|
|
3450
|
+
const selected = selectedId !== null ? shown.find((i) => i.id === selectedId) ?? null : null;
|
|
3748
3451
|
if (selected !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExtDetail, {
|
|
3749
3452
|
item: selected,
|
|
3750
|
-
onBack: () => setSelectedId(null)
|
|
3751
|
-
onToggle: toggle
|
|
3453
|
+
onBack: () => setSelectedId(null)
|
|
3752
3454
|
});
|
|
3753
3455
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3754
3456
|
className: "dshr-tabBody",
|
|
@@ -3758,7 +3460,7 @@ window.__ModuleLoader__.load({
|
|
|
3758
3460
|
}), !loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3759
3461
|
className: "dshr-empty",
|
|
3760
3462
|
children: "加载中…"
|
|
3761
|
-
}) :
|
|
3463
|
+
}) : shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3762
3464
|
className: "dshr-keyEmpty",
|
|
3763
3465
|
children: [
|
|
3764
3466
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -3770,20 +3472,20 @@ window.__ModuleLoader__.load({
|
|
|
3770
3472
|
}),
|
|
3771
3473
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3772
3474
|
className: "dshr-keyEmptyTitle",
|
|
3773
|
-
children: "
|
|
3475
|
+
children: "暂无启用的扩展"
|
|
3774
3476
|
}),
|
|
3775
3477
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3776
3478
|
className: "dshr-keyEmptyDesc",
|
|
3777
3479
|
children: [
|
|
3778
|
-
"
|
|
3779
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
3780
|
-
"
|
|
3480
|
+
"开关在 ",
|
|
3481
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "设置 → 插件 → dsh-router-core" }),
|
|
3482
|
+
" 的「路由组件」一节; 启用后这里会列出它"
|
|
3781
3483
|
]
|
|
3782
3484
|
})
|
|
3783
3485
|
]
|
|
3784
3486
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3785
3487
|
className: "dshr-supplierGrid",
|
|
3786
|
-
children:
|
|
3488
|
+
children: shown.map((item) => {
|
|
3787
3489
|
const notReady = item.ready === false;
|
|
3788
3490
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3789
3491
|
className: "dshr-supplierCard",
|
|
@@ -3816,8 +3518,8 @@ window.__ModuleLoader__.load({
|
|
|
3816
3518
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3817
3519
|
className: "dshr-supplierMeta",
|
|
3818
3520
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3819
|
-
className: "dshr-muted",
|
|
3820
|
-
children: item.
|
|
3521
|
+
className: "dshr-mono dshr-muted",
|
|
3522
|
+
children: item.id
|
|
3821
3523
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3822
3524
|
className: "dshr-chevron",
|
|
3823
3525
|
children: "›"
|
|
@@ -3891,6 +3593,7 @@ window.__ModuleLoader__.load({
|
|
|
3891
3593
|
};
|
|
3892
3594
|
}, []);
|
|
3893
3595
|
const suppliers = snapshot.health?.suppliers ?? [];
|
|
3596
|
+
const panel = supplierVisibility(suppliers);
|
|
3894
3597
|
const accounts = snapshot.status?.accounts ?? [];
|
|
3895
3598
|
const combos = snapshot.combos?.combos ?? [];
|
|
3896
3599
|
const suppliersError = snapshot.status?.ok === false ? snapshot.status.error : void 0;
|
|
@@ -4022,12 +3725,15 @@ window.__ModuleLoader__.load({
|
|
|
4022
3725
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4023
3726
|
className: "dshr-tabBody",
|
|
4024
3727
|
children: [
|
|
4025
|
-
|
|
4026
|
-
className: "dshr-empty",
|
|
4027
|
-
children: "
|
|
3728
|
+
snapshot.health !== null && panel.empty !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3729
|
+
className: "dshr-empty dshr-emptyStack",
|
|
3730
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: panel.empty.title }), panel.empty.desc !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3731
|
+
className: "dshr-muted",
|
|
3732
|
+
children: panel.empty.desc
|
|
3733
|
+
})]
|
|
4028
3734
|
}),
|
|
4029
|
-
renderSupplierGroup("内置",
|
|
4030
|
-
renderSupplierGroup("插件",
|
|
3735
|
+
renderSupplierGroup("内置", panel.builtin),
|
|
3736
|
+
renderSupplierGroup("插件", panel.external),
|
|
4031
3737
|
suppliersError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4032
3738
|
className: "dshr-empty",
|
|
4033
3739
|
children: suppliersError
|
|
@@ -4207,7 +3913,7 @@ window.__ModuleLoader__.load({
|
|
|
4207
3913
|
};
|
|
4208
3914
|
}
|
|
4209
3915
|
//#endregion
|
|
4210
|
-
//#region \0@oxc-project+runtime@0.
|
|
3916
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/typeof.js
|
|
4211
3917
|
function _typeof(o) {
|
|
4212
3918
|
"@babel/helpers - typeof";
|
|
4213
3919
|
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
@@ -4217,7 +3923,7 @@ window.__ModuleLoader__.load({
|
|
|
4217
3923
|
}, _typeof(o);
|
|
4218
3924
|
}
|
|
4219
3925
|
//#endregion
|
|
4220
|
-
//#region \0@oxc-project+runtime@0.
|
|
3926
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/toPrimitive.js
|
|
4221
3927
|
function toPrimitive(t, r) {
|
|
4222
3928
|
if ("object" != _typeof(t) || !t) return t;
|
|
4223
3929
|
var e = t[Symbol.toPrimitive];
|
|
@@ -4229,13 +3935,13 @@ window.__ModuleLoader__.load({
|
|
|
4229
3935
|
return ("string" === r ? String : Number)(t);
|
|
4230
3936
|
}
|
|
4231
3937
|
//#endregion
|
|
4232
|
-
//#region \0@oxc-project+runtime@0.
|
|
3938
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/toPropertyKey.js
|
|
4233
3939
|
function toPropertyKey(t) {
|
|
4234
3940
|
var i = toPrimitive(t, "string");
|
|
4235
3941
|
return "symbol" == _typeof(i) ? i : i + "";
|
|
4236
3942
|
}
|
|
4237
3943
|
//#endregion
|
|
4238
|
-
//#region \0@oxc-project+runtime@0.
|
|
3944
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/defineProperty.js
|
|
4239
3945
|
function _defineProperty(e, r, t) {
|
|
4240
3946
|
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
4241
3947
|
value: t,
|
|
@@ -4388,31 +4094,794 @@ window.__ModuleLoader__.load({
|
|
|
4388
4094
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, {});
|
|
4389
4095
|
}
|
|
4390
4096
|
//#endregion
|
|
4391
|
-
//#region src/client/
|
|
4097
|
+
//#region src/client/router-components.ts
|
|
4098
|
+
/** 空组:加载前与加载失败都用它,保证形状恒定。 */
|
|
4099
|
+
var EMPTY_COMPONENTS = {
|
|
4100
|
+
external: [],
|
|
4101
|
+
ext: []
|
|
4102
|
+
};
|
|
4103
|
+
/** 一条外部供应商摘要 → 只读行。 */
|
|
4104
|
+
function supplierRow(s) {
|
|
4105
|
+
return {
|
|
4106
|
+
key: `supplier:${s.id}`,
|
|
4107
|
+
name: s.name,
|
|
4108
|
+
enabled: s.enabled !== false,
|
|
4109
|
+
togglable: true,
|
|
4110
|
+
...s.icon === void 0 ? {} : { icon: s.icon }
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
4392
4113
|
/**
|
|
4393
|
-
*
|
|
4394
|
-
*
|
|
4395
|
-
* 为什么需要这个:DSH 0.1.x 的 `settings.section` 注册**只认 id / order / label
|
|
4396
|
-
* 三个字段**,图标是宿主内部按「内置 id 白名单」选的,插件传不了。所以外部
|
|
4397
|
-
* section 一律拿到通用齿轮(跟「通用设置」「远程访问」同一个)。
|
|
4398
|
-
* 在契约长出 icon 字段之前,只能注册后认领自己的那一行。
|
|
4114
|
+
* 把两个端点的答复分成两组。
|
|
4399
4115
|
*
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
4402
|
-
* `
|
|
4403
|
-
*
|
|
4116
|
+
* 供应商这边只收 `source === 'external'`(独立安装的供应商插件)。`builtin`
|
|
4117
|
+
* 的那三个已经作为**原生行**在宿主的「包含的组件」里显示了(core 自己 patch
|
|
4118
|
+
* insert 的子路径模块,开关是宿主管的那个行开关),`user` 的是用户目录里投放的
|
|
4119
|
+
* js —— 两者都不该在这里出现第二次。`source` 缺失按「不是 external」处理:
|
|
4120
|
+
* 不显示总比把内置的错标成独立插件强。
|
|
4404
4121
|
*
|
|
4405
|
-
*
|
|
4406
|
-
*
|
|
4407
|
-
*
|
|
4122
|
+
* 两组都在这一页开关,指向核心的同两个端点(`PATCH /suppliers/:id/enabled` 与
|
|
4123
|
+
* `PATCH /ext`),所以「在哪开关」是一致的:**设置 → 插件 → dsh-router-core**。
|
|
4124
|
+
* 供应商的开关关掉是真的不参与路由(见 Router 的活跃集合),不是只隐藏卡片。
|
|
4408
4125
|
*/
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4126
|
+
function groupRouterComponents(health, ext) {
|
|
4127
|
+
return {
|
|
4128
|
+
external: (health.suppliers ?? []).filter((s) => s.source === "external").map(supplierRow),
|
|
4129
|
+
ext: (ext.enhancers ?? []).filter((e) => e.source !== "builtin").map((e) => ({
|
|
4130
|
+
key: `ext:${e.id}`,
|
|
4131
|
+
name: e.name,
|
|
4132
|
+
enabled: e.enabled === true,
|
|
4133
|
+
ready: e.ready === true,
|
|
4134
|
+
togglable: true,
|
|
4135
|
+
...e.icon === void 0 ? {} : { icon: e.icon },
|
|
4136
|
+
...e.detail === void 0 ? {} : { detail: e.detail }
|
|
4137
|
+
}))
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
/** 两组加起来一个都没有 → 这一节不渲染(不留空标题)。 */
|
|
4141
|
+
function isEmptyComponents(components) {
|
|
4142
|
+
return components.external.length === 0 && components.ext.length === 0;
|
|
4143
|
+
}
|
|
4144
|
+
//#endregion
|
|
4145
|
+
//#region src/client/RouterComponentsSection.tsx
|
|
4146
|
+
/**
|
|
4147
|
+
* 官方插件页(设置 → 插件)里 dsh-router-core 详情页的「路由组件」一节。
|
|
4148
|
+
*
|
|
4149
|
+
* 为什么在这里而不是官方那个「包含的组件」:那一节的行由宿主从**本 bundle 自己
|
|
4150
|
+
* 的 cordis.patch.yml** 算出来(plugin-manager `declaredRows`),跨 bundle 没有
|
|
4151
|
+
* 注入通道;而子插件(dsh-router-codebuddy / -ext-rtk …)各自是独立 bundle,
|
|
4152
|
+
* 必须保持独立安装。官方给的位置就是本节用的 `plugins.detail.section` 槽位
|
|
4153
|
+
* (渲染在原生行列表**之后**,自带 chrome)。
|
|
4154
|
+
*
|
|
4155
|
+
* **内置的三个供应商不在这一节**:它们是 dsh-router-core 自己 patch 里的行
|
|
4156
|
+
* (`cordis.patch.yml` insert 的子路径模块),已经在宿主的原生「包含的组件」里,
|
|
4157
|
+
* 带着宿主管的开关。在这里再列一遍就是同一个东西显示两处。
|
|
4158
|
+
*
|
|
4159
|
+
* 数据全部来自核心**已经在提供**的两个端点,不新增任何 API:
|
|
4160
|
+
* - `GET /router/api/health` → `suppliers[]`(只取 `source === 'external'`)
|
|
4161
|
+
* - `GET /router/api/ext` → `enhancers[]`(核心已把开关与就绪状态合并好)
|
|
4162
|
+
*
|
|
4163
|
+
* 交互按各组真实能力给,不假装能开关:只有 ext 走 `PATCH /router/api/ext`
|
|
4164
|
+
* (核心持久化到 `<dataDir>/ext.json`);供应商走 `PATCH /suppliers/:id/enabled`
|
|
4165
|
+
* (落到 `supplier-config.json`),关掉后核心的活跃集合里就没有它,请求不会落到它。
|
|
4166
|
+
*/
|
|
4167
|
+
/**
|
|
4168
|
+
* 本 bundle 的包名 —— 宿主用它标识「哪个插件的详情页」。
|
|
4169
|
+
* 之前导出去是为了给行详情页的槽位 key 拼前缀;那个入口已撤,这一节自己用即可。
|
|
4170
|
+
*/
|
|
4171
|
+
var BUNDLE_NAME = "dsh-router-core";
|
|
4172
|
+
/** 两组的标题与组级说明(顺序即渲染顺序)。 */
|
|
4173
|
+
var GROUPS = [{
|
|
4174
|
+
key: "external",
|
|
4175
|
+
title: "供应商",
|
|
4176
|
+
hint: "关掉的供应商不参与路由,请求不会落到它"
|
|
4177
|
+
}, {
|
|
4178
|
+
key: "ext",
|
|
4179
|
+
title: "扩展",
|
|
4180
|
+
hint: "独立安装的扩展插件;内置扩展在上面的「包含的组件」里"
|
|
4181
|
+
}];
|
|
4182
|
+
/** 读一个 GET 端点;失败当空答复,形状恒定。 */
|
|
4183
|
+
async function readJson(path) {
|
|
4184
|
+
try {
|
|
4185
|
+
const data = await (await fetch(`${ROUTER_API_BASE}${path}`, { cache: "no-store" })).json();
|
|
4186
|
+
if (data.ok === false) return void 0;
|
|
4187
|
+
return data;
|
|
4188
|
+
} catch {
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
/** 写扩展开关;答复带回合并后的整张扩展表(核心 PATCH 后会重发一次列表)。 */
|
|
4193
|
+
async function writeExt(id, enabled) {
|
|
4194
|
+
try {
|
|
4195
|
+
return await (await fetch(`${ROUTER_API_BASE}/ext`, {
|
|
4196
|
+
method: "PATCH",
|
|
4197
|
+
headers: { "Content-Type": "application/json" },
|
|
4198
|
+
body: JSON.stringify({
|
|
4199
|
+
id,
|
|
4200
|
+
enabled
|
|
4201
|
+
}),
|
|
4202
|
+
cache: "no-store"
|
|
4203
|
+
})).json();
|
|
4204
|
+
} catch {
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* 写供应商开关(核心的通用端点,`supplierRoutes` 生成)。
|
|
4210
|
+
*
|
|
4211
|
+
* 关掉是真的不参与路由 —— 核心的活跃集合里就没有它了,请求不会落到它,而它自己
|
|
4212
|
+
* 仍留在列表里(关掉后还能再开)。答复只回 `{ok, id, enabled}`,所以成功后就重读
|
|
4213
|
+
* `/health`,不在这儿猜状态。
|
|
4214
|
+
*/
|
|
4215
|
+
async function writeSupplier(id, enabled) {
|
|
4216
|
+
try {
|
|
4217
|
+
return await (await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(id)}/enabled`, {
|
|
4218
|
+
method: "PATCH",
|
|
4219
|
+
headers: { "Content-Type": "application/json" },
|
|
4220
|
+
body: JSON.stringify({ enabled }),
|
|
4221
|
+
cache: "no-store"
|
|
4222
|
+
})).json();
|
|
4223
|
+
} catch {
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
function RowIcon({ row }) {
|
|
4228
|
+
if (row.icon === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4229
|
+
className: "dshr-compRowIcon",
|
|
4230
|
+
"aria-hidden": "true",
|
|
4231
|
+
children: "◇"
|
|
4232
|
+
});
|
|
4233
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4234
|
+
className: "dshr-compRowIcon",
|
|
4235
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
4236
|
+
src: row.icon,
|
|
4237
|
+
alt: "",
|
|
4238
|
+
onError: (e) => {
|
|
4239
|
+
e.currentTarget.style.display = "none";
|
|
4240
|
+
}
|
|
4241
|
+
})
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
/** 这一行是不是扩展(键前缀 `ext:`)—— 扩展有 `ready` 自检,供应商没有。 */
|
|
4245
|
+
function isExt(row) {
|
|
4246
|
+
return row.key.startsWith("ext:");
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* 一行:图标 + 名称 + 状态说明。
|
|
4250
|
+
*
|
|
4251
|
+
* 状态说明只给 ext 行(它的启停与就绪是运行时事实,值得逐条说);供应商行
|
|
4252
|
+
* 在这一页没有可写的开关,含义由组标题承载,不编一句假状态给它。
|
|
4253
|
+
*/
|
|
4254
|
+
function Row({ row, busy, onToggle }) {
|
|
4255
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4256
|
+
className: "dshr-compRow",
|
|
4257
|
+
children: [
|
|
4258
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowIcon, { row }),
|
|
4259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4260
|
+
className: "dshr-compRowMain",
|
|
4261
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4262
|
+
className: "dshr-compRowName",
|
|
4263
|
+
children: row.name
|
|
4264
|
+
}), row.togglable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4265
|
+
className: "dshr-compRowState",
|
|
4266
|
+
children: row.detail ?? (row.enabled === true ? "已启用" : "已关闭")
|
|
4267
|
+
})]
|
|
4268
|
+
}),
|
|
4269
|
+
row.togglable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
4270
|
+
checked: row.enabled === true,
|
|
4271
|
+
disabled: busy || !row.enabled && isExt(row) && row.ready !== true,
|
|
4272
|
+
label: `${row.name} 开关`,
|
|
4273
|
+
title: !row.enabled && isExt(row) && row.ready !== true ? row.detail ?? "当前不可用,无法开启" : row.enabled === true ? `关闭 ${row.name}` : `开启 ${row.name}`,
|
|
4274
|
+
onChange: (next) => {
|
|
4275
|
+
onToggle(row, next);
|
|
4276
|
+
}
|
|
4277
|
+
})
|
|
4278
|
+
]
|
|
4279
|
+
});
|
|
4280
|
+
}
|
|
4281
|
+
function RouterComponentsSection({ subject }) {
|
|
4282
|
+
const [components, setComponents] = (0, react.useState)(EMPTY_COMPONENTS);
|
|
4283
|
+
const [error, setError] = (0, react.useState)("");
|
|
4284
|
+
const [loaded, setLoaded] = (0, react.useState)(false);
|
|
4285
|
+
const [busy, setBusy] = (0, react.useState)("");
|
|
4286
|
+
const live = (0, react.useRef)(true);
|
|
4287
|
+
const mine = subject?.kind === "bundle" && subject?.pkg?.name === BUNDLE_NAME;
|
|
4288
|
+
const load = async () => {
|
|
4289
|
+
const [health, ext] = await Promise.all([readJson("/health"), readJson("/ext")]);
|
|
4290
|
+
if (!live.current) return;
|
|
4291
|
+
setError(health === void 0 && ext === void 0 ? "读取路由组件失败" : "");
|
|
4292
|
+
setComponents(groupRouterComponents(health ?? { ok: false }, ext ?? { ok: false }));
|
|
4293
|
+
setLoaded(true);
|
|
4294
|
+
};
|
|
4295
|
+
(0, react.useEffect)(() => {
|
|
4296
|
+
if (!mine) return;
|
|
4297
|
+
live.current = true;
|
|
4298
|
+
load();
|
|
4299
|
+
return () => {
|
|
4300
|
+
live.current = false;
|
|
4301
|
+
};
|
|
4302
|
+
}, [mine]);
|
|
4303
|
+
if (!mine) return null;
|
|
4304
|
+
const toggle = async (row, next) => {
|
|
4305
|
+
const id = row.key.slice(row.key.indexOf(":") + 1);
|
|
4306
|
+
setBusy(id);
|
|
4307
|
+
setError("");
|
|
4308
|
+
if (isExt(row)) {
|
|
4309
|
+
const data = await writeExt(id, next);
|
|
4310
|
+
if (!live.current) return;
|
|
4311
|
+
setBusy("");
|
|
4312
|
+
if (data?.ok === true) {
|
|
4313
|
+
const health = await readJson("/health");
|
|
4314
|
+
setComponents(groupRouterComponents(health ?? { ok: true }, data));
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
setError(data?.error ?? "切换扩展失败");
|
|
4318
|
+
await load();
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
const data = await writeSupplier(id, next);
|
|
4322
|
+
if (!live.current) return;
|
|
4323
|
+
setBusy("");
|
|
4324
|
+
if (data?.ok === true) {
|
|
4325
|
+
await load();
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
setError(data?.error ?? "切换供应商失败");
|
|
4329
|
+
await load();
|
|
4330
|
+
};
|
|
4331
|
+
const groups = GROUPS.map(({ key, title, hint }) => ({
|
|
4332
|
+
title,
|
|
4333
|
+
hint,
|
|
4334
|
+
rows: components[key]
|
|
4335
|
+
})).filter((g) => g.rows.length > 0);
|
|
4336
|
+
if (!loaded || isEmptyComponents(components)) return null;
|
|
4337
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
4338
|
+
className: "dshr-comp",
|
|
4339
|
+
"data-router-components": true,
|
|
4340
|
+
children: [error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4341
|
+
className: "dshr-compError",
|
|
4342
|
+
role: "status",
|
|
4343
|
+
children: error
|
|
4344
|
+
}), groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4345
|
+
className: "dshr-compGroup",
|
|
4346
|
+
children: [
|
|
4347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4348
|
+
className: "dshr-compHead",
|
|
4349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
4350
|
+
className: "dshr-compTitle",
|
|
4351
|
+
children: group.title
|
|
4352
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4353
|
+
className: "dshr-compCount",
|
|
4354
|
+
children: [
|
|
4355
|
+
"共 ",
|
|
4356
|
+
group.rows.length,
|
|
4357
|
+
" 个"
|
|
4358
|
+
]
|
|
4359
|
+
})]
|
|
4360
|
+
}),
|
|
4361
|
+
group.hint !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4362
|
+
className: "dshr-compHint",
|
|
4363
|
+
children: group.hint
|
|
4364
|
+
}),
|
|
4365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
4366
|
+
className: "dshr-compRows",
|
|
4367
|
+
children: group.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
4368
|
+
row,
|
|
4369
|
+
busy: busy === row.key.slice(row.key.indexOf(":") + 1),
|
|
4370
|
+
onToggle: toggle
|
|
4371
|
+
}, row.key))
|
|
4372
|
+
})
|
|
4373
|
+
]
|
|
4374
|
+
}, group.title))]
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
//#endregion
|
|
4378
|
+
//#region src/client/model-choice.ts
|
|
4379
|
+
/** 从端点答复里挑出可用的模型,并说明「空的」到底是哪一种空。 */
|
|
4380
|
+
function modelChoice(models) {
|
|
4381
|
+
const ids = models.filter((m) => m.enabled !== false).map((m) => m.id);
|
|
4382
|
+
if (ids.length > 0) return { ids };
|
|
4383
|
+
return {
|
|
4384
|
+
ids,
|
|
4385
|
+
empty: models.length === 0 ? "none" : "all-disabled"
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4388
|
+
//#endregion
|
|
4389
|
+
//#region src/client/ExtTestPanel.tsx
|
|
4390
|
+
/**
|
|
4391
|
+
* 插件自检面板 —— 扩展「插件自检」(`dsh-router-ext-test` / 扩展 id `test`) 的详情内容。
|
|
4392
|
+
*
|
|
4393
|
+
* **入口只有一处**:设置 → 路由 → 扩展 → 点开「插件自检」那张卡片(`ExtDetail` 查
|
|
4394
|
+
* `ext-panels.ts` 的注册表拿到本组件)。
|
|
4395
|
+
*
|
|
4396
|
+
* 刻意**不给插件页那一行也挂一个详情页**:那一行的宿主行开关已经能开/关它,再点进去
|
|
4397
|
+
* 看同一块面板就是同一个东西有两个入口 —— 与「内置扩展不在自绘节里重复列」同理。
|
|
4398
|
+
* 插件页那一行只负责开关。
|
|
4399
|
+
*
|
|
4400
|
+
* 视觉上照着宿主的插件页写(`--dsw-alias-*` 那套 token),**不复用设置页的
|
|
4401
|
+
* `dshr-*` 组件样式**:那些样式依赖 `--rs-*` 变量,而那套变量只定义在
|
|
4402
|
+
* `.dshr-settings` / `.dshr-shell` 下,插件页不在其祖先里(未定义的 `var()` 会让
|
|
4403
|
+
* 整条声明失效)。按钮直接用宿主的 `Button` 原语 —— 配色由原语自己的 token 家族
|
|
4404
|
+
* 决定,不必在这里猜「主色底上该配什么字色」。
|
|
4405
|
+
*/
|
|
4406
|
+
/** 每个契约成员状态的字形(颜色由 data-state 给)。 */
|
|
4407
|
+
/** 模型启用状态那一行属于核心侧,没在 members 里,状态由「拿不拿得到」决定。 */
|
|
4408
|
+
function coreModelsState(report) {
|
|
4409
|
+
return report.core.models.total === null ? "unverified" : "ok";
|
|
4410
|
+
}
|
|
4411
|
+
var MEMBER_MARK = {
|
|
4412
|
+
ok: "✓",
|
|
4413
|
+
fail: "✕",
|
|
4414
|
+
absent: "–",
|
|
4415
|
+
unverified: "⊘"
|
|
4416
|
+
};
|
|
4417
|
+
var TONE_ICON = {
|
|
4418
|
+
ok: "M20 6L9 17l-5-5",
|
|
4419
|
+
fail: "M18 6L6 18M6 6l12 12",
|
|
4420
|
+
network: "M12 9v4m0 4h.01M10.3 3.9L2 18a2 2 0 003 2h16a2 2 0 002-2L13.7 3.9a2 2 0 00-3.4 0z"
|
|
4421
|
+
};
|
|
4422
|
+
function Glyph({ d }) {
|
|
4423
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4424
|
+
className: "dshr-compNoticeIcon",
|
|
4425
|
+
viewBox: "0 0 24 24",
|
|
4426
|
+
fill: "none",
|
|
4427
|
+
stroke: "currentColor",
|
|
4428
|
+
strokeWidth: "2",
|
|
4429
|
+
strokeLinecap: "round",
|
|
4430
|
+
strokeLinejoin: "round",
|
|
4431
|
+
"aria-hidden": "true",
|
|
4432
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
/** 三个下拉里的一栏:标题行(可带右侧小注)+ 控件。`emptyHint` 在没得选时说明「为什么没有」。 */
|
|
4436
|
+
function Picker(props) {
|
|
4437
|
+
const empty = props.options.length === 0;
|
|
4438
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4439
|
+
className: "dshr-compField",
|
|
4440
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4441
|
+
className: "dshr-compFieldHead",
|
|
4442
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
4443
|
+
className: "dshr-compFieldLabel",
|
|
4444
|
+
children: props.label
|
|
4445
|
+
}), props.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4446
|
+
className: "dshr-compFieldNote",
|
|
4447
|
+
children: props.note
|
|
4448
|
+
})]
|
|
4449
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
4450
|
+
className: "dshr-compSelect",
|
|
4451
|
+
value: props.value,
|
|
4452
|
+
disabled: props.disabled === true || empty,
|
|
4453
|
+
"aria-label": props.label,
|
|
4454
|
+
onChange: (e) => {
|
|
4455
|
+
props.onChange(e.target.value);
|
|
4456
|
+
},
|
|
4457
|
+
children: empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4458
|
+
value: "",
|
|
4459
|
+
children: props.emptyHint
|
|
4460
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4461
|
+
value: "",
|
|
4462
|
+
children: "请选择"
|
|
4463
|
+
}), props.options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
4464
|
+
value: o.value,
|
|
4465
|
+
children: o.label
|
|
4466
|
+
}, o.value))] })
|
|
4467
|
+
})]
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
function ExtTestPanel() {
|
|
4471
|
+
const [suppliers, setSuppliers] = (0, react.useState)([]);
|
|
4472
|
+
const [accounts, setAccounts] = (0, react.useState)([]);
|
|
4473
|
+
const [models, setModels] = (0, react.useState)({ ids: [] });
|
|
4474
|
+
const [supplierId, setSupplierId] = (0, react.useState)("");
|
|
4475
|
+
const [modelId, setModelId] = (0, react.useState)("");
|
|
4476
|
+
const [uid, setUid] = (0, react.useState)("");
|
|
4477
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
4478
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
4479
|
+
const [loaded, setLoaded] = (0, react.useState)(false);
|
|
4480
|
+
const [pluginId, setPluginId] = (0, react.useState)("");
|
|
4481
|
+
const [probing, setProbing] = (0, react.useState)(false);
|
|
4482
|
+
const [report, setReport] = (0, react.useState)(null);
|
|
4483
|
+
const [probeError, setProbeError] = (0, react.useState)("");
|
|
4484
|
+
(0, react.useEffect)(() => {
|
|
4485
|
+
let live = true;
|
|
4486
|
+
(async () => {
|
|
4487
|
+
try {
|
|
4488
|
+
const [health, status] = await Promise.all([fetch(`${ROUTER_API_BASE}/health`, { cache: "no-store" }).then((r) => r.json()), fetch(`${ROUTER_API_BASE}/status`, { cache: "no-store" }).then((r) => r.json())]);
|
|
4489
|
+
if (!live) return;
|
|
4490
|
+
setSuppliers((health.suppliers ?? []).filter((s) => s.enabled !== false));
|
|
4491
|
+
setAccounts(status.ok ? status.accounts ?? [] : []);
|
|
4492
|
+
} catch (err) {
|
|
4493
|
+
if (live) setNotice({
|
|
4494
|
+
tone: "network",
|
|
4495
|
+
text: err.message
|
|
4496
|
+
});
|
|
4497
|
+
} finally {
|
|
4498
|
+
if (live) setLoaded(true);
|
|
4499
|
+
}
|
|
4500
|
+
})();
|
|
4501
|
+
return () => {
|
|
4502
|
+
live = false;
|
|
4503
|
+
};
|
|
4504
|
+
}, []);
|
|
4505
|
+
(0, react.useEffect)(() => {
|
|
4506
|
+
setModelId("");
|
|
4507
|
+
setUid("");
|
|
4508
|
+
setNotice(null);
|
|
4509
|
+
if (supplierId === "") {
|
|
4510
|
+
setModels({ ids: [] });
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
let live = true;
|
|
4514
|
+
(async () => {
|
|
4515
|
+
try {
|
|
4516
|
+
const data = await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(supplierId)}/models`, { cache: "no-store" }).then((r) => r.json());
|
|
4517
|
+
if (live) setModels(data.ok ? modelChoice(data.models ?? []) : { ids: [] });
|
|
4518
|
+
} catch {
|
|
4519
|
+
if (live) setModels({ ids: [] });
|
|
4520
|
+
}
|
|
4521
|
+
})();
|
|
4522
|
+
return () => {
|
|
4523
|
+
live = false;
|
|
4524
|
+
};
|
|
4525
|
+
}, [supplierId]);
|
|
4526
|
+
const pluginSuppliers = suppliers.filter((x) => x.source === "external");
|
|
4527
|
+
const runProbe = async () => {
|
|
4528
|
+
if (pluginId === "" || probing) return;
|
|
4529
|
+
setProbing(true);
|
|
4530
|
+
setProbeError("");
|
|
4531
|
+
setReport(null);
|
|
4532
|
+
try {
|
|
4533
|
+
const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(pluginId)}/probe`, {
|
|
4534
|
+
method: "POST",
|
|
4535
|
+
headers: { "Content-Type": "application/json" },
|
|
4536
|
+
body: JSON.stringify({}),
|
|
4537
|
+
cache: "no-store"
|
|
4538
|
+
})).json();
|
|
4539
|
+
if (data.ok && data.report !== void 0) setReport(data.report);
|
|
4540
|
+
else setProbeError(data.error ?? "体检失败");
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
setProbeError(err.message);
|
|
4543
|
+
} finally {
|
|
4544
|
+
setProbing(false);
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
const links = accounts.filter((a) => a.supplier === supplierId);
|
|
4548
|
+
const runnable = supplierId !== "" && modelId !== "";
|
|
4549
|
+
const supplierName = suppliers.find((s) => s.id === supplierId)?.name ?? supplierId;
|
|
4550
|
+
const run = async () => {
|
|
4551
|
+
if (!runnable || busy) return;
|
|
4552
|
+
setBusy(true);
|
|
4553
|
+
setNotice(null);
|
|
4554
|
+
try {
|
|
4555
|
+
const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(supplierId)}/models/test`, {
|
|
4556
|
+
method: "POST",
|
|
4557
|
+
headers: { "Content-Type": "application/json" },
|
|
4558
|
+
body: JSON.stringify({
|
|
4559
|
+
id: modelId,
|
|
4560
|
+
uid
|
|
4561
|
+
}),
|
|
4562
|
+
cache: "no-store"
|
|
4563
|
+
})).json();
|
|
4564
|
+
const result = {
|
|
4565
|
+
ok: data.ok,
|
|
4566
|
+
uid,
|
|
4567
|
+
supplier: supplierName,
|
|
4568
|
+
model: modelId,
|
|
4569
|
+
...data.error === void 0 ? {} : { error: data.error }
|
|
4570
|
+
};
|
|
4571
|
+
setNotice({
|
|
4572
|
+
tone: data.ok ? "ok" : "fail",
|
|
4573
|
+
result
|
|
4574
|
+
});
|
|
4575
|
+
} catch (err) {
|
|
4576
|
+
setNotice({
|
|
4577
|
+
tone: "network",
|
|
4578
|
+
text: err.message
|
|
4579
|
+
});
|
|
4580
|
+
} finally {
|
|
4581
|
+
setBusy(false);
|
|
4582
|
+
}
|
|
4583
|
+
};
|
|
4584
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4585
|
+
className: "dshr-tabBody dshr-comp",
|
|
4586
|
+
children: [
|
|
4587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4588
|
+
className: "dshr-compHead",
|
|
4589
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
4590
|
+
className: "dshr-compSectionTitle",
|
|
4591
|
+
children: "连接测试"
|
|
4592
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4593
|
+
className: "dshr-compSectionNote",
|
|
4594
|
+
children: "选供应商 / 模型 / 连接"
|
|
4595
|
+
})]
|
|
4596
|
+
}),
|
|
4597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4598
|
+
className: "dshr-compForm",
|
|
4599
|
+
children: [
|
|
4600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
4601
|
+
label: "供应商",
|
|
4602
|
+
value: supplierId,
|
|
4603
|
+
options: suppliers.map((s) => ({
|
|
4604
|
+
value: s.id,
|
|
4605
|
+
label: s.name
|
|
4606
|
+
})),
|
|
4607
|
+
emptyHint: loaded ? "没有已启用的供应商" : "加载中…",
|
|
4608
|
+
onChange: setSupplierId
|
|
4609
|
+
}),
|
|
4610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
4611
|
+
label: "模型",
|
|
4612
|
+
note: "仅可用",
|
|
4613
|
+
value: modelId,
|
|
4614
|
+
options: models.ids.map((id) => ({
|
|
4615
|
+
value: id,
|
|
4616
|
+
label: id
|
|
4617
|
+
})),
|
|
4618
|
+
emptyHint: supplierId === "" ? "先选供应商" : models.empty === "all-disabled" ? "模型都被停用了(在供应商详情里开启)" : models.empty === "none" ? "没有模型(可先在供应商详情里拉取)" : "请选择",
|
|
4619
|
+
disabled: supplierId === "",
|
|
4620
|
+
onChange: (v) => {
|
|
4621
|
+
setModelId(v);
|
|
4622
|
+
setNotice(null);
|
|
4623
|
+
}
|
|
4624
|
+
}),
|
|
4625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
4626
|
+
label: "连接",
|
|
4627
|
+
note: "留空 = 池内任选",
|
|
4628
|
+
value: uid,
|
|
4629
|
+
options: links.map((a) => ({
|
|
4630
|
+
value: a.uid,
|
|
4631
|
+
label: `${a.nickname ?? a.uid}${a.cooling ? "(冷却中)" : ""}`
|
|
4632
|
+
})),
|
|
4633
|
+
emptyHint: supplierId === "" ? "先选供应商" : "没有连接(留空则不指定)",
|
|
4634
|
+
disabled: supplierId === "",
|
|
4635
|
+
onChange: (v) => {
|
|
4636
|
+
setUid(v);
|
|
4637
|
+
setNotice(null);
|
|
4638
|
+
}
|
|
4639
|
+
})
|
|
4640
|
+
]
|
|
4641
|
+
}),
|
|
4642
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4643
|
+
className: "dshr-compActions dshr-compBtnRow",
|
|
4644
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4645
|
+
variant: "primary",
|
|
4646
|
+
size: "md",
|
|
4647
|
+
disabled: !runnable || busy,
|
|
4648
|
+
onClick: () => {
|
|
4649
|
+
run();
|
|
4650
|
+
},
|
|
4651
|
+
children: busy ? "测试中…" : "测试"
|
|
4652
|
+
}), !runnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4653
|
+
className: "dshr-compHint",
|
|
4654
|
+
children: "先选供应商与模型"
|
|
4655
|
+
})]
|
|
4656
|
+
}),
|
|
4657
|
+
notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4658
|
+
className: "dshr-compNotice",
|
|
4659
|
+
"data-tone": notice.tone,
|
|
4660
|
+
role: "status",
|
|
4661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { d: TONE_ICON[notice.tone] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4662
|
+
className: "dshr-compNoticeBody",
|
|
4663
|
+
children: notice.tone === "network" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4664
|
+
className: "dshr-compNoticeTitle",
|
|
4665
|
+
children: "请求失败"
|
|
4666
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4667
|
+
className: "dshr-compNoticeText",
|
|
4668
|
+
children: notice.text
|
|
4669
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4671
|
+
className: "dshr-compNoticeTitle",
|
|
4672
|
+
children: notice.tone === "ok" ? "测试通过" : "测试失败"
|
|
4673
|
+
}),
|
|
4674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4675
|
+
className: "dshr-compNoticeText",
|
|
4676
|
+
children: notice.tone === "ok" ? "这个连接能正常访问该模型。" : notice.result.error ?? "上游没有给出原因"
|
|
4677
|
+
}),
|
|
4678
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4679
|
+
className: "dshr-compNoticeMeta",
|
|
4680
|
+
children: [
|
|
4681
|
+
notice.result.supplier,
|
|
4682
|
+
" · ",
|
|
4683
|
+
notice.result.model,
|
|
4684
|
+
" · ",
|
|
4685
|
+
notice.result.uid === "" ? "池内任选" : notice.result.uid
|
|
4686
|
+
]
|
|
4687
|
+
})
|
|
4688
|
+
] })
|
|
4689
|
+
})]
|
|
4690
|
+
}),
|
|
4691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4692
|
+
className: "dshr-compSection",
|
|
4693
|
+
children: [
|
|
4694
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4695
|
+
className: "dshr-compHead",
|
|
4696
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
4697
|
+
className: "dshr-compSectionTitle",
|
|
4698
|
+
children: "插件测试"
|
|
4699
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4700
|
+
className: "dshr-compSectionNote",
|
|
4701
|
+
children: "外部供应商插件"
|
|
4702
|
+
})]
|
|
4703
|
+
}),
|
|
4704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4705
|
+
className: "dshr-compIntro",
|
|
4706
|
+
children: "前提:连接池里先有一个可用 token,剩下交给它。"
|
|
4707
|
+
}),
|
|
4708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4709
|
+
className: "dshr-compProbeRow",
|
|
4710
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
4711
|
+
label: "供应商插件",
|
|
4712
|
+
value: pluginId,
|
|
4713
|
+
options: pluginSuppliers.map((x) => ({
|
|
4714
|
+
value: x.id,
|
|
4715
|
+
label: x.name
|
|
4716
|
+
})),
|
|
4717
|
+
emptyHint: pluginSuppliers.length === 0 ? "没有装外部供应商插件" : "请选择",
|
|
4718
|
+
onChange: (v) => {
|
|
4719
|
+
setPluginId(v);
|
|
4720
|
+
setReport(null);
|
|
4721
|
+
setProbeError("");
|
|
4722
|
+
}
|
|
4723
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4724
|
+
className: "dshr-compProbeBtn dshr-compBtnRow",
|
|
4725
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
4726
|
+
variant: "primary",
|
|
4727
|
+
size: "md",
|
|
4728
|
+
disabled: pluginId === "" || probing,
|
|
4729
|
+
onClick: () => {
|
|
4730
|
+
runProbe();
|
|
4731
|
+
},
|
|
4732
|
+
children: probing ? "测试中…" : "测试"
|
|
4733
|
+
})
|
|
4734
|
+
})]
|
|
4735
|
+
}),
|
|
4736
|
+
probeError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4737
|
+
className: "dshr-compNotice",
|
|
4738
|
+
"data-tone": "network",
|
|
4739
|
+
role: "status",
|
|
4740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { d: TONE_ICON.network }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4741
|
+
className: "dshr-compNoticeBody",
|
|
4742
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4743
|
+
className: "dshr-compNoticeTitle",
|
|
4744
|
+
children: "体检失败"
|
|
4745
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4746
|
+
className: "dshr-compNoticeText",
|
|
4747
|
+
children: probeError
|
|
4748
|
+
})]
|
|
4749
|
+
})]
|
|
4750
|
+
}),
|
|
4751
|
+
report !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4752
|
+
className: "dshr-compReport",
|
|
4753
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4754
|
+
className: "dshr-compReportHead",
|
|
4755
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4756
|
+
className: "dshr-compReportName",
|
|
4757
|
+
children: report.name
|
|
4758
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4759
|
+
className: "dshr-compReportSum",
|
|
4760
|
+
children: [
|
|
4761
|
+
"共 ",
|
|
4762
|
+
report.summary.total,
|
|
4763
|
+
" 个 · 实现 ",
|
|
4764
|
+
report.summary.implemented,
|
|
4765
|
+
" · 实跑 ",
|
|
4766
|
+
report.summary.ran,
|
|
4767
|
+
" · 通过 ",
|
|
4768
|
+
report.summary.ok,
|
|
4769
|
+
" · 失败 ",
|
|
4770
|
+
report.summary.fail,
|
|
4771
|
+
" · 未验 ",
|
|
4772
|
+
report.summary.unverified,
|
|
4773
|
+
" · 未实现 ",
|
|
4774
|
+
report.summary.absent,
|
|
4775
|
+
report.summary.bulk === "fail" ? " · 全部启用/禁用有问题" : report.summary.bulk === "ok" ? " · 全部启用/禁用通过" : ""
|
|
4776
|
+
]
|
|
4777
|
+
})]
|
|
4778
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
4779
|
+
className: "dshr-compMembers",
|
|
4780
|
+
children: [
|
|
4781
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4782
|
+
className: "dshr-compMember",
|
|
4783
|
+
"data-state": coreModelsState(report),
|
|
4784
|
+
children: [
|
|
4785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4786
|
+
className: "dshr-compMemberState",
|
|
4787
|
+
"aria-hidden": "true",
|
|
4788
|
+
children: MEMBER_MARK[coreModelsState(report)]
|
|
4789
|
+
}),
|
|
4790
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4791
|
+
className: "dshr-compMemberName",
|
|
4792
|
+
children: ["模型启用状态", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
4793
|
+
className: "dshr-compMemberKey",
|
|
4794
|
+
children: "core"
|
|
4795
|
+
})]
|
|
4796
|
+
}),
|
|
4797
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4798
|
+
className: "dshr-compMemberDetail",
|
|
4799
|
+
children: report.core.models.total === null ? report.core.models.note ?? "拿不到" : `共 ${report.core.models.total} 个 · 启用 ${report.core.models.enabled} · 停用 ${report.core.models.disabled}`
|
|
4800
|
+
})
|
|
4801
|
+
]
|
|
4802
|
+
}),
|
|
4803
|
+
report.core.operations.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4804
|
+
className: "dshr-compMember",
|
|
4805
|
+
"data-state": o.ran === false ? "unverified" : o.ok === true ? "ok" : "fail",
|
|
4806
|
+
children: [
|
|
4807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4808
|
+
className: "dshr-compMemberState",
|
|
4809
|
+
"aria-hidden": "true",
|
|
4810
|
+
children: MEMBER_MARK[o.ran === false ? "unverified" : o.ok === true ? "ok" : "fail"]
|
|
4811
|
+
}),
|
|
4812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4813
|
+
className: "dshr-compMemberName",
|
|
4814
|
+
children: [o.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
4815
|
+
className: "dshr-compMemberKey",
|
|
4816
|
+
children: "core"
|
|
4817
|
+
})]
|
|
4818
|
+
}),
|
|
4819
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4820
|
+
className: "dshr-compMemberDetail",
|
|
4821
|
+
children: o.detail
|
|
4822
|
+
})
|
|
4823
|
+
]
|
|
4824
|
+
}, o.key)),
|
|
4825
|
+
report.members.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
4826
|
+
className: "dshr-compMember",
|
|
4827
|
+
"data-state": m.state,
|
|
4828
|
+
children: [
|
|
4829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4830
|
+
className: "dshr-compMemberState",
|
|
4831
|
+
"aria-hidden": "true",
|
|
4832
|
+
children: MEMBER_MARK[m.state]
|
|
4833
|
+
}),
|
|
4834
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4835
|
+
className: "dshr-compMemberName",
|
|
4836
|
+
children: [m.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
4837
|
+
className: "dshr-compMemberKey",
|
|
4838
|
+
children: m.key
|
|
4839
|
+
})]
|
|
4840
|
+
}),
|
|
4841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4842
|
+
className: "dshr-compMemberDetail",
|
|
4843
|
+
children: m.detail ?? ""
|
|
4844
|
+
})
|
|
4845
|
+
]
|
|
4846
|
+
}, m.key))
|
|
4847
|
+
]
|
|
4848
|
+
})]
|
|
4849
|
+
})
|
|
4850
|
+
]
|
|
4851
|
+
})
|
|
4852
|
+
]
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
//#endregion
|
|
4856
|
+
//#region src/ext-test/plugin.ts
|
|
4857
|
+
/** 扩展开关表里的注册键(也是 URL 面板读它的 id)。 */
|
|
4858
|
+
var EXT_TEST_ID = "test";
|
|
4859
|
+
//#endregion
|
|
4860
|
+
//#region src/client/settings-nav-icon.ts
|
|
4861
|
+
/**
|
|
4862
|
+
* 给设置导航里「路由」这一行换掉宿主的默认齿轮。
|
|
4863
|
+
*
|
|
4864
|
+
* 为什么需要这个:DSH 0.1.x 的 `settings.section` 注册**只认 id / order / label
|
|
4865
|
+
* 三个字段**,图标是宿主内部按「内置 id 白名单」选的,插件传不了。所以外部
|
|
4866
|
+
* section 一律拿到通用齿轮(跟「通用设置」「远程访问」同一个)。
|
|
4867
|
+
* 在契约长出 icon 字段之前,只能注册后认领自己的那一行。
|
|
4868
|
+
*
|
|
4869
|
+
* 做法跟 dsh-better-sidebar 的 settings-nav-icon.ts 一致:给自己的行打一个
|
|
4870
|
+
* 属性标记,再由 CSS 用 mask 渲染图标(见 router.css 的
|
|
4871
|
+
* `[data-dsh-router-settings-nav]`)。用 mask + currentColor 是为了跟随
|
|
4872
|
+
* 宿主导航的 hover/active 配色,也保持 16px 的图标节奏。
|
|
4873
|
+
*
|
|
4874
|
+
* 匹配方式:按行的**可见文案**认领(label 可能本地化),且只认 `nav button`,
|
|
4875
|
+
* 碰到同名的别人家 section 不会误伤(我们同时校验它确实在设置面板里)。
|
|
4876
|
+
* 卸载时断开观察并摘掉标记,HMR / 插件热卸载安全。
|
|
4877
|
+
*/
|
|
4878
|
+
/** 打在设置导航按钮上的标记属性。 */
|
|
4879
|
+
var SETTINGS_NAV_MARKER = "data-dsh-router-settings-nav";
|
|
4880
|
+
/** 设置面板的导航按钮选择器(宿主结构:dialog 里的 nav)。 */
|
|
4881
|
+
var NAV_BUTTON_SELECTOR = "[role=\"dialog\"] nav button";
|
|
4882
|
+
/**
|
|
4883
|
+
* 持续把标记打在文案等于 `label` 的那一行的身上。
|
|
4884
|
+
* @param label 当前本地化的 section 标题(跟注册时的 label 一致)
|
|
4416
4885
|
* @returns 清理函数:断开观察 + 摘掉所有本插件打的标记
|
|
4417
4886
|
*/
|
|
4418
4887
|
function registerSettingsNavIcon(label) {
|
|
@@ -4443,7 +4912,7 @@ window.__ModuleLoader__.load({
|
|
|
4443
4912
|
*
|
|
4444
4913
|
* 为什么不能直接改源头:Router 的卡片走宿主的 "unknown" 布局(宿主只认得
|
|
4445
4914
|
* llm-deepseek / llm-pi-ai 两套表单),那句
|
|
4446
|
-
* 「其余字段在 settings.yaml 中,请直接编辑对应段。 (
|
|
4915
|
+
* 「其余字段在 settings.yaml 中,请直接编辑对应段。 (dsh-router)」
|
|
4447
4916
|
* 属于宿主 `settings.models` 的 locale 字典,而字典注册对同一个 (ns, locale)
|
|
4448
4917
|
* 只允许一个占位者(重复注册直接抛),插件覆写不了,契约也没给可覆写字段。
|
|
4449
4918
|
* 所以沿用本插件既有的认领手法(同 settings-nav-icon.ts):按文案指纹认出宿主
|
|
@@ -4455,7 +4924,7 @@ window.__ModuleLoader__.load({
|
|
|
4455
4924
|
* 升级路径:宿主若给该提示长出可覆写字段(或 Router 有了专属布局),删掉本文件。
|
|
4456
4925
|
*/
|
|
4457
4926
|
/** 指纹:语言无关(不受中英文切换影响),且只出现在 Router 卡片上。 */
|
|
4458
|
-
var FINGERPRINT = "(
|
|
4927
|
+
var FINGERPRINT = "(dsh-router)";
|
|
4459
4928
|
/** 正确说法:对齐 README「组合即模型」。 */
|
|
4460
4929
|
var ROUTER_MODEL_HINT_COPY = "组合即模型 · 建好的组合会自动出现在 DSH 模型目录中,选中组合名即可直接使用。";
|
|
4461
4930
|
/**
|
|
@@ -4491,6 +4960,219 @@ window.__ModuleLoader__.load({
|
|
|
4491
4960
|
return () => observer.disconnect();
|
|
4492
4961
|
}
|
|
4493
4962
|
//#endregion
|
|
4963
|
+
//#region src/client/last-hit-store.ts
|
|
4964
|
+
/**
|
|
4965
|
+
* 「最近命中」的会话级数据源。
|
|
4966
|
+
*
|
|
4967
|
+
* 为什么要有这层:徽章组件可能因宿主重渲染而卸载/重建,但**轮询不该跟着重建**
|
|
4968
|
+
* (每个会话一个轮询器,多会话各自独立;组件只订阅结果)。这里做两件事:
|
|
4969
|
+
* - 按 sessionId 维护「最近一次命中」的最新值 + 订阅者集合;
|
|
4970
|
+
* - 每个会话一个定时器,只在有订阅者时开、无订阅者时停。
|
|
4971
|
+
*
|
|
4972
|
+
* 数据源:核心 `/router/api/last-hit?session=`(轻端点,不打 status)。
|
|
4973
|
+
*/
|
|
4974
|
+
var DIGEST_API = "/router/api/last-hit";
|
|
4975
|
+
/** 刷新间隔:命中信息跟着请求走,轮询取一个不打扰的频率。 */
|
|
4976
|
+
var POLL_MS = 3e3;
|
|
4977
|
+
var entries = /* @__PURE__ */ new Map();
|
|
4978
|
+
function entryOf(key) {
|
|
4979
|
+
let e = entries.get(key);
|
|
4980
|
+
if (e === void 0) {
|
|
4981
|
+
e = {
|
|
4982
|
+
value: null,
|
|
4983
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
4984
|
+
timer: null
|
|
4985
|
+
};
|
|
4986
|
+
entries.set(key, e);
|
|
4987
|
+
}
|
|
4988
|
+
return e;
|
|
4989
|
+
}
|
|
4990
|
+
function emit(e) {
|
|
4991
|
+
for (const fn of e.listeners) fn(e.value);
|
|
4992
|
+
}
|
|
4993
|
+
async function refresh(key) {
|
|
4994
|
+
const e = entryOf(key);
|
|
4995
|
+
try {
|
|
4996
|
+
const qs = key === "" ? "" : `?session=${encodeURIComponent(key)}`;
|
|
4997
|
+
const data = await (await fetch(`${DIGEST_API}${qs}`, { cache: "no-store" })).json();
|
|
4998
|
+
e.value = data.ok && data.hit ? data.hit : null;
|
|
4999
|
+
} catch {}
|
|
5000
|
+
emit(e);
|
|
5001
|
+
}
|
|
5002
|
+
/** 读该会话当前的最近命中(同步快照)。 */
|
|
5003
|
+
function getLastHit(sessionId) {
|
|
5004
|
+
return entryOf(sessionId ?? "").value;
|
|
5005
|
+
}
|
|
5006
|
+
/**
|
|
5007
|
+
* 订阅该会话的最近命中。首次订阅即启动轮询,最后一个退订时停。
|
|
5008
|
+
* @param sessionId 宿主会话身份(可能为 undefined,此时键为空串 = 全局)
|
|
5009
|
+
* @param listener 值变化回调
|
|
5010
|
+
* @returns 退订函数
|
|
5011
|
+
*/
|
|
5012
|
+
function subscribeLastHit(sessionId, listener) {
|
|
5013
|
+
const key = sessionId ?? "";
|
|
5014
|
+
const e = entryOf(key);
|
|
5015
|
+
e.listeners.add(listener);
|
|
5016
|
+
if (e.timer === null) {
|
|
5017
|
+
refresh(key);
|
|
5018
|
+
e.timer = window.setInterval(() => void refresh(key), POLL_MS);
|
|
5019
|
+
}
|
|
5020
|
+
return () => {
|
|
5021
|
+
e.listeners.delete(listener);
|
|
5022
|
+
if (e.listeners.size === 0 && e.timer !== null) {
|
|
5023
|
+
window.clearInterval(e.timer);
|
|
5024
|
+
e.timer = null;
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
//#endregion
|
|
5029
|
+
//#region src/client/LastHitDock.tsx
|
|
5030
|
+
/**
|
|
5031
|
+
* 输入框上方的「最近命中」徽章 —— 走 DSH 官方 `conversation.input.dock` 座位
|
|
5032
|
+
* (scope: session,见 dsh-client-ui-conversation 的 slots 契约)。
|
|
5033
|
+
*
|
|
5034
|
+
* 为什么用 slot 而不是认领 DOM:
|
|
5035
|
+
* - slot 的 session scope 会**注入 `sessionId`**(SessionStandardProps),
|
|
5036
|
+
* 于是能按当前会话取「最近命中」—— team 多会话下不会显示别的成员;
|
|
5037
|
+
* - slot 是稳定契约,宿主 hash 类名(如之前的 `.JObwrW_root`)随版本会变。
|
|
5038
|
+
* 参照实现:@deepseek-ai/dsh-client-ui-goal 的 GoalDock 同挂这个座位,
|
|
5039
|
+
* 组件 props 由宿主注入,`inject(sessionId)` 拿会话身份。
|
|
5040
|
+
*
|
|
5041
|
+
* 数据源:核心 `/router/api/last-hit?session=`(按会话取一条轻记录)。
|
|
5042
|
+
*/
|
|
5043
|
+
/** 路由字形(纯 currentColor 描边,14px 与相邻控件对齐)。 */
|
|
5044
|
+
var ICON = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
5045
|
+
viewBox: "0 0 24 24",
|
|
5046
|
+
fill: "none",
|
|
5047
|
+
stroke: "currentColor",
|
|
5048
|
+
strokeWidth: "2",
|
|
5049
|
+
strokeLinecap: "round",
|
|
5050
|
+
strokeLinejoin: "round",
|
|
5051
|
+
"aria-hidden": "true",
|
|
5052
|
+
children: [
|
|
5053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
5054
|
+
cx: "5",
|
|
5055
|
+
cy: "5",
|
|
5056
|
+
r: "2.4"
|
|
5057
|
+
}),
|
|
5058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
5059
|
+
cx: "19",
|
|
5060
|
+
cy: "5",
|
|
5061
|
+
r: "2.4"
|
|
5062
|
+
}),
|
|
5063
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
5064
|
+
cx: "12",
|
|
5065
|
+
cy: "19",
|
|
5066
|
+
r: "2.4"
|
|
5067
|
+
}),
|
|
5068
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7.4 5h9.2" }),
|
|
5069
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 7.4v6.2c0 2.6 3 3.4 4.6 3.8" }),
|
|
5070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19 7.4v6.2c0 2.6-3 3.4-4.6 3.8" })
|
|
5071
|
+
]
|
|
5072
|
+
});
|
|
5073
|
+
/** 账号名:别名 → uid → 「无连接」。 */
|
|
5074
|
+
function connectionName(hit) {
|
|
5075
|
+
const name = hit.account !== void 0 && hit.account !== "" ? hit.account : hit.uid;
|
|
5076
|
+
return name === "" ? "无连接" : name;
|
|
5077
|
+
}
|
|
5078
|
+
/**
|
|
5079
|
+
* 积分展示。**未知就是未知**:`-1` / 缺省一律显示「未知」,
|
|
5080
|
+
* 绝不编一个 0 —— 沿用 store 那条「不用 0 冒充未知」的纪律。
|
|
5081
|
+
*/
|
|
5082
|
+
function creditsText(hit) {
|
|
5083
|
+
return hit.credits === void 0 || hit.credits < 0 ? "未知" : `${Math.round(hit.credits)}`;
|
|
5084
|
+
}
|
|
5085
|
+
/** 模型短名(去掉 `prefix/`;前缀在「服务商」行已单独列出)。 */
|
|
5086
|
+
function modelShort(hit) {
|
|
5087
|
+
const full = hit.model === "" ? hit.supplier : hit.model;
|
|
5088
|
+
const slash = full.lastIndexOf("/");
|
|
5089
|
+
return slash > 0 ? full.slice(slash + 1) : full;
|
|
5090
|
+
}
|
|
5091
|
+
/**
|
|
5092
|
+
* 输入框上方的「最近命中」徽章:图标 + 模型短名;点击展开连接/积分卡片。
|
|
5093
|
+
*
|
|
5094
|
+
* @param props 宿主注入的会话身份
|
|
5095
|
+
*/
|
|
5096
|
+
function LastHitDock({ sessionId }) {
|
|
5097
|
+
const [hit, setHit] = (0, react.useState)(() => getLastHit(sessionId));
|
|
5098
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
5099
|
+
const rootRef = (0, react.useRef)(null);
|
|
5100
|
+
(0, react.useEffect)(() => {
|
|
5101
|
+
console.info("[dsh-router] last-hit dock mounted, sessionId =", sessionId);
|
|
5102
|
+
return subscribeLastHit(sessionId, setHit);
|
|
5103
|
+
}, [sessionId]);
|
|
5104
|
+
(0, react.useEffect)(() => {
|
|
5105
|
+
if (!open) return;
|
|
5106
|
+
const onPointerDown = (e) => {
|
|
5107
|
+
if (e.target instanceof Node && rootRef.current?.contains(e.target) === true) return;
|
|
5108
|
+
setOpen(false);
|
|
5109
|
+
};
|
|
5110
|
+
const onKeyDown = (e) => {
|
|
5111
|
+
if (e.key === "Escape") setOpen(false);
|
|
5112
|
+
};
|
|
5113
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
5114
|
+
document.addEventListener("keydown", onKeyDown);
|
|
5115
|
+
return () => {
|
|
5116
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
5117
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
5118
|
+
};
|
|
5119
|
+
}, [open]);
|
|
5120
|
+
const rows = hit === null ? [] : [
|
|
5121
|
+
["服务商", hit.supplier],
|
|
5122
|
+
["模型", modelShort(hit)],
|
|
5123
|
+
["连接", connectionName(hit)],
|
|
5124
|
+
["积分", creditsText(hit)]
|
|
5125
|
+
];
|
|
5126
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5127
|
+
className: "dshr-lastHit",
|
|
5128
|
+
ref: rootRef,
|
|
5129
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
5130
|
+
type: "button",
|
|
5131
|
+
className: `dshr-lastHit-btn${hit !== null && !hit.ok ? " dshr-lastHit-fail" : ""}`,
|
|
5132
|
+
"aria-label": "最近一次命中",
|
|
5133
|
+
"aria-expanded": open,
|
|
5134
|
+
title: hit === null ? "暂无命中记录" : `${hit.model === "" ? hit.supplier : hit.model} · ${connectionName(hit)}`,
|
|
5135
|
+
onClick: () => setOpen((v) => !v),
|
|
5136
|
+
children: [ICON, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5137
|
+
className: "dshr-lastHit-label",
|
|
5138
|
+
children: hit === null ? "暂无" : modelShort(hit)
|
|
5139
|
+
})]
|
|
5140
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5141
|
+
className: "dshr-lastHit-card",
|
|
5142
|
+
children: [
|
|
5143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5144
|
+
className: "dshr-lastHit-title",
|
|
5145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5146
|
+
className: "dshr-lastHit-titleLabel",
|
|
5147
|
+
children: [ICON, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "路由" })]
|
|
5148
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5149
|
+
className: "dshr-lastHit-titleValue",
|
|
5150
|
+
title: hit?.requested,
|
|
5151
|
+
children: hit === null || hit.requested === "" ? "—" : hit.requested
|
|
5152
|
+
})]
|
|
5153
|
+
}),
|
|
5154
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dshr-lastHit-titleRule" }),
|
|
5155
|
+
rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5156
|
+
className: "dshr-lastHit-row",
|
|
5157
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5158
|
+
className: "dshr-lastHit-key",
|
|
5159
|
+
children: "暂无命中记录"
|
|
5160
|
+
})
|
|
5161
|
+
}) : rows.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5162
|
+
className: "dshr-lastHit-row",
|
|
5163
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5164
|
+
className: "dshr-lastHit-key",
|
|
5165
|
+
children: k
|
|
5166
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5167
|
+
className: "dshr-lastHit-val",
|
|
5168
|
+
children: v
|
|
5169
|
+
})]
|
|
5170
|
+
}, k))
|
|
5171
|
+
]
|
|
5172
|
+
})]
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
//#endregion
|
|
4494
5176
|
//#region src/client/index.tsx
|
|
4495
5177
|
/**
|
|
4496
5178
|
* Client half of dsh-router.
|
|
@@ -4513,6 +5195,39 @@ window.__ModuleLoader__.load({
|
|
|
4513
5195
|
* 连 `ctx.slots === undefined` 这种防御性判断都跑不到,getter 先炸。
|
|
4514
5196
|
*/
|
|
4515
5197
|
var inject = ["slots"];
|
|
5198
|
+
/**
|
|
5199
|
+
* 挂载输入框底部的「最近命中」徽章:注册到官方 `conversation.composer.dock`
|
|
5200
|
+
* (kind: list, scope: session —— 即宿主 composer 卡片底部操作行
|
|
5201
|
+
* `.uV2eYG_dock` 里、原生上下文环旁边的位置)。
|
|
5202
|
+
* scope 为 session 时宿主给组件注入 `sessionId`,据此按**当前会话**取命中。
|
|
5203
|
+
*
|
|
5204
|
+
* **仅宿主 >= 0.1.7 才挂**:composer.dock 这个座位在 0.1.5 也在,但渲染在
|
|
5205
|
+
* InputBar 之后的独立块(位置不对),只有 0.1.7 才放进底部操作行。座位名相同
|
|
5206
|
+
* 没法靠契约区分,故先问核心 `/router/api/health` 的 `lastHitDock` 支持位。
|
|
5207
|
+
* @returns 清理函数;不支持或宿主无 slots 时为空操作
|
|
5208
|
+
*/
|
|
5209
|
+
function mountLastHitDock(ctx, supported) {
|
|
5210
|
+
const slots = ctx.slots;
|
|
5211
|
+
if (slots === void 0 || !supported) return () => {};
|
|
5212
|
+
return slots.inject("conversation.composer.dock", () => slots.register({
|
|
5213
|
+
name: "conversation.composer.dock",
|
|
5214
|
+
id: "dsh-router-last-hit",
|
|
5215
|
+
order: 10
|
|
5216
|
+
}, LastHitDock));
|
|
5217
|
+
}
|
|
5218
|
+
/**
|
|
5219
|
+
* 问核心是否支持「最近命中」徽章(宿主 >= 0.1.7)。
|
|
5220
|
+
* 网络/解析失败一律当**不支持**——宁可不显示,也不在 0.1.5 上摆错位置。
|
|
5221
|
+
*/
|
|
5222
|
+
async function fetchLastHitDockSupport() {
|
|
5223
|
+
try {
|
|
5224
|
+
const res = await fetch(`${ROUTER_API_BASE}/health`);
|
|
5225
|
+
if (!res.ok) return false;
|
|
5226
|
+
return (await res.json()).lastHitDock === true;
|
|
5227
|
+
} catch {
|
|
5228
|
+
return false;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
4516
5231
|
/** 挂载「设置 → 路由」页;拿不到 slots 时返回 undefined 表示跳过。 */
|
|
4517
5232
|
function mountSettingsSection(ctx) {
|
|
4518
5233
|
const slots = ctx.slots;
|
|
@@ -4524,7 +5239,40 @@ window.__ModuleLoader__.load({
|
|
|
4524
5239
|
label: SECTION_LABEL
|
|
4525
5240
|
}, RouterSettingsSection));
|
|
4526
5241
|
}
|
|
5242
|
+
/**
|
|
5243
|
+
* 官方插件页(设置 → 插件)里 dsh-router-core 详情页的「路由组件」一节。
|
|
5244
|
+
*
|
|
5245
|
+
* 为什么走 `plugins.detail.section` 而不是官方那个「包含的组件」:后者的行由
|
|
5246
|
+
* 宿主从**本 bundle 自己的 cordis.patch.yml** 算出来,跨 bundle 没有注入通道,
|
|
5247
|
+
* 而子插件(codebuddy / ext-rtk …)必须保持各自独立安装。官方给的位置就是这一
|
|
5248
|
+
* 个槽位(渲染在原生行列表之后)。槽位不存在的老宿主上 `inject` 不会触发,
|
|
5249
|
+
* 自动不挂——不引入硬 client inject。
|
|
5250
|
+
*/
|
|
5251
|
+
function mountComponentsSection(ctx) {
|
|
5252
|
+
const slots = ctx.slots;
|
|
5253
|
+
if (slots === void 0) return void 0;
|
|
5254
|
+
return slots.inject("plugins.detail.section", () => slots.register({
|
|
5255
|
+
name: "plugins.detail.section",
|
|
5256
|
+
id: "dsh-router-components",
|
|
5257
|
+
order: 10
|
|
5258
|
+
}, RouterComponentsSection));
|
|
5259
|
+
}
|
|
5260
|
+
/**
|
|
5261
|
+
* 给「插件自检」扩展登记它的详情面板。
|
|
5262
|
+
*
|
|
5263
|
+
* 面板的**唯一入口**是设置 → 路由 → 扩展 → 点开那张卡片(`ExtDetail` 查注册表拿到
|
|
5264
|
+
* 本组件)。**刻意不给插件页那一行再挂一个详情页**:那一行的宿主行开关已经能开/关
|
|
5265
|
+
* 它了,再点进去看同一块面板就是同一个东西有两个入口 —— 与「内置扩展不在自绘节里
|
|
5266
|
+
* 重复列」是同一条理由。
|
|
5267
|
+
*/
|
|
5268
|
+
function registerExtTestPanel(ctx) {
|
|
5269
|
+
const dispose = registerExtPanel(EXT_TEST_ID, ExtTestPanel);
|
|
5270
|
+
ctx.effect(() => dispose, "dsh-router: ext test panel registry");
|
|
5271
|
+
}
|
|
4527
5272
|
function apply(ctx) {
|
|
5273
|
+
fetchLastHitDockSupport().then((supported) => {
|
|
5274
|
+
ctx.effect(() => mountLastHitDock(ctx, supported), "dsh-router: last-hit dock");
|
|
5275
|
+
});
|
|
4528
5276
|
const disposeSettings = mountSettingsSection(ctx);
|
|
4529
5277
|
if (disposeSettings === void 0) {
|
|
4530
5278
|
console.warn("[dsh-router] 宿主未提供 slots,回退到侧边栏面板");
|
|
@@ -4532,6 +5280,9 @@ window.__ModuleLoader__.load({
|
|
|
4532
5280
|
return;
|
|
4533
5281
|
}
|
|
4534
5282
|
ctx.effect(() => disposeSettings, "dsh-router: settings section");
|
|
5283
|
+
const disposeComponents = mountComponentsSection(ctx);
|
|
5284
|
+
if (disposeComponents !== void 0) ctx.effect(() => disposeComponents, "dsh-router: plugin page components");
|
|
5285
|
+
registerExtTestPanel(ctx);
|
|
4535
5286
|
ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
|
|
4536
5287
|
ctx.effect(() => rewriteRouterModelHint(), "dsh-router: models provider hint copy");
|
|
4537
5288
|
}
|