dsh-router-core 0.2.0 → 0.3.1

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