dsh-router-core 0.4.6 → 0.4.8

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.
Files changed (94) hide show
  1. package/README.md +156 -76
  2. package/cordis.patch.yml +2 -7
  3. package/docs/ext.md +4 -18
  4. package/docs/protocols.md +198 -0
  5. package/lib/client-registry.js +193 -668
  6. package/lib/client.js +193 -668
  7. package/lib/ext-prompt/index.js +202 -32
  8. package/lib/index.js +2805 -547
  9. package/lib/types/ext/contract.d.ts +20 -1
  10. package/lib/types/ext-prompt/content.d.ts +18 -1
  11. package/lib/types/ext-prompt/index.d.ts +7 -9
  12. package/lib/types/ext-prompt/mount.d.ts +14 -3
  13. package/lib/types/ext-prompt/plugin.d.ts +13 -5
  14. package/lib/types/ext-prompt/render.d.ts +14 -3
  15. package/lib/types/ext-prompt/sections.d.ts +95 -0
  16. package/lib/types/protocol/anthropic.d.ts +35 -0
  17. package/lib/types/protocol/chat.d.ts +25 -0
  18. package/lib/types/protocol/entry.d.ts +137 -0
  19. package/lib/types/protocol/gemini.d.ts +57 -0
  20. package/lib/types/protocol/index.d.ts +13 -0
  21. package/lib/types/protocol/responses.d.ts +41 -0
  22. package/lib/types/protocol/sse.d.ts +41 -0
  23. package/lib/types/protocol/stream.d.ts +92 -0
  24. package/lib/types/protocol/types.d.ts +224 -0
  25. package/lib/types/router/index.d.ts +42 -1
  26. package/lib/types/router/types.d.ts +19 -0
  27. package/lib/types/shared.d.ts +11 -0
  28. package/lib/types/suppliers/contract.d.ts +8 -0
  29. package/lib/types/suppliers/registry.d.ts +0 -1
  30. package/package.json +6 -15
  31. package/src/client/ExtDetail.tsx +26 -18
  32. package/src/client/group-controls.test.ts +53 -0
  33. package/src/client/group-controls.ts +43 -0
  34. package/src/client/index.tsx +0 -17
  35. package/src/client/router-components.test.ts +1 -1
  36. package/src/client/router.css +23 -355
  37. package/src/ext/contract.ts +20 -1
  38. package/src/ext-prompt/assembly.test.ts +63 -0
  39. package/src/ext-prompt/content.ts +25 -22
  40. package/src/ext-prompt/ext-prompt.test.ts +41 -31
  41. package/src/ext-prompt/index.ts +57 -11
  42. package/src/ext-prompt/mount.test.ts +18 -1
  43. package/src/ext-prompt/mount.ts +24 -9
  44. package/src/ext-prompt/plugin.ts +67 -6
  45. package/src/ext-prompt/render.test.ts +1 -1
  46. package/src/ext-prompt/render.ts +17 -6
  47. package/src/ext-prompt/section-controls.test.ts +148 -0
  48. package/src/ext-prompt/section-filter.test.ts +140 -0
  49. package/src/ext-prompt/sections.test.ts +123 -0
  50. package/src/ext-prompt/sections.ts +193 -0
  51. package/src/ext-route.test.ts +16 -0
  52. package/src/index.ts +109 -1
  53. package/src/protocol/anthropic.ts +662 -0
  54. package/src/protocol/chat.ts +272 -0
  55. package/src/protocol/entry.ts +440 -0
  56. package/src/protocol/gemini.ts +548 -0
  57. package/src/protocol/index.ts +31 -0
  58. package/src/protocol/protocol.test.ts +728 -0
  59. package/src/protocol/responses.ts +551 -0
  60. package/src/protocol/sse.ts +94 -0
  61. package/src/protocol/stream.ts +195 -0
  62. package/src/protocol/types.ts +249 -0
  63. package/src/protocol/wire-e2e.test.ts +208 -0
  64. package/src/protocol/wiring.test.ts +225 -0
  65. package/src/router/index.ts +144 -26
  66. package/src/router/models-cache.test.ts +5 -6
  67. package/src/router/supplier-switch.test.ts +1 -1
  68. package/src/router/types.ts +19 -0
  69. package/src/shared.ts +11 -0
  70. package/src/suppliers/contract.ts +8 -0
  71. package/src/suppliers/registry.ts +0 -71
  72. package/lib/contract-D4BWZ7Xe.js +0 -13
  73. package/lib/ext-test/index.js +0 -36
  74. package/lib/ext-test/locale/en.json +0 -6
  75. package/lib/ext-test/locale/zh.json +0 -6
  76. package/lib/ext-test/package.json +0 -5
  77. package/lib/types/ext-test/index.d.ts +0 -21
  78. package/lib/types/ext-test/plugin.d.ts +0 -23
  79. package/lib/types/suppliers/probe.d.ts +0 -160
  80. package/src/client/ExtTestPanel.tsx +0 -392
  81. package/src/client/comp-button-scope.test.ts +0 -58
  82. package/src/client/comp-panel-layout.test.ts +0 -58
  83. package/src/client/ext-panels.test.ts +0 -44
  84. package/src/client/ext-panels.ts +0 -49
  85. package/src/client/model-choice.test.ts +0 -41
  86. package/src/client/model-choice.ts +0 -37
  87. package/src/ext-test/ext-test.test.ts +0 -60
  88. package/src/ext-test/index.ts +0 -35
  89. package/src/ext-test/locale/en.json +0 -6
  90. package/src/ext-test/locale/zh.json +0 -6
  91. package/src/ext-test/plugin.ts +0 -34
  92. package/src/ext-test/row.json +0 -5
  93. package/src/suppliers/probe.test.ts +0 -266
  94. package/src/suppliers/probe.ts +0 -475
