dsh-router-core 0.4.0 → 0.4.2
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 +174 -38
- package/cordis.patch.yml +22 -0
- package/docs/ext.md +29 -6
- package/docs/suppliers.md +28 -5
- package/lib/client-registry.js +939 -132
- package/lib/client.js +939 -132
- 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 +838 -89
- 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 -0
- package/lib/types/ext-test/index.d.ts +21 -0
- package/lib/types/ext-test/plugin.d.ts +23 -0
- package/lib/types/host-base-url.d.ts +31 -0
- package/lib/types/llm/adapter.d.ts +40 -4
- package/lib/types/router/index.d.ts +61 -8
- package/lib/types/shared.d.ts +12 -0
- package/lib/types/supplier-config.d.ts +15 -0
- package/lib/types/suppliers/contract.d.ts +35 -2
- 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 -1
- package/package.json +39 -4
- package/src/client/ExtDetail.tsx +21 -22
- package/src/client/ExtTab.tsx +25 -42
- package/src/client/ExtTestPanel.tsx +392 -0
- package/src/client/RouterComponentsSection.tsx +241 -0
- package/src/client/RouterView.tsx +12 -5
- 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 +41 -0
- package/src/client/model-choice.test.ts +41 -0
- package/src/client/model-choice.ts +37 -0
- package/src/client/router-components.test.ts +114 -0
- package/src/client/router-components.ts +104 -0
- package/src/client/router.css +490 -9
- package/src/client/supplier-visibility.test.ts +57 -0
- package/src/client/supplier-visibility.ts +63 -0
- package/src/ext/contract.ts +9 -0
- 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-base-url.test.ts +129 -0
- package/src/host-base-url.ts +43 -0
- package/src/index.ts +46 -11
- package/src/llm/adapter.test.ts +101 -4
- package/src/llm/adapter.ts +89 -20
- package/src/router/disconnect-leak.test.ts +7 -12
- package/src/router/first-byte-budget.test.ts +160 -0
- package/src/router/index.ts +255 -35
- package/src/router/models-cache.test.ts +45 -0
- package/src/router/supplier-switch.test.ts +220 -0
- package/src/shared.ts +12 -0
- package/src/supplier-config.ts +32 -1
- package/src/suppliers/contract.ts +54 -2
- package/src/suppliers/loader.test.ts +36 -2
- 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 +102 -3
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(\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/* 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-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 {\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: 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 {\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: 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 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: 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:\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/*$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";
|
|
@@ -2448,6 +2449,40 @@ window.__ModuleLoader__.load({
|
|
|
2448
2449
|
});
|
|
2449
2450
|
}
|
|
2450
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
|
|
2451
2486
|
//#region src/client/CheckinCard.tsx
|
|
2452
2487
|
/**
|
|
2453
2488
|
* 概览的「签到」汇总卡:一键签到所有支持签到的供应商 + 显示今天点过没。
|
|
@@ -3248,6 +3283,28 @@ window.__ModuleLoader__.load({
|
|
|
3248
3283
|
});
|
|
3249
3284
|
}
|
|
3250
3285
|
//#endregion
|
|
3286
|
+
//#region src/client/ext-panels.ts
|
|
3287
|
+
/** 扩展 id → 它的详情面板。 */
|
|
3288
|
+
var panels = /* @__PURE__ */ new Map();
|
|
3289
|
+
/**
|
|
3290
|
+
* 给某个扩展登记详情面板。同 id 后登记的**顶掉**先登记的(热重载/重复 apply
|
|
3291
|
+
* 时的常见情形);返回清理函数。
|
|
3292
|
+
*
|
|
3293
|
+
* @param extId - 扩展 id(`router.ext` 表里的键)
|
|
3294
|
+
* @param panel - 详情面板组件。**不收 props**:面板的渲染点只有详情页一处
|
|
3295
|
+
* (`ExtDetail`),没有第二种调用形状。
|
|
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
|
|
3251
3308
|
//#region src/client/ExtDetail.tsx
|
|
3252
3309
|
function Icon$1({ d, size = 18 }) {
|
|
3253
3310
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -3267,97 +3324,81 @@ window.__ModuleLoader__.load({
|
|
|
3267
3324
|
back: "M19 12H5M12 19l-7-7 7-7",
|
|
3268
3325
|
bolt: "M13 2L4 14h6l-1 8 9-12h-6z"
|
|
3269
3326
|
};
|
|
3270
|
-
function ExtDetail({ item, onBack
|
|
3327
|
+
function ExtDetail({ item, onBack }) {
|
|
3328
|
+
const Custom = extPanel(item.id);
|
|
3271
3329
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3272
3330
|
className: "dshr-tabBody",
|
|
3273
|
-
children: [
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
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
|
|
3359
|
+
})
|
|
3284
3360
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3285
|
-
className: "dshr-
|
|
3286
|
-
children: [
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
children: item.
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
alt: "",
|
|
3297
|
-
onError: (e) => {
|
|
3298
|
-
e.currentTarget.style.display = "none";
|
|
3299
|
-
}
|
|
3300
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
|
|
3301
|
-
d: I$1.bolt,
|
|
3302
|
-
size: 22
|
|
3303
|
-
})
|
|
3304
|
-
}),
|
|
3305
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3306
|
-
className: "dshr-providerMeta",
|
|
3307
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
3308
|
-
className: "dshr-providerName",
|
|
3309
|
-
children: item.name
|
|
3310
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3311
|
-
className: "dshr-providerCount",
|
|
3312
|
-
children: [
|
|
3313
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3314
|
-
className: "dshr-mono",
|
|
3315
|
-
children: item.id
|
|
3316
|
-
}),
|
|
3317
|
-
item.enabled ? " · 已启用" : " · 未启用",
|
|
3318
|
-
item.ready === false ? " · 未就绪" : ""
|
|
3319
|
-
]
|
|
3320
|
-
})]
|
|
3321
|
-
}),
|
|
3322
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3323
|
-
type: "button",
|
|
3324
|
-
className: `dshr-toggle dshr-extDetailToggle ${item.enabled ? "dshr-toggle-on" : ""}`,
|
|
3325
|
-
role: "switch",
|
|
3326
|
-
"aria-checked": !!item.enabled,
|
|
3327
|
-
disabled: item.ready === false && !item.enabled,
|
|
3328
|
-
onClick: () => void onToggle(item, !item.enabled),
|
|
3329
|
-
title: item.ready === false && !item.enabled ? "当前不可用,无法开启" : item.enabled ? `关闭 ${item.name}` : `开启 ${item.name}`,
|
|
3330
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshr-toggleKnob" })
|
|
3331
|
-
})
|
|
3332
|
-
]
|
|
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
|
+
})]
|
|
3333
3372
|
})]
|
|
3334
|
-
})
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
})
|
|
3342
|
-
}),
|
|
3343
|
-
item.ready === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3344
|
-
className: "dshr-alert",
|
|
3345
|
-
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 : "当前不可用" })]
|
|
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
|
|
3346
3380
|
})
|
|
3347
|
-
|
|
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
|
+
})] })]
|
|
3348
3385
|
});
|
|
3349
3386
|
}
|
|
3350
3387
|
//#endregion
|
|
3351
3388
|
//#region src/client/ExtTab.tsx
|
|
3352
3389
|
/**
|
|
3353
|
-
* 扩展 (Ext) 页 ——
|
|
3390
|
+
* 扩展 (Ext) 页 —— 列出**已启用**的扩展插件,只读。
|
|
3391
|
+
*
|
|
3392
|
+
* 数据来自 `/router/api/ext`:核心把自己存的开关(`ext.json`)+ 扩展器报的
|
|
3393
|
+
* 运行时事实(`ready` / `detail`)合并后给面板。扩展器只实现具体能力(如 RTK)。
|
|
3354
3394
|
*
|
|
3355
|
-
*
|
|
3356
|
-
*
|
|
3357
|
-
*
|
|
3395
|
+
* 两个刻意的形状(与「开关在官方插件页」这一决定配套):
|
|
3396
|
+
* 1. **只列已启用的** —— 关闭的扩展连卡片都不出现。这里是「正在生效的东西」的
|
|
3397
|
+
* 展台,不是配置页;要去改开关,去设置 → 插件 → dsh-router-core 详情页的
|
|
3398
|
+
* 「路由组件」一节。
|
|
3399
|
+
* 2. **本页不发 PATCH** —— 启停只有一个入口,避免两处状态各说各话。
|
|
3358
3400
|
*
|
|
3359
|
-
*
|
|
3360
|
-
* 未装任何扩展 → 空状态提示。
|
|
3401
|
+
* 未装任何扩展,或装了但都没启用 → 同一条空状态提示。
|
|
3361
3402
|
*/
|
|
3362
3403
|
function Icon({ d, size = 18 }) {
|
|
3363
3404
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -3374,8 +3415,8 @@ window.__ModuleLoader__.load({
|
|
|
3374
3415
|
});
|
|
3375
3416
|
}
|
|
3376
3417
|
var I = {
|
|
3377
|
-
bolt: "M13 2L4 14h6l-1 8 9-12h-
|
|
3378
|
-
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"
|
|
3379
3420
|
};
|
|
3380
3421
|
function ExtTab() {
|
|
3381
3422
|
const [items, setItems] = (0, react.useState)([]);
|
|
@@ -3405,45 +3446,11 @@ window.__ModuleLoader__.load({
|
|
|
3405
3446
|
}
|
|
3406
3447
|
}
|
|
3407
3448
|
};
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
...i,
|
|
3411
|
-
enabled: value
|
|
3412
|
-
} : i));
|
|
3413
|
-
setError("");
|
|
3414
|
-
try {
|
|
3415
|
-
const data = await (await fetch(`${ROUTER_API_BASE}/ext`, {
|
|
3416
|
-
method: "PATCH",
|
|
3417
|
-
headers: { "Content-Type": "application/json" },
|
|
3418
|
-
body: JSON.stringify({
|
|
3419
|
-
id: item.id,
|
|
3420
|
-
enabled: value
|
|
3421
|
-
}),
|
|
3422
|
-
cache: "no-store"
|
|
3423
|
-
})).json();
|
|
3424
|
-
if (mounted.current && data.enhancers) setItems(data.enhancers);
|
|
3425
|
-
else if (mounted.current && !data.ok) {
|
|
3426
|
-
setError(data.error ?? "切换失败");
|
|
3427
|
-
setItems((prev) => prev.map((i) => i.id === item.id ? {
|
|
3428
|
-
...i,
|
|
3429
|
-
enabled: !value
|
|
3430
|
-
} : i));
|
|
3431
|
-
}
|
|
3432
|
-
} catch (err) {
|
|
3433
|
-
if (mounted.current) {
|
|
3434
|
-
setError(err.message);
|
|
3435
|
-
setItems((prev) => prev.map((i) => i.id === item.id ? {
|
|
3436
|
-
...i,
|
|
3437
|
-
enabled: !value
|
|
3438
|
-
} : i));
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
};
|
|
3442
|
-
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;
|
|
3443
3451
|
if (selected !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExtDetail, {
|
|
3444
3452
|
item: selected,
|
|
3445
|
-
onBack: () => setSelectedId(null)
|
|
3446
|
-
onToggle: toggle
|
|
3453
|
+
onBack: () => setSelectedId(null)
|
|
3447
3454
|
});
|
|
3448
3455
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3449
3456
|
className: "dshr-tabBody",
|
|
@@ -3453,7 +3460,7 @@ window.__ModuleLoader__.load({
|
|
|
3453
3460
|
}), !loaded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3454
3461
|
className: "dshr-empty",
|
|
3455
3462
|
children: "加载中…"
|
|
3456
|
-
}) :
|
|
3463
|
+
}) : shown.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3457
3464
|
className: "dshr-keyEmpty",
|
|
3458
3465
|
children: [
|
|
3459
3466
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -3465,20 +3472,20 @@ window.__ModuleLoader__.load({
|
|
|
3465
3472
|
}),
|
|
3466
3473
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3467
3474
|
className: "dshr-keyEmptyTitle",
|
|
3468
|
-
children: "
|
|
3475
|
+
children: "暂无启用的扩展"
|
|
3469
3476
|
}),
|
|
3470
3477
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
3471
3478
|
className: "dshr-keyEmptyDesc",
|
|
3472
3479
|
children: [
|
|
3473
|
-
"
|
|
3474
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
3475
|
-
"
|
|
3480
|
+
"开关在 ",
|
|
3481
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "设置 → 插件 → dsh-router-core" }),
|
|
3482
|
+
" 的「路由组件」一节; 启用后这里会列出它"
|
|
3476
3483
|
]
|
|
3477
3484
|
})
|
|
3478
3485
|
]
|
|
3479
3486
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3480
3487
|
className: "dshr-supplierGrid",
|
|
3481
|
-
children:
|
|
3488
|
+
children: shown.map((item) => {
|
|
3482
3489
|
const notReady = item.ready === false;
|
|
3483
3490
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3484
3491
|
className: "dshr-supplierCard",
|
|
@@ -3511,8 +3518,8 @@ window.__ModuleLoader__.load({
|
|
|
3511
3518
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3512
3519
|
className: "dshr-supplierMeta",
|
|
3513
3520
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3514
|
-
className: "dshr-muted",
|
|
3515
|
-
children: item.
|
|
3521
|
+
className: "dshr-mono dshr-muted",
|
|
3522
|
+
children: item.id
|
|
3516
3523
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3517
3524
|
className: "dshr-chevron",
|
|
3518
3525
|
children: "›"
|
|
@@ -3586,6 +3593,7 @@ window.__ModuleLoader__.load({
|
|
|
3586
3593
|
};
|
|
3587
3594
|
}, []);
|
|
3588
3595
|
const suppliers = snapshot.health?.suppliers ?? [];
|
|
3596
|
+
const panel = supplierVisibility(suppliers);
|
|
3589
3597
|
const accounts = snapshot.status?.accounts ?? [];
|
|
3590
3598
|
const combos = snapshot.combos?.combos ?? [];
|
|
3591
3599
|
const suppliersError = snapshot.status?.ok === false ? snapshot.status.error : void 0;
|
|
@@ -3717,12 +3725,15 @@ window.__ModuleLoader__.load({
|
|
|
3717
3725
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3718
3726
|
className: "dshr-tabBody",
|
|
3719
3727
|
children: [
|
|
3720
|
-
|
|
3721
|
-
className: "dshr-empty",
|
|
3722
|
-
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
|
+
})]
|
|
3723
3734
|
}),
|
|
3724
|
-
renderSupplierGroup("内置",
|
|
3725
|
-
renderSupplierGroup("插件",
|
|
3735
|
+
renderSupplierGroup("内置", panel.builtin),
|
|
3736
|
+
renderSupplierGroup("插件", panel.external),
|
|
3726
3737
|
suppliersError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3727
3738
|
className: "dshr-empty",
|
|
3728
3739
|
children: suppliersError
|
|
@@ -4083,6 +4094,769 @@ window.__ModuleLoader__.load({
|
|
|
4083
4094
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, {});
|
|
4084
4095
|
}
|
|
4085
4096
|
//#endregion
|
|
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
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* 把两个端点的答复分成两组。
|
|
4115
|
+
*
|
|
4116
|
+
* 供应商这边只收 `source === 'external'`(独立安装的供应商插件)。`builtin`
|
|
4117
|
+
* 的那三个已经作为**原生行**在宿主的「包含的组件」里显示了(core 自己 patch
|
|
4118
|
+
* insert 的子路径模块,开关是宿主管的那个行开关),`user` 的是用户目录里投放的
|
|
4119
|
+
* js —— 两者都不该在这里出现第二次。`source` 缺失按「不是 external」处理:
|
|
4120
|
+
* 不显示总比把内置的错标成独立插件强。
|
|
4121
|
+
*
|
|
4122
|
+
* 两组都在这一页开关,指向核心的同两个端点(`PATCH /suppliers/:id/enabled` 与
|
|
4123
|
+
* `PATCH /ext`),所以「在哪开关」是一致的:**设置 → 插件 → dsh-router-core**。
|
|
4124
|
+
* 供应商的开关关掉是真的不参与路由(见 Router 的活跃集合),不是只隐藏卡片。
|
|
4125
|
+
*/
|
|
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
|
|
4086
4860
|
//#region src/client/settings-nav-icon.ts
|
|
4087
4861
|
/**
|
|
4088
4862
|
* 给设置导航里「路由」这一行换掉宿主的默认齿轮。
|
|
@@ -4465,6 +5239,36 @@ window.__ModuleLoader__.load({
|
|
|
4465
5239
|
label: SECTION_LABEL
|
|
4466
5240
|
}, RouterSettingsSection));
|
|
4467
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
|
+
}
|
|
4468
5272
|
function apply(ctx) {
|
|
4469
5273
|
fetchLastHitDockSupport().then((supported) => {
|
|
4470
5274
|
ctx.effect(() => mountLastHitDock(ctx, supported), "dsh-router: last-hit dock");
|
|
@@ -4476,6 +5280,9 @@ window.__ModuleLoader__.load({
|
|
|
4476
5280
|
return;
|
|
4477
5281
|
}
|
|
4478
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);
|
|
4479
5286
|
ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
|
|
4480
5287
|
ctx.effect(() => rewriteRouterModelHint(), "dsh-router: models provider hint copy");
|
|
4481
5288
|
}
|