@@ -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/* 两行空状态(标题 + 说明)。`.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/* 扩展页:未就绪问题描述(红字) */\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-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-accent);\n background: color-mix(in srgb, var(--rs-accent) 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 —— 写死的值,与容器无关。改由 flex 链给。 */\n flex: 1;\n min-height: 0;\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-accent);\n background: color-mix(in srgb, var(--rs-accent) 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-accent);\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-accent) 50%, transparent);\n background: color-mix(in srgb, var(--rs-accent) 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 —— 写死的值,与容器无关。改由 flex 链给。 */\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 /* stretch 而非 start:两列都吃满行高,右列那个滚动列表才有高可用。\n 用 start 的话行高由最高的一列决定,短的那列留白 —— 同一个病。 */\n align-items: stretch;\n /* 行高必须是 minmax(0,1fr) 而不是隐式 auto:auto 行高由内容决定,\n 视口一小内容压不下去,滚动就又跑到外层 body 去了。 */\n grid-template-rows: minmax(0, 1fr);\n flex: 1;\n min-height: 0;\n}\n\n.dshr-comboFormLeft {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n}\n\n/* 「模型」那一坨(标题 + 列表)吃掉剩余高度,列表才有空间滚动 */\n.dshr-comboFormLeft > div:last-child {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.dshr-comboFormRight {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n min-height: 0;\n}\n\n.dshr-comboFormRight .dshr-comboPickList {\n flex: 1;\n min-height: 0;\n}\n\n/* 组合编辑弹窗:加宽加高(body 更高,内部列表同步放大) */\n.dshr-modal-wide {\n /* 高度上限必须**扣掉遮罩的 24px×2 padding**:原来的 90vh 在视口\n 低于 480px 时,90vh + 48px 必然超过视口,弹窗顶出遮罩。\n (基础 .dshr-modal 用 80vh 所以一直没这问题,只有 -wide 变体有。) */\n --dshr-modalMaxH: calc(100vh - 48px);\n width: min(760px, 100%);\n max-height: min(var(--dshr-modalMaxH), 820px);\n}\n\n/* ⚠️ 这条 min-height **只预留**了高度,不往内传递:body 是块容器,里面的内容\n 各自按自己的 max-height 摆(pick list 300 / 左列表 380 / 右列 460),\n 560 - 内容高 就是底部那条空白 —— 选 2 个模型时最明显,选满时反而看不见,\n 所以它一直躲过了「按内容相关问题」的眼。\n 改成 flex 列,并让 .dshr-modalForm 吃掉剩余高度,预留才真正被内容用掉。\n\n ⚠️ 地板也不能写死:视口一小,560px 会把弹窗顶出遮罩,逼得**外层 body**\n 出来滚动(多一道滚动条,还裁掉一截)。上限跟着 --dshr-modalMaxH 走\n (60 = 头部 48 + 边框 2 + 余量),空间不够时该滚的是内层\n .dshr-comboPickList,不是整个 body。 */\n.dshr-modal-wide .dshr-modalBody {\n min-height: min(560px, calc(var(--dshr-modalMaxH) - 60px));\n display: flex;\n flex-direction: column;\n}\n\n.dshr-modal-wide .dshr-modalForm {\n flex: 1;\n min-height: 0;\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 /* ⚠️ 左右 14px(原先 2px):行贴着卡片边缘,文字几乎顶到边框,读起来\"挤\"。\n 2026-09-29 用户提出。与 `-compHead` 的内缩对齐。 */\n padding: 12px 14px;\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-toast` 的**窄范围覆盖**。\n ⚠️ 不去改 `.dshr-toast` 本身 —— 它被 `SupplierDetail` 也在用(那边全是\n 「连接池顺序已保存」这种 9 字短句,所以从没暴露过宽度问题)。\n 本处的错误文案更长(「只能删除自定义准则(内置条目请用开关关掉)」19 字),\n 而 `.dshr-toast` 是 `white-space: nowrap` **且没有 `max-width`** ⇒ 会横向溢出屏幕。\n ⇒ 只加**换行 + 上限 + 居中**,定位用绝对定位挂在提示自身,**不影响既有调用方**。 */\n.dshr-toastWrap {\n position: fixed;\n z-index: 2000;\n left: 50%;\n bottom: 28px;\n transform: translateX(-50%);\n max-width: min(420px, calc(100vw - 32px));\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: pre-wrap; /* 允许换行,不再 nowrap 溢出 */\n text-align: center;\n animation: dshrToastIn 0.18s ease;\n}\n\n/* 标题 + 「修改」按钮的**同一行**(`ExtDetail` 的子开关区)。\n ⚠️ 单独一个类而不改 `.dshr-compRowName`:那个类 `RouterComponentsSection` /\n `ExtTestPanel` 也在用,给它加 `display:flex` 会把**那些页面的标题**也变成\n flex 盒子(它们标题后面没有按钮,平白多一层)。\n 没有这层时按钮会掉到**下一行**:`.dshr-compRowMain` 是 column,而\n `.dshr-compRowName` 是行内元素、标题一长就折行(2026-09-29 用户报)。 */\n/* 子开关区:`.dshr-compGroup` 的 `gap: 12px` 覆盖为 0(2026-09-30 用户要求)。\n ⚠️ **不能直接改 `.dshr-compGroup`**:它还被 `RouterComponentsSection` 用着,\n 那里的 12px 隔的是「组标题 / hint / 列表」三块,删了会挤成一坨。\n ⇒ 本处单独覆盖,且**只覆盖 gap**(其余属性仍复用同一个类)。\n 本页不需要这个间距:页头自带 `border-bottom` 分割线,再加 12px 留白\n 会让分割线\"悬\"在一段空白上方(用户说的「分割线有高度」)。 */\n.dshr-compGroupTight {\n gap: 0;\n}\n\n/* 行内的「修改」按钮:**hover 该行才显形**(2026-09-29)。\n 14 行各挂一个常驻图标 = 一排按钮压过内容本身。\n ⚠️ **必须同时有 `:focus-within`**:纯 `:hover` 会让**键盘用户永远够不到**\n (Tab 到它时它是 `opacity:0`,看得见焦点环但看不见按钮)。\n ⚠️ 用 `opacity` 而不是 `visibility`/`display` —— 后两者会让它**不可聚焦**\n (`visibility:hidden` 的元素不进 Tab 序),`opacity:0` 仍在。 */\n.dshr-compRowAct {\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n.dshr-compRow:hover .dshr-compRowAct,\n.dshr-compRow:focus-within .dshr-compRowAct {\n opacity: 1;\n}\n\n/* 触屏没有 hover ⇒ 按钮永远点不到。窄屏(触屏为主)直接常显。 */\n@media (hover: none) {\n .dshr-compRowAct {\n opacity: 1;\n }\n}\n\n.dshr-compRowTitleLine {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\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 /* 保留换行:子开关的**原文**是这个类的另一个用法(2026-09-29 合并进来),\n 它是多行段落。状态文案都是单行,所以这条对它们没有副作用。\n ⇒ 原来的 `.dshr-compRowBody` 整块删掉:14 条规则的正文不需要左边框 +\n 缩进那种「引用块」的额外装饰(用户要求),层级只靠颜色就够。 */\n white-space: pre-wrap;\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); }})();
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/* 扩展页:未就绪问题描述(红字) */\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-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-accent);\n background: color-mix(in srgb, var(--rs-accent) 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 —— 写死的值,与容器无关。改由 flex 链给。 */\n flex: 1;\n min-height: 0;\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-accent);\n background: color-mix(in srgb, var(--rs-accent) 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-accent);\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-accent) 50%, transparent);\n background: color-mix(in srgb, var(--rs-accent) 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 —— 写死的值,与容器无关。改由 flex 链给。 */\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 /* stretch 而非 start:两列都吃满行高,右列那个滚动列表才有高可用。\n 用 start 的话行高由最高的一列决定,短的那列留白 —— 同一个病。 */\n align-items: stretch;\n /* 行高必须是 minmax(0,1fr) 而不是隐式 auto:auto 行高由内容决定,\n 视口一小内容压不下去,滚动就又跑到外层 body 去了。 */\n grid-template-rows: minmax(0, 1fr);\n flex: 1;\n min-height: 0;\n}\n\n.dshr-comboFormLeft {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n}\n\n/* 「模型」那一坨(标题 + 列表)吃掉剩余高度,列表才有空间滚动 */\n.dshr-comboFormLeft > div:last-child {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n}\n\n.dshr-comboFormRight {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n min-height: 0;\n}\n\n.dshr-comboFormRight .dshr-comboPickList {\n flex: 1;\n min-height: 0;\n}\n\n/* 组合编辑弹窗:加宽加高(body 更高,内部列表同步放大) */\n.dshr-modal-wide {\n /* 高度上限必须**扣掉遮罩的 24px×2 padding**:原来的 90vh 在视口\n 低于 480px 时,90vh + 48px 必然超过视口,弹窗顶出遮罩。\n (基础 .dshr-modal 用 80vh 所以一直没这问题,只有 -wide 变体有。) */\n --dshr-modalMaxH: calc(100vh - 48px);\n width: min(760px, 100%);\n max-height: min(var(--dshr-modalMaxH), 820px);\n}\n\n/* ⚠️ 这条 min-height **只预留**了高度,不往内传递:body 是块容器,里面的内容\n 各自按自己的 max-height 摆(pick list 300 / 左列表 380 / 右列 460),\n 560 - 内容高 就是底部那条空白 —— 选 2 个模型时最明显,选满时反而看不见,\n 所以它一直躲过了「按内容相关问题」的眼。\n 改成 flex 列,并让 .dshr-modalForm 吃掉剩余高度,预留才真正被内容用掉。\n\n ⚠️ 地板也不能写死:视口一小,560px 会把弹窗顶出遮罩,逼得**外层 body**\n 出来滚动(多一道滚动条,还裁掉一截)。上限跟着 --dshr-modalMaxH 走\n (60 = 头部 48 + 边框 2 + 余量),空间不够时该滚的是内层\n .dshr-comboPickList,不是整个 body。 */\n.dshr-modal-wide .dshr-modalBody {\n min-height: min(560px, calc(var(--dshr-modalMaxH) - 60px));\n display: flex;\n flex-direction: column;\n}\n\n.dshr-modal-wide .dshr-modalForm {\n flex: 1;\n min-height: 0;\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/* 分组标题行 —— `ExtControl.group` 的渲染落点,插在 `.dshr-compRows` 里\n 的非交互 <li>(无拖把/开关,不可点)。与 `compTitle` 的区别:那是详情页\n 主标题(页头),这是**组**名(次要色 + 小字号)。左右 14px 对齐\n `.dshr-compRow` 的行内缩,组名与行文字左缘一致。 */\n.dshr-extGroupTitle {\n margin: 14px 0 2px;\n padding: 0 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n list-style: none;\n}\n\n/* 全部分组的扩展:首个渲染节点就是组标题,去掉上边距避免顶双空。 */\n.dshr-extGroupTitle:first-child {\n margin-top: 0;\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 /* ⚠️ 左右 14px(原先 2px):行贴着卡片边缘,文字几乎顶到边框,读起来\"挤\"。\n 2026-09-29 用户提出。与 `-compHead` 的内缩对齐。 */\n padding: 12px 14px;\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-toast` 的**窄范围覆盖**。\n ⚠️ 不去改 `.dshr-toast` 本身 —— 它被 `SupplierDetail` 也在用(那边全是\n 「连接池顺序已保存」这种 9 字短句,所以从没暴露过宽度问题)。\n 本处的错误文案更长(「只能删除自定义准则(内置条目请用开关关掉)」19 字),\n 而 `.dshr-toast` 是 `white-space: nowrap` **且没有 `max-width`** ⇒ 会横向溢出屏幕。\n ⇒ 只加**换行 + 上限 + 居中**,定位用绝对定位挂在提示自身,**不影响既有调用方**。 */\n.dshr-toastWrap {\n position: fixed;\n z-index: 2000;\n left: 50%;\n bottom: 28px;\n transform: translateX(-50%);\n max-width: min(420px, calc(100vw - 32px));\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: pre-wrap; /* 允许换行,不再 nowrap 溢出 */\n text-align: center;\n animation: dshrToastIn 0.18s ease;\n}\n\n/* 标题 + 「修改」按钮的**同一行**(`ExtDetail` 的子开关区)。\n ⚠️ 单独一个类而不改 `.dshr-compRowName`:那个类 `RouterComponentsSection`\n 也在用,给它加 `display:flex` 会把**那一页的标题**也变成\n flex 盒子(那边标题后面没有按钮,平白多一层)。\n 没有这层时按钮会掉到**下一行**:`.dshr-compRowMain` 是 column,而\n `.dshr-compRowName` 是行内元素、标题一长就折行(2026-09-29 用户报)。 */\n/* 子开关区:`.dshr-compGroup` 的 `gap: 12px` 覆盖为 0(2026-09-30 用户要求)。\n ⚠️ **不能直接改 `.dshr-compGroup`**:它还被 `RouterComponentsSection` 用着,\n 那里的 12px 隔的是「组标题 / hint / 列表」三块,删了会挤成一坨。\n ⇒ 本处单独覆盖,且**只覆盖 gap**(其余属性仍复用同一个类)。\n 本页不需要这个间距:页头自带 `border-bottom` 分割线,再加 12px 留白\n 会让分割线\"悬\"在一段空白上方(用户说的「分割线有高度」)。 */\n.dshr-compGroupTight {\n gap: 0;\n}\n\n/* 行内的「修改」按钮:**hover 该行才显形**(2026-09-29)。\n 14 行各挂一个常驻图标 = 一排按钮压过内容本身。\n ⚠️ **必须同时有 `:focus-within`**:纯 `:hover` 会让**键盘用户永远够不到**\n (Tab 到它时它是 `opacity:0`,看得见焦点环但看不见按钮)。\n ⚠️ 用 `opacity` 而不是 `visibility`/`display` —— 后两者会让它**不可聚焦**\n (`visibility:hidden` 的元素不进 Tab 序),`opacity:0` 仍在。 */\n.dshr-compRowAct {\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n.dshr-compRow:hover .dshr-compRowAct,\n.dshr-compRow:focus-within .dshr-compRowAct {\n opacity: 1;\n}\n\n/* 触屏没有 hover ⇒ 按钮永远点不到。窄屏(触屏为主)直接常显。 */\n@media (hover: none) {\n .dshr-compRowAct {\n opacity: 1;\n }\n}\n\n.dshr-compRowTitleLine {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\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 /* 保留换行:子开关的**原文**是这个类的另一个用法(2026-09-29 合并进来),\n 它是多行段落。状态文案都是单行,所以这条对它们没有副作用。\n ⇒ 原来的 `.dshr-compRowBody` 整块删掉:各条规则的正文不需要左边框 +\n 缩进那种「引用块」的额外装饰(用户要求),层级只靠颜色就够。 */\n white-space: pre-wrap;\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) => {
@@ -3291,26 +3291,31 @@ window.__ModuleLoader__.load({
3291
3291
  });
3292
3292
  }
3293
3293
  //#endregion
3294
- //#region src/client/ext-panels.ts
3295
- /** 扩展 id → 它的详情面板。 */
3296
- var panels = /* @__PURE__ */ new Map();
3297
- /**
3298
- * 给某个扩展登记详情面板。同 id 后登记的**顶掉**先登记的(热重载/重复 apply
3299
- * 时的常见情形);返回清理函数。
3300
- *
3301
- * @param extId - 扩展 id(`router.ext` 表里的键)
3302
- * @param panel - 详情面板组件。**不收 props**:面板的渲染点只有详情页一处
3303
- * (`ExtDetail`),没有第二种调用形状。
3304
- */
3305
- function registerExtPanel(extId, panel) {
3306
- panels.set(extId, panel);
3307
- return () => {
3308
- if (panels.get(extId) === panel) panels.delete(extId);
3309
- };
3310
- }
3311
- /** 取某个扩展的详情面板;没登记返回 undefined(调用方据此走通用只读页)。 */
3312
- function extPanel(extId) {
3313
- return panels.get(extId);
3294
+ //#region src/client/group-controls.ts
3295
+ /** 按 `group` 分组;空输入返回空数组(不产出空的无组节)。 */
3296
+ function groupControlRows(rows) {
3297
+ const plain = [];
3298
+ const groups = [];
3299
+ const byLabel = /* @__PURE__ */ new Map();
3300
+ for (const c of rows) {
3301
+ if (c.group === void 0) {
3302
+ plain.push(c);
3303
+ continue;
3304
+ }
3305
+ let items = byLabel.get(c.group);
3306
+ if (!items) {
3307
+ items = [];
3308
+ byLabel.set(c.group, items);
3309
+ groups.push({
3310
+ label: c.group,
3311
+ items
3312
+ });
3313
+ }
3314
+ items.push(c);
3315
+ }
3316
+ const out = [];
3317
+ if (plain.length > 0) out.push({ items: plain });
3318
+ return [...out, ...groups];
3314
3319
  }
3315
3320
  //#endregion
3316
3321
  //#region src/client/ExtDetail.tsx
@@ -3329,8 +3334,7 @@ window.__ModuleLoader__.load({
3329
3334
  *
3330
3335
  * 列表页只列**已启用**的扩展(关闭的连卡片都不出现),所以这里也不必再显示
3331
3336
  * 「已启用/未启用」——那行字只能是常量。布局同供应商详情:返回链接 + 图标 +
3332
- * 名称/副行,下面跟扩展说明卡与未就绪横幅。有 `api()` 支持的自定义扩展可在未来
3333
- * 按需加独立面板,此处不内置任何具体扩展的专属视图。
3337
+ * 名称/副行,下面跟扩展说明卡与未就绪横幅。
3334
3338
  */
3335
3339
  /**
3336
3340
  * 服务端的 `error` 是**给调试看的**(`'controlId must be a non-empty string'`、
@@ -3739,109 +3743,122 @@ window.__ModuleLoader__.load({
3739
3743
  }),
3740
3744
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
3741
3745
  className: "dshr-compRows",
3742
- children: controls.map((c, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
3743
- className: `dshr-compRow${dragIndex === index ? " dshr-linkRowDragging" : ""}`,
3744
- draggable: true,
3745
- onDragStart: (e) => {
3746
- setDragIndex(index);
3747
- e.dataTransfer.effectAllowed = "move";
3748
- },
3749
- onDragOver: (e) => {
3750
- e.preventDefault();
3751
- e.dataTransfer.dropEffect = "move";
3752
- },
3753
- onDrop: (e) => {
3754
- e.preventDefault();
3755
- if (dragIndex === null || dragIndex === index) {
3746
+ children: groupControlRows(controls).map((g) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [g.label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
3747
+ className: "dshr-extGroupTitle",
3748
+ children: g.label
3749
+ }), g.items.map((c) => {
3750
+ const index = controls.indexOf(c);
3751
+ const sortable = c.sortable !== false;
3752
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
3753
+ className: `dshr-compRow${dragIndex === index ? " dshr-linkRowDragging" : ""}`,
3754
+ draggable: sortable,
3755
+ onDragStart: (e) => {
3756
+ if (!sortable) return;
3757
+ setDragIndex(index);
3758
+ e.dataTransfer.effectAllowed = "move";
3759
+ },
3760
+ onDragOver: (e) => {
3761
+ if (!sortable) return;
3762
+ e.preventDefault();
3763
+ e.dataTransfer.dropEffect = "move";
3764
+ },
3765
+ onDrop: (e) => {
3766
+ if (!sortable) {
3767
+ setDragIndex(null);
3768
+ return;
3769
+ }
3770
+ e.preventDefault();
3771
+ if (dragIndex === null || dragIndex === index) {
3772
+ setDragIndex(null);
3773
+ return;
3774
+ }
3775
+ const next = [...controls];
3776
+ const [moved] = next.splice(dragIndex, 1);
3777
+ if (moved !== void 0) next.splice(index, 0, moved);
3756
3778
  setDragIndex(null);
3757
- return;
3758
- }
3759
- const next = [...controls];
3760
- const [moved] = next.splice(dragIndex, 1);
3761
- if (moved !== void 0) next.splice(index, 0, moved);
3762
- setDragIndex(null);
3763
- persist(next.map((x) => x.id));
3764
- },
3765
- onDragEnd: () => {
3766
- setDragIndex(null);
3767
- },
3768
- children: [
3769
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3770
- className: "dshr-linkGrip",
3771
- title: "拖动排序",
3772
- "aria-hidden": "true",
3773
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3774
- viewBox: "0 0 24 24",
3775
- width: "14",
3776
- height: "14",
3777
- fill: "currentColor",
3778
- children: [
3779
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3780
- cx: "9",
3781
- cy: "6",
3782
- r: "1.5"
3783
- }),
3784
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3785
- cx: "15",
3786
- cy: "6",
3787
- r: "1.5"
3788
- }),
3789
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3790
- cx: "9",
3791
- cy: "12",
3792
- r: "1.5"
3793
- }),
3794
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3795
- cx: "15",
3796
- cy: "12",
3797
- r: "1.5"
3798
- }),
3799
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3800
- cx: "9",
3801
- cy: "18",
3802
- r: "1.5"
3803
- }),
3804
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3805
- cx: "15",
3806
- cy: "18",
3807
- r: "1.5"
3808
- })
3809
- ]
3810
- })
3811
- }),
3812
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3813
- className: "dshr-compRowMain",
3814
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3815
- className: "dshr-compRowTitleLine",
3816
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3817
- className: "dshr-compRowName",
3818
- children: titles[c.id] ?? c.title
3819
- }), c.editable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3820
- type: "button",
3821
- className: "dshr-iconBtn dshr-iconBtn-sm dshr-compRowAct",
3822
- "aria-label": `修改「${titles[c.id] ?? c.title}」`,
3823
- title: `修改「${titles[c.id] ?? c.title}」的标题与内容`,
3824
- onClick: () => {
3825
- setEditing(c.id);
3826
- },
3827
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowIcon$1, { d: I_EDIT })
3779
+ persist(next.map((x) => x.id));
3780
+ },
3781
+ onDragEnd: () => {
3782
+ setDragIndex(null);
3783
+ },
3784
+ children: [
3785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3786
+ className: "dshr-linkGrip",
3787
+ title: "拖动排序",
3788
+ "aria-hidden": "true",
3789
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3790
+ viewBox: "0 0 24 24",
3791
+ width: "14",
3792
+ height: "14",
3793
+ fill: "currentColor",
3794
+ children: [
3795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3796
+ cx: "9",
3797
+ cy: "6",
3798
+ r: "1.5"
3799
+ }),
3800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3801
+ cx: "15",
3802
+ cy: "6",
3803
+ r: "1.5"
3804
+ }),
3805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3806
+ cx: "9",
3807
+ cy: "12",
3808
+ r: "1.5"
3809
+ }),
3810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3811
+ cx: "15",
3812
+ cy: "12",
3813
+ r: "1.5"
3814
+ }),
3815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3816
+ cx: "9",
3817
+ cy: "18",
3818
+ r: "1.5"
3819
+ }),
3820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3821
+ cx: "15",
3822
+ cy: "18",
3823
+ r: "1.5"
3824
+ })
3825
+ ]
3826
+ })
3827
+ }),
3828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3829
+ className: "dshr-compRowMain",
3830
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3831
+ className: "dshr-compRowTitleLine",
3832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3833
+ className: "dshr-compRowName",
3834
+ children: titles[c.id] ?? c.title
3835
+ }), c.editable === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3836
+ type: "button",
3837
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-compRowAct",
3838
+ "aria-label": `修改「${titles[c.id] ?? c.title}」`,
3839
+ title: `修改「${titles[c.id] ?? c.title}」的标题与内容`,
3840
+ onClick: () => {
3841
+ setEditing(c.id);
3842
+ },
3843
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowIcon$1, { d: I_EDIT })
3844
+ })]
3845
+ }), typeof c.body === "string" && c.body !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3846
+ className: "dshr-compRowState",
3847
+ children: c.body
3828
3848
  })]
3829
- }), typeof c.body === "string" && c.body !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3830
- className: "dshr-compRowState",
3831
- children: c.body
3832
- })]
3833
- }),
3834
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
3835
- checked: state[c.id] === true,
3836
- disabled: disabled || item.enabled !== true,
3837
- label: `${c.title} 开关`,
3838
- title: item.enabled !== true ? "扩展总开关已关闭,子开关暂不生效" : state[c.id] === true ? `关闭 ${c.title}` : `开启 ${c.title}`,
3839
- onChange: (next) => {
3840
- toggle(c.id, next);
3841
- }
3842
- })
3843
- ]
3844
- }, c.id))
3849
+ }),
3850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
3851
+ checked: state[c.id] === true,
3852
+ disabled: disabled || item.enabled !== true,
3853
+ label: `${c.title} 开关`,
3854
+ title: item.enabled !== true ? "扩展总开关已关闭,子开关暂不生效" : state[c.id] === true ? `关闭 ${c.title}` : `开启 ${c.title}`,
3855
+ onChange: (next) => {
3856
+ toggle(c.id, next);
3857
+ }
3858
+ })
3859
+ ]
3860
+ }, c.id);
3861
+ })] }, g.label ?? "__plain__"))
3845
3862
  }),
3846
3863
  withBody.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3847
3864
  className: "dshr-compHint",
@@ -3944,56 +3961,59 @@ window.__ModuleLoader__.load({
3944
3961
  });
3945
3962
  }
3946
3963
  function ExtDetail({ item, onBack }) {
3947
- const Custom = extPanel(item.id);
3948
3964
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3949
3965
  className: "dshr-tabBody",
3950
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3951
- className: "dshr-providerHead",
3952
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3953
- type: "button",
3954
- className: "dshr-backLink",
3955
- onClick: onBack,
3956
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
3957
- d: I$1.back,
3958
- size: 16
3959
- }), "返回"]
3960
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3961
- className: "dshr-providerTitleRow",
3962
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3963
- className: "dshr-providerIcon",
3964
- style: {
3965
- color: "var(--rs-faint)",
3966
- background: "var(--rs-layer-2)"
3967
- },
3968
- children: item.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3969
- className: "dshr-providerImg",
3970
- src: item.icon,
3971
- alt: "",
3972
- onError: (e) => {
3973
- e.currentTarget.style.display = "none";
3974
- }
3975
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
3976
- d: I$1.bolt,
3977
- size: 22
3978
- })
3966
+ children: [
3967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3968
+ className: "dshr-providerHead",
3969
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3970
+ type: "button",
3971
+ className: "dshr-backLink",
3972
+ onClick: onBack,
3973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
3974
+ d: I$1.back,
3975
+ size: 16
3976
+ }), "返回"]
3979
3977
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3980
- className: "dshr-providerMeta",
3981
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
3982
- className: "dshr-providerName",
3983
- children: item.name
3984
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3985
- className: "dshr-providerCount",
3986
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3987
- className: "dshr-mono",
3988
- children: item.id
3989
- }), item.ready === false ? " · 未就绪" : ""]
3978
+ className: "dshr-providerTitleRow",
3979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3980
+ className: "dshr-providerIcon",
3981
+ style: {
3982
+ color: "var(--rs-faint)",
3983
+ background: "var(--rs-layer-2)"
3984
+ },
3985
+ children: item.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3986
+ className: "dshr-providerImg",
3987
+ src: item.icon,
3988
+ alt: "",
3989
+ onError: (e) => {
3990
+ e.currentTarget.style.display = "none";
3991
+ }
3992
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
3993
+ d: I$1.bolt,
3994
+ size: 22
3995
+ })
3996
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3997
+ className: "dshr-providerMeta",
3998
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
3999
+ className: "dshr-providerName",
4000
+ children: item.name
4001
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4002
+ className: "dshr-providerCount",
4003
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4004
+ className: "dshr-mono",
4005
+ children: item.id
4006
+ }), item.ready === false ? " · 未就绪" : ""]
4007
+ })]
3990
4008
  })]
3991
4009
  })]
3992
- })]
3993
- }), Custom !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Custom, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [item.controls !== void 0 && item.controls.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExtControls, { item }), item.ready === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3994
- className: "dshr-alert",
3995
- 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 : "当前不可用" })]
3996
- })] })]
4010
+ }),
4011
+ item.controls !== void 0 && item.controls.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExtControls, { item }),
4012
+ item.ready === false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4013
+ className: "dshr-alert",
4014
+ 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 : "当前不可用" })]
4015
+ })
4016
+ ]
3997
4017
  });
3998
4018
  }
3999
4019
  //#endregion
@@ -4525,7 +4545,7 @@ window.__ModuleLoader__.load({
4525
4545
  };
4526
4546
  }
4527
4547
  //#endregion
4528
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/typeof.js
4548
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/typeof.js
4529
4549
  function _typeof(o) {
4530
4550
  "@babel/helpers - typeof";
4531
4551
  return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
@@ -4535,7 +4555,7 @@ window.__ModuleLoader__.load({
4535
4555
  }, _typeof(o);
4536
4556
  }
4537
4557
  //#endregion
4538
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/toPrimitive.js
4558
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/toPrimitive.js
4539
4559
  function toPrimitive(t, r) {
4540
4560
  if ("object" != _typeof(t) || !t) return t;
4541
4561
  var e = t[Symbol.toPrimitive];
@@ -4547,13 +4567,13 @@ window.__ModuleLoader__.load({
4547
4567
  return ("string" === r ? String : Number)(t);
4548
4568
  }
4549
4569
  //#endregion
4550
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/toPropertyKey.js
4570
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/toPropertyKey.js
4551
4571
  function toPropertyKey(t) {
4552
4572
  var i = toPrimitive(t, "string");
4553
4573
  return "symbol" == _typeof(i) ? i : i + "";
4554
4574
  }
4555
4575
  //#endregion
4556
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/defineProperty.js
4576
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/defineProperty.js
4557
4577
  function _defineProperty(e, r, t) {
4558
4578
  return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
4559
4579
  value: t,
@@ -4987,488 +5007,6 @@ window.__ModuleLoader__.load({
4987
5007
  });
4988
5008
  }
4989
5009
  //#endregion
4990
- //#region src/client/model-choice.ts
4991
- /** 从端点答复里挑出可用的模型,并说明「空的」到底是哪一种空。 */
4992
- function modelChoice(models) {
4993
- const ids = models.filter((m) => m.enabled !== false).map((m) => m.id);
4994
- if (ids.length > 0) return { ids };
4995
- return {
4996
- ids,
4997
- empty: models.length === 0 ? "none" : "all-disabled"
4998
- };
4999
- }
5000
- //#endregion
5001
- //#region src/client/ExtTestPanel.tsx
5002
- /**
5003
- * 插件自检面板 —— 扩展「插件自检」(`dsh-router-ext-test` / 扩展 id `test`) 的详情内容。
5004
- *
5005
- * **入口只有一处**:设置 → 路由 → 扩展 → 点开「插件自检」那张卡片(`ExtDetail` 查
5006
- * `ext-panels.ts` 的注册表拿到本组件)。
5007
- *
5008
- * 刻意**不给插件页那一行也挂一个详情页**:那一行的宿主行开关已经能开/关它,再点进去
5009
- * 看同一块面板就是同一个东西有两个入口 —— 与「内置扩展不在自绘节里重复列」同理。
5010
- * 插件页那一行只负责开关。
5011
- *
5012
- * 视觉上照着宿主的插件页写(`--dsw-alias-*` 那套 token),**不复用设置页的
5013
- * `dshr-*` 组件样式**:那些样式依赖 `--rs-*` 变量,而那套变量只定义在
5014
- * `.dshr-settings` / `.dshr-shell` 下,插件页不在其祖先里(未定义的 `var()` 会让
5015
- * 整条声明失效)。按钮直接用宿主的 `Button` 原语 —— 配色由原语自己的 token 家族
5016
- * 决定,不必在这里猜「主色底上该配什么字色」。
5017
- */
5018
- /** 每个契约成员状态的字形(颜色由 data-state 给)。 */
5019
- /** 模型启用状态那一行属于核心侧,没在 members 里,状态由「拿不拿得到」决定。 */
5020
- function coreModelsState(report) {
5021
- return report.core.models.total === null ? "unverified" : "ok";
5022
- }
5023
- var MEMBER_MARK = {
5024
- ok: "✓",
5025
- fail: "✕",
5026
- absent: "–",
5027
- unverified: "⊘"
5028
- };
5029
- var TONE_ICON = {
5030
- ok: "M20 6L9 17l-5-5",
5031
- fail: "M18 6L6 18M6 6l12 12",
5032
- 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"
5033
- };
5034
- function Glyph({ d }) {
5035
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
5036
- className: "dshr-compNoticeIcon",
5037
- viewBox: "0 0 24 24",
5038
- fill: "none",
5039
- stroke: "currentColor",
5040
- strokeWidth: "2",
5041
- strokeLinecap: "round",
5042
- strokeLinejoin: "round",
5043
- "aria-hidden": "true",
5044
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
5045
- });
5046
- }
5047
- /** 三个下拉里的一栏:标题行(可带右侧小注)+ 控件。`emptyHint` 在没得选时说明「为什么没有」。 */
5048
- function Picker(props) {
5049
- const empty = props.options.length === 0;
5050
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5051
- className: "dshr-compField",
5052
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5053
- className: "dshr-compFieldHead",
5054
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
5055
- className: "dshr-compFieldLabel",
5056
- children: props.label
5057
- }), props.note !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5058
- className: "dshr-compFieldNote",
5059
- children: props.note
5060
- })]
5061
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
5062
- className: "dshr-compSelect",
5063
- value: props.value,
5064
- disabled: props.disabled === true || empty,
5065
- "aria-label": props.label,
5066
- onChange: (e) => {
5067
- props.onChange(e.target.value);
5068
- },
5069
- children: empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5070
- value: "",
5071
- children: props.emptyHint
5072
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5073
- value: "",
5074
- children: "请选择"
5075
- }), props.options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
5076
- value: o.value,
5077
- children: o.label
5078
- }, o.value))] })
5079
- })]
5080
- });
5081
- }
5082
- function ExtTestPanel() {
5083
- const [suppliers, setSuppliers] = (0, react.useState)([]);
5084
- const [accounts, setAccounts] = (0, react.useState)([]);
5085
- const [models, setModels] = (0, react.useState)({ ids: [] });
5086
- const [supplierId, setSupplierId] = (0, react.useState)("");
5087
- const [modelId, setModelId] = (0, react.useState)("");
5088
- const [uid, setUid] = (0, react.useState)("");
5089
- const [busy, setBusy] = (0, react.useState)(false);
5090
- const [notice, setNotice] = (0, react.useState)(null);
5091
- const [loaded, setLoaded] = (0, react.useState)(false);
5092
- const [pluginId, setPluginId] = (0, react.useState)("");
5093
- const [probing, setProbing] = (0, react.useState)(false);
5094
- const [report, setReport] = (0, react.useState)(null);
5095
- const [probeError, setProbeError] = (0, react.useState)("");
5096
- (0, react.useEffect)(() => {
5097
- let live = true;
5098
- (async () => {
5099
- try {
5100
- 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())]);
5101
- if (!live) return;
5102
- setSuppliers((health.suppliers ?? []).filter((s) => s.enabled !== false));
5103
- setAccounts(status.ok ? status.accounts ?? [] : []);
5104
- } catch (err) {
5105
- if (live) setNotice({
5106
- tone: "network",
5107
- text: err.message
5108
- });
5109
- } finally {
5110
- if (live) setLoaded(true);
5111
- }
5112
- })();
5113
- return () => {
5114
- live = false;
5115
- };
5116
- }, []);
5117
- (0, react.useEffect)(() => {
5118
- setModelId("");
5119
- setUid("");
5120
- setNotice(null);
5121
- if (supplierId === "") {
5122
- setModels({ ids: [] });
5123
- return;
5124
- }
5125
- let live = true;
5126
- (async () => {
5127
- try {
5128
- const data = await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(supplierId)}/models`, { cache: "no-store" }).then((r) => r.json());
5129
- if (live) setModels(data.ok ? modelChoice(data.models ?? []) : { ids: [] });
5130
- } catch {
5131
- if (live) setModels({ ids: [] });
5132
- }
5133
- })();
5134
- return () => {
5135
- live = false;
5136
- };
5137
- }, [supplierId]);
5138
- const pluginSuppliers = suppliers.filter((x) => x.source === "external");
5139
- const runProbe = async () => {
5140
- if (pluginId === "" || probing) return;
5141
- setProbing(true);
5142
- setProbeError("");
5143
- setReport(null);
5144
- try {
5145
- const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(pluginId)}/probe`, {
5146
- method: "POST",
5147
- headers: { "Content-Type": "application/json" },
5148
- body: JSON.stringify({}),
5149
- cache: "no-store"
5150
- })).json();
5151
- if (data.ok && data.report !== void 0) setReport(data.report);
5152
- else setProbeError(data.error ?? "体检失败");
5153
- } catch (err) {
5154
- setProbeError(err.message);
5155
- } finally {
5156
- setProbing(false);
5157
- }
5158
- };
5159
- const links = accounts.filter((a) => a.supplier === supplierId);
5160
- const runnable = supplierId !== "" && modelId !== "";
5161
- const supplierName = suppliers.find((s) => s.id === supplierId)?.name ?? supplierId;
5162
- const run = async () => {
5163
- if (!runnable || busy) return;
5164
- setBusy(true);
5165
- setNotice(null);
5166
- try {
5167
- const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${encodeURIComponent(supplierId)}/models/test`, {
5168
- method: "POST",
5169
- headers: { "Content-Type": "application/json" },
5170
- body: JSON.stringify({
5171
- id: modelId,
5172
- uid
5173
- }),
5174
- cache: "no-store"
5175
- })).json();
5176
- const result = {
5177
- ok: data.ok,
5178
- uid,
5179
- supplier: supplierName,
5180
- model: modelId,
5181
- ...data.error === void 0 ? {} : { error: data.error }
5182
- };
5183
- setNotice({
5184
- tone: data.ok ? "ok" : "fail",
5185
- result
5186
- });
5187
- } catch (err) {
5188
- setNotice({
5189
- tone: "network",
5190
- text: err.message
5191
- });
5192
- } finally {
5193
- setBusy(false);
5194
- }
5195
- };
5196
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5197
- className: "dshr-tabBody dshr-comp",
5198
- children: [
5199
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5200
- className: "dshr-compHead",
5201
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
5202
- className: "dshr-compSectionTitle",
5203
- children: "连接测试"
5204
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5205
- className: "dshr-compSectionNote",
5206
- children: "选供应商 / 模型 / 连接"
5207
- })]
5208
- }),
5209
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5210
- className: "dshr-compForm",
5211
- children: [
5212
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
5213
- label: "供应商",
5214
- value: supplierId,
5215
- options: suppliers.map((s) => ({
5216
- value: s.id,
5217
- label: s.name
5218
- })),
5219
- emptyHint: loaded ? "没有已启用的供应商" : "加载中…",
5220
- onChange: setSupplierId
5221
- }),
5222
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
5223
- label: "模型",
5224
- note: "仅可用",
5225
- value: modelId,
5226
- options: models.ids.map((id) => ({
5227
- value: id,
5228
- label: id
5229
- })),
5230
- emptyHint: supplierId === "" ? "先选供应商" : models.empty === "all-disabled" ? "模型都被停用了(在供应商详情里开启)" : models.empty === "none" ? "没有模型(可先在供应商详情里拉取)" : "请选择",
5231
- disabled: supplierId === "",
5232
- onChange: (v) => {
5233
- setModelId(v);
5234
- setNotice(null);
5235
- }
5236
- }),
5237
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
5238
- label: "连接",
5239
- note: "留空 = 池内任选",
5240
- value: uid,
5241
- options: links.map((a) => ({
5242
- value: a.uid,
5243
- label: `${a.nickname ?? a.uid}${a.cooling ? "(冷却中)" : ""}`
5244
- })),
5245
- emptyHint: supplierId === "" ? "先选供应商" : "没有连接(留空则不指定)",
5246
- disabled: supplierId === "",
5247
- onChange: (v) => {
5248
- setUid(v);
5249
- setNotice(null);
5250
- }
5251
- })
5252
- ]
5253
- }),
5254
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5255
- className: "dshr-compActions dshr-compBtnRow",
5256
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5257
- variant: "primary",
5258
- size: "md",
5259
- disabled: !runnable || busy,
5260
- onClick: () => {
5261
- run();
5262
- },
5263
- children: busy ? "测试中…" : "测试"
5264
- }), !runnable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5265
- className: "dshr-compHint",
5266
- children: "先选供应商与模型"
5267
- })]
5268
- }),
5269
- notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5270
- className: "dshr-compNotice",
5271
- "data-tone": notice.tone,
5272
- role: "status",
5273
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { d: TONE_ICON[notice.tone] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5274
- className: "dshr-compNoticeBody",
5275
- children: notice.tone === "network" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5276
- className: "dshr-compNoticeTitle",
5277
- children: "请求失败"
5278
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5279
- className: "dshr-compNoticeText",
5280
- children: notice.text
5281
- })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5282
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5283
- className: "dshr-compNoticeTitle",
5284
- children: notice.tone === "ok" ? "测试通过" : "测试失败"
5285
- }),
5286
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5287
- className: "dshr-compNoticeText",
5288
- children: notice.tone === "ok" ? "这个连接能正常访问该模型。" : notice.result.error ?? "上游没有给出原因"
5289
- }),
5290
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5291
- className: "dshr-compNoticeMeta",
5292
- children: [
5293
- notice.result.supplier,
5294
- " · ",
5295
- notice.result.model,
5296
- " · ",
5297
- notice.result.uid === "" ? "池内任选" : notice.result.uid
5298
- ]
5299
- })
5300
- ] })
5301
- })]
5302
- }),
5303
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5304
- className: "dshr-compSection",
5305
- children: [
5306
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5307
- className: "dshr-compHead",
5308
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
5309
- className: "dshr-compSectionTitle",
5310
- children: "插件测试"
5311
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5312
- className: "dshr-compSectionNote",
5313
- children: "外部供应商插件"
5314
- })]
5315
- }),
5316
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5317
- className: "dshr-compIntro",
5318
- children: "前提:连接池里先有一个可用 token,剩下交给它。"
5319
- }),
5320
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5321
- className: "dshr-compProbeRow",
5322
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
5323
- label: "供应商插件",
5324
- value: pluginId,
5325
- options: pluginSuppliers.map((x) => ({
5326
- value: x.id,
5327
- label: x.name
5328
- })),
5329
- emptyHint: pluginSuppliers.length === 0 ? "没有装外部供应商插件" : "请选择",
5330
- onChange: (v) => {
5331
- setPluginId(v);
5332
- setReport(null);
5333
- setProbeError("");
5334
- }
5335
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5336
- className: "dshr-compProbeBtn dshr-compBtnRow",
5337
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
5338
- variant: "primary",
5339
- size: "md",
5340
- disabled: pluginId === "" || probing,
5341
- onClick: () => {
5342
- runProbe();
5343
- },
5344
- children: probing ? "测试中…" : "测试"
5345
- })
5346
- })]
5347
- }),
5348
- probeError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5349
- className: "dshr-compNotice",
5350
- "data-tone": "network",
5351
- role: "status",
5352
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { d: TONE_ICON.network }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5353
- className: "dshr-compNoticeBody",
5354
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5355
- className: "dshr-compNoticeTitle",
5356
- children: "体检失败"
5357
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5358
- className: "dshr-compNoticeText",
5359
- children: probeError
5360
- })]
5361
- })]
5362
- }),
5363
- report !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5364
- className: "dshr-compReport",
5365
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5366
- className: "dshr-compReportHead",
5367
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5368
- className: "dshr-compReportName",
5369
- children: report.name
5370
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5371
- className: "dshr-compReportSum",
5372
- children: [
5373
- "共 ",
5374
- report.summary.total,
5375
- " 个 · 实现 ",
5376
- report.summary.implemented,
5377
- " · 实跑 ",
5378
- report.summary.ran,
5379
- " · 通过 ",
5380
- report.summary.ok,
5381
- " · 失败 ",
5382
- report.summary.fail,
5383
- " · 未验 ",
5384
- report.summary.unverified,
5385
- " · 未实现 ",
5386
- report.summary.absent,
5387
- report.summary.bulk === "fail" ? " · 全部启用/禁用有问题" : report.summary.bulk === "ok" ? " · 全部启用/禁用通过" : ""
5388
- ]
5389
- })]
5390
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
5391
- className: "dshr-compMembers",
5392
- children: [
5393
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5394
- className: "dshr-compMember",
5395
- "data-state": coreModelsState(report),
5396
- children: [
5397
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5398
- className: "dshr-compMemberState",
5399
- "aria-hidden": "true",
5400
- children: MEMBER_MARK[coreModelsState(report)]
5401
- }),
5402
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5403
- className: "dshr-compMemberName",
5404
- children: ["模型启用状态", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5405
- className: "dshr-compMemberKey",
5406
- children: "core"
5407
- })]
5408
- }),
5409
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5410
- className: "dshr-compMemberDetail",
5411
- children: report.core.models.total === null ? report.core.models.note ?? "拿不到" : `共 ${report.core.models.total} 个 · 启用 ${report.core.models.enabled} · 停用 ${report.core.models.disabled}`
5412
- })
5413
- ]
5414
- }),
5415
- report.core.operations.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5416
- className: "dshr-compMember",
5417
- "data-state": o.ran === false ? "unverified" : o.ok === true ? "ok" : "fail",
5418
- children: [
5419
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5420
- className: "dshr-compMemberState",
5421
- "aria-hidden": "true",
5422
- children: MEMBER_MARK[o.ran === false ? "unverified" : o.ok === true ? "ok" : "fail"]
5423
- }),
5424
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5425
- className: "dshr-compMemberName",
5426
- children: [o.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5427
- className: "dshr-compMemberKey",
5428
- children: "core"
5429
- })]
5430
- }),
5431
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5432
- className: "dshr-compMemberDetail",
5433
- children: o.detail
5434
- })
5435
- ]
5436
- }, o.key)),
5437
- report.members.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5438
- className: "dshr-compMember",
5439
- "data-state": m.state,
5440
- children: [
5441
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5442
- className: "dshr-compMemberState",
5443
- "aria-hidden": "true",
5444
- children: MEMBER_MARK[m.state]
5445
- }),
5446
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5447
- className: "dshr-compMemberName",
5448
- children: [m.label, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
5449
- className: "dshr-compMemberKey",
5450
- children: m.key
5451
- })]
5452
- }),
5453
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5454
- className: "dshr-compMemberDetail",
5455
- children: m.detail ?? ""
5456
- })
5457
- ]
5458
- }, m.key))
5459
- ]
5460
- })]
5461
- })
5462
- ]
5463
- })
5464
- ]
5465
- });
5466
- }
5467
- //#endregion
5468
- //#region src/ext-test/plugin.ts
5469
- /** 扩展开关表里的注册键(也是 URL 面板读它的 id)。 */
5470
- var EXT_TEST_ID = "test";
5471
- //#endregion
5472
5010
  //#region src/client/settings-nav-icon.ts
5473
5011
  /**
5474
5012
  * 给设置导航里「路由」这一行换掉宿主的默认齿轮。
@@ -5852,18 +5390,6 @@ window.__ModuleLoader__.load({
5852
5390
  order: 10
5853
5391
  }, RouterComponentsSection));
5854
5392
  }
5855
- /**
5856
- * 给「插件自检」扩展登记它的详情面板。
5857
- *
5858
- * 面板的**唯一入口**是设置 → 路由 → 扩展 → 点开那张卡片(`ExtDetail` 查注册表拿到
5859
- * 本组件)。**刻意不给插件页那一行再挂一个详情页**:那一行的宿主行开关已经能开/关
5860
- * 它了,再点进去看同一块面板就是同一个东西有两个入口 —— 与「内置扩展不在自绘节里
5861
- * 重复列」是同一条理由。
5862
- */
5863
- function registerExtTestPanel(ctx) {
5864
- const dispose = registerExtPanel(EXT_TEST_ID, ExtTestPanel);
5865
- ctx.effect(() => dispose, "dsh-router: ext test panel registry");
5866
- }
5867
5393
  function apply(ctx) {
5868
5394
  ctx.effect(() => mountLastHitDock(ctx), "dsh-router: last-hit dock");
5869
5395
  const disposeSettings = mountSettingsSection(ctx);
@@ -5875,7 +5401,6 @@ window.__ModuleLoader__.load({
5875
5401
  ctx.effect(() => disposeSettings, "dsh-router: settings section");
5876
5402
  const disposeComponents = mountComponentsSection(ctx);
5877
5403
  if (disposeComponents !== void 0) ctx.effect(() => disposeComponents, "dsh-router: plugin page components");
5878
- registerExtTestPanel(ctx);
5879
5404
  ctx.effect(() => registerSettingsNavIcon(SECTION_LABEL), "dsh-router: settings nav icon");
5880
5405
  ctx.effect(() => rewriteRouterModelHint(), "dsh-router: models provider hint copy");
5881
5406
  }