dsh-router-core 0.1.0 → 0.3.0

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 (42) hide show
  1. package/README.md +58 -13
  2. package/lib/client-registry.js +824 -153
  3. package/lib/client.js +824 -153
  4. package/lib/index.js +1606 -227
  5. package/lib/types/llm/adapter.d.ts +2 -0
  6. package/lib/types/router/account-pool.d.ts +41 -0
  7. package/lib/types/router/index.d.ts +72 -4
  8. package/lib/types/router/types.d.ts +12 -10
  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 +69 -0
  12. package/lib/types/supplier-config.d.ts +15 -4
  13. package/lib/types/suppliers/contract.d.ts +70 -21
  14. package/lib/types/suppliers/registry.d.ts +4 -3
  15. package/package.json +2 -1
  16. package/src/client/CombosTab.tsx +28 -16
  17. package/src/client/RouterView.tsx +15 -2
  18. package/src/client/StatsTab.tsx +521 -0
  19. package/src/client/SupplierDetail.tsx +58 -34
  20. package/src/client/router.css +406 -0
  21. package/src/client/workspace-mount.tsx +2 -9
  22. package/src/index.ts +52 -4
  23. package/src/llm/adapter.test.ts +108 -0
  24. package/src/llm/adapter.ts +18 -6
  25. package/src/router/account-pool.test.ts +120 -0
  26. package/src/router/account-pool.ts +161 -0
  27. package/src/router/index.ts +652 -26
  28. package/src/router/models-cache.test.ts +455 -0
  29. package/src/router/precise-dispatch.test.ts +198 -0
  30. package/src/router/types.ts +12 -7
  31. package/src/router/usage-store.ts +499 -0
  32. package/src/router/usage-tokens.ts +327 -0
  33. package/src/router/usage.test.ts +558 -0
  34. package/src/shared.ts +77 -0
  35. package/src/supplier-config.ts +31 -14
  36. package/src/suppliers/contract.ts +71 -8
  37. package/src/suppliers/loader.ts +21 -13
  38. package/src/suppliers/nvidia/plugin.ts +161 -119
  39. package/src/suppliers/opencode/plugin.ts +47 -95
  40. package/src/suppliers/openrouter/plugin.ts +67 -122
  41. package/src/suppliers/registry.test.ts +194 -0
  42. package/src/suppliers/registry.ts +79 -59
@@ -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 min-width: 0;\n min-height: 0;\n flex-direction: column;\n gap: 14px;\n padding: 14px 18px;\n}\n\n.dshr-card {\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/* 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 — 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\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/* 周期按钮三态。特异性阶梯(类/伪类数 , 元素数):\n 文件顶部有 `.dshr-shell button { color: inherit; font: inherit }` = (1,1),\n 单类的 .dshr-period (1,0) 会被它打死(白字、600 字重全丢),所以下面每条\n 都必须带 .dshr-shell 抬到 (2,0) 以上。\n 两种 hover 用 :not() 做成互斥,谁赢跟源码顺序无关(免得以后有人挪块就翻车)。 */\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-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-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-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.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/*$vite$:1*/"));
7
7
  document.head.appendChild(elementStyle);
8
8
  }
9
9
  } catch (e) {
@@ -17,8 +17,8 @@ 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. */
@@ -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$3({ 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$3 = {
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",
@@ -116,7 +116,8 @@ window.__ModuleLoader__.load({
116
116
  check: "M4 12.5l5 5L20 6.5",
117
117
  edit: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3zM13.5 6.5l3 3",
118
118
  loading: "M12 3a9 9 0 1 0 9 9M12 3a9 9 0 0 1 9 9",
119
- search: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3"
119
+ search: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3",
120
+ refresh: "M20.5 12a8.5 8.5 0 1 1-2.6-6.1M20.5 4v5h-5"
120
121
  };
121
122
  function untilLabel(account) {
122
123
  if (!account.cooling || account.until === void 0) return "";
@@ -176,8 +177,8 @@ window.__ModuleLoader__.load({
176
177
  const [removeModelTarget, setRemoveModelTarget] = (0, react.useState)(null);
177
178
  const [poolStrategy, setPoolStrategy] = (0, react.useState)("fallback");
178
179
  const [poolOrder, setPoolOrder] = (0, react.useState)([]);
179
- const [checkinRule, setCheckinRule] = (0, react.useState)("all");
180
180
  const [checkingIn, setCheckingIn] = (0, react.useState)(false);
181
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
181
182
  const [dragIndex, setDragIndex] = (0, react.useState)(null);
182
183
  const mounted = (0, react.useRef)(true);
183
184
  (0, react.useEffect)(() => {
@@ -208,20 +209,11 @@ window.__ModuleLoader__.load({
208
209
  }, [loadModels]);
209
210
  const loadPoolConfig = (0, react.useCallback)(async () => {
210
211
  try {
211
- const [strategyRes, orderRes, ruleRes] = await Promise.all([
212
- fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, { cache: "no-store" }),
213
- fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/order`, { cache: "no-store" }),
214
- fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin/rule`, { cache: "no-store" })
215
- ]);
216
- const [strategyData, orderData, ruleData] = await Promise.all([
217
- strategyRes.json(),
218
- orderRes.json(),
219
- ruleRes.json()
220
- ]);
212
+ const [strategyRes, orderRes] = await Promise.all([fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, { cache: "no-store" }), fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/order`, { cache: "no-store" })]);
213
+ const [strategyData, orderData] = await Promise.all([strategyRes.json(), orderRes.json()]);
221
214
  if (mounted.current) {
222
215
  if (strategyData.ok && strategyData.strategy) setPoolStrategy(strategyData.strategy);
223
216
  if (orderData.ok && Array.isArray(orderData.order)) setPoolOrder(orderData.order);
224
- if (ruleData.ok && ruleData.rule) setCheckinRule(ruleData.rule);
225
217
  }
226
218
  } catch {}
227
219
  }, []);
@@ -268,6 +260,41 @@ window.__ModuleLoader__.load({
268
260
  setCheckingIn(false);
269
261
  }
270
262
  };
263
+ /** 刷新链接池:积分刷一遍 + 全供应商健康探测(最简会话,走真实 chatCompletions
264
+ * 并按账号池回退,所以「这个供应商还有没有活着的链接」能回答;不按链接细分 —— 那需要
265
+ * 按 uid 指定账号的管道,会给插件增负)。 */
266
+ const runRefresh = async () => {
267
+ if (refreshing) return;
268
+ setRefreshing(true);
269
+ let msg = "";
270
+ try {
271
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/links/refresh`, {
272
+ method: "POST",
273
+ cache: "no-store"
274
+ })).json();
275
+ if (!data.ok) {
276
+ showToast(data.error ?? "刷新失败");
277
+ return;
278
+ }
279
+ const probe = models?.find((m) => m.enabled) ?? models?.[0];
280
+ if (probe === void 0) msg = `已刷新 ${data.accounts?.length ?? 0} 链接 · 无模型可探测`;
281
+ else {
282
+ const probeData = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/test`, {
283
+ method: "POST",
284
+ headers: { "Content-Type": "application/json" },
285
+ body: JSON.stringify({ id: probe.id }),
286
+ cache: "no-store"
287
+ })).json();
288
+ msg = probeData.ok ? `已刷新 ${data.accounts?.length ?? 0} 链接 · 健康` : `已刷新 ${data.accounts?.length ?? 0} 链接 · 不可用:${probeData.error ?? "未知"}`;
289
+ }
290
+ showToast(msg);
291
+ onRefresh();
292
+ } catch (err) {
293
+ showToast(`刷新失败:${err.message}`);
294
+ } finally {
295
+ setRefreshing(false);
296
+ }
297
+ };
271
298
  const startLogin = async () => {
272
299
  if (canApiKey) {
273
300
  setShowLogin(true);
@@ -297,15 +324,15 @@ window.__ModuleLoader__.load({
297
324
  setBusy(false);
298
325
  }
299
326
  };
300
- const completeLogin = async () => {
301
- if (callbackUrl.trim() === "") return;
327
+ const completeLogin = async (poll = false) => {
328
+ if (!poll && callbackUrl.trim() === "") return;
302
329
  setBusy(true);
303
330
  setError("");
304
331
  try {
305
332
  const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/login/callback`, {
306
333
  method: "POST",
307
334
  headers: { "Content-Type": "application/json" },
308
- body: JSON.stringify({ callbackUrl: callbackUrl.trim() }),
335
+ body: JSON.stringify({ callbackUrl: poll ? "" : callbackUrl.trim() }),
309
336
  cache: "no-store"
310
337
  })).json();
311
338
  if (data.ok && data.account) {
@@ -532,14 +559,6 @@ window.__ModuleLoader__.load({
532
559
  cache: "no-store"
533
560
  });
534
561
  } catch {}
535
- try {
536
- await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin/rule`, {
537
- method: "POST",
538
- headers: { "Content-Type": "application/json" },
539
- body: JSON.stringify({ rule: checkinRule }),
540
- cache: "no-store"
541
- });
542
- } catch {}
543
562
  setShowAlias(false);
544
563
  } else setAliasError(data.error ?? "保存失败");
545
564
  } catch (err) {
@@ -576,8 +595,8 @@ window.__ModuleLoader__.load({
576
595
  type: "button",
577
596
  className: "dshr-backLink",
578
597
  onClick: onBack,
579
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
580
- d: I$2.back,
598
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
599
+ d: I$3.back,
581
600
  size: 16
582
601
  }), "返回"]
583
602
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -589,8 +608,8 @@ window.__ModuleLoader__.load({
589
608
  className: "dshr-providerImg",
590
609
  src: supplier.icon,
591
610
  alt: ""
592
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
593
- d: I$2.link,
611
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
612
+ d: I$3.link,
594
613
  size: 22
595
614
  })
596
615
  }),
@@ -618,8 +637,8 @@ window.__ModuleLoader__.load({
618
637
  setAliasError("");
619
638
  setShowAlias(true);
620
639
  },
621
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
622
- d: I$2.edit,
640
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
641
+ d: I$3.edit,
623
642
  size: 15
624
643
  })
625
644
  })
@@ -639,22 +658,48 @@ window.__ModuleLoader__.load({
639
658
  children: "链接池"
640
659
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
660
  className: "dshr-cardActions",
642
- children: [canCheckin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
643
- type: "button",
644
- className: "dshr-miniButton",
645
- onClick: () => void runCheckin(),
646
- disabled: checkingIn || supplierAccounts.length === 0,
647
- children: checkingIn ? "签到中…" : "签到"
648
- }), canLogin && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
649
- type: "button",
650
- className: "dshr-primaryButton",
651
- onClick: () => void startLogin(),
652
- disabled: busy,
653
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
654
- d: I$2.add,
655
- size: 14
656
- }), "添加链接"]
657
- })]
661
+ children: [
662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
663
+ type: "button",
664
+ className: "dshr-iconBtn dshr-iconBtn-sm",
665
+ onClick: () => void runRefresh(),
666
+ disabled: refreshing || supplierAccounts.length === 0,
667
+ title: "刷新积分并探测健康",
668
+ "aria-label": "刷新积分并探测健康",
669
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
670
+ className: refreshing ? "dshr-spin" : void 0,
671
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
672
+ d: I$3.refresh,
673
+ size: 15
674
+ })
675
+ })
676
+ }),
677
+ canCheckin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
678
+ type: "button",
679
+ className: "dshr-iconBtn dshr-iconBtn-sm",
680
+ onClick: () => void runCheckin(),
681
+ disabled: checkingIn || supplierAccounts.length === 0,
682
+ title: "签到所有链接",
683
+ "aria-label": "签到所有链接",
684
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
685
+ className: checkingIn ? "dshr-spin" : void 0,
686
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
687
+ d: I$3.checkCircle,
688
+ size: 15
689
+ })
690
+ })
691
+ }),
692
+ canLogin && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
693
+ type: "button",
694
+ className: "dshr-primaryButton",
695
+ onClick: () => void startLogin(),
696
+ disabled: busy,
697
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
698
+ d: I$3.add,
699
+ size: 14
700
+ }), "添加链接"]
701
+ })
702
+ ]
658
703
  })]
659
704
  }), statusLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
660
705
  className: "dshr-empty",
@@ -664,8 +709,8 @@ window.__ModuleLoader__.load({
664
709
  children: [
665
710
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
711
  className: "dshr-linkEmptyIcon",
667
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
668
- d: I$2.link,
712
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
713
+ d: I$3.link,
669
714
  size: 28
670
715
  })
671
716
  }),
@@ -684,8 +729,8 @@ window.__ModuleLoader__.load({
684
729
  className: "dshr-primaryButton",
685
730
  onClick: () => void startLogin(),
686
731
  disabled: busy,
687
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
688
- d: I$2.add,
732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
733
+ d: I$3.add,
689
734
  size: 14
690
735
  }), "添加链接"]
691
736
  })
@@ -810,8 +855,8 @@ window.__ModuleLoader__.load({
810
855
  className: "dshr-iconBtn dshr-iconBtn-sm dshr-linkDelete",
811
856
  title: "删除链接",
812
857
  onClick: () => setRemoveTarget(account),
813
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
814
- d: I$2.delete,
858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
859
+ d: I$3.delete,
815
860
  size: 15
816
861
  })
817
862
  })
@@ -855,8 +900,8 @@ window.__ModuleLoader__.load({
855
900
  setShowAddModel(true);
856
901
  },
857
902
  disabled: toggling,
858
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
859
- d: I$2.add,
903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
904
+ d: I$3.add,
860
905
  size: 13
861
906
  }), "添加模型"]
862
907
  }),
@@ -882,8 +927,8 @@ window.__ModuleLoader__.load({
882
927
  className: "dshr-modelSearch",
883
928
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
884
929
  className: "dshr-modelSearchIcon",
885
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
886
- d: I$2.search,
930
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
931
+ d: I$3.search,
887
932
  size: 13
888
933
  })
889
934
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -914,8 +959,8 @@ window.__ModuleLoader__.load({
914
959
  onClick: () => void toggleModel(model.id, false),
915
960
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
916
961
  className: "dshr-modelChipStatus",
917
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
918
- d: status === "ok" ? I$2.checkCircle : status === "error" ? I$2.cancel : I$2.robot,
962
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
963
+ d: status === "ok" ? I$3.checkCircle : status === "error" ? I$3.cancel : I$3.robot,
919
964
  size: 13
920
965
  })
921
966
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -932,8 +977,8 @@ window.__ModuleLoader__.load({
932
977
  onClick: () => void testModel(model.id),
933
978
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
934
979
  className: testingIds.has(model.id) ? "dshr-spin" : void 0,
935
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
936
- d: testingIds.has(model.id) ? I$2.loading : I$2.test,
980
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
981
+ d: testingIds.has(model.id) ? I$3.loading : I$3.test,
937
982
  size: 13
938
983
  })
939
984
  })
@@ -943,8 +988,8 @@ window.__ModuleLoader__.load({
943
988
  className: "dshr-iconBtn dshr-iconBtn-sm",
944
989
  title: "复制模型名",
945
990
  onClick: () => void copyModel(model.id),
946
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
947
- d: copiedModel === model.id ? I$2.check : I$2.copy,
991
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
992
+ d: copiedModel === model.id ? I$3.check : I$3.copy,
948
993
  size: 13
949
994
  })
950
995
  }),
@@ -953,8 +998,8 @@ window.__ModuleLoader__.load({
953
998
  className: "dshr-iconBtn dshr-iconBtn-sm dshr-modelDelete",
954
999
  title: "删除自定义模型",
955
1000
  onClick: () => setRemoveModelTarget(model.id),
956
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
957
- d: I$2.delete,
1001
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$3, {
1002
+ d: I$3.delete,
958
1003
  size: 13
959
1004
  })
960
1005
  })
@@ -1089,26 +1134,6 @@ window.__ModuleLoader__.load({
1089
1134
  })]
1090
1135
  })
1091
1136
  ] }),
1092
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1093
- className: "dshr-fieldLabel",
1094
- children: "签到规则"
1095
- }),
1096
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1097
- className: "dshr-muted",
1098
- children: "所有链接 = 签到每个链接;首个链接 = 只签到第一个链接。(dsh-router 通用策略)"
1099
- }),
1100
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1101
- className: "dshr-select dshr-input",
1102
- value: checkinRule,
1103
- onChange: (e) => setCheckinRule(e.target.value),
1104
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1105
- value: "all",
1106
- children: "所有链接"
1107
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1108
- value: "first",
1109
- children: "首个链接"
1110
- })]
1111
- }),
1112
1137
  aliasError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
1138
  className: "dshr-alert",
1114
1139
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: aliasError })]
@@ -1255,7 +1280,7 @@ window.__ModuleLoader__.load({
1255
1280
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1256
1281
  type: "button",
1257
1282
  className: "dshr-primaryButton",
1258
- onClick: () => void completeLogin(),
1283
+ onClick: () => void completeLogin(true),
1259
1284
  disabled: busy,
1260
1285
  children: busy ? "轮询中…" : "完成添加"
1261
1286
  })
@@ -1340,7 +1365,7 @@ window.__ModuleLoader__.load({
1340
1365
  * API Endpoint 卡片 + API Keys 卡片(Require API key + flex 行 key 列表 +
1341
1366
  * Create/暂停确认/Created 弹窗)。
1342
1367
  */
1343
- function Icon$1({ d, size = 18 }) {
1368
+ function Icon$2({ d, size = 18 }) {
1344
1369
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1345
1370
  viewBox: "0 0 24 24",
1346
1371
  width: size,
@@ -1354,7 +1379,7 @@ window.__ModuleLoader__.load({
1354
1379
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
1355
1380
  });
1356
1381
  }
1357
- var I$1 = {
1382
+ var I$2 = {
1358
1383
  api: "M4 4h16v16H4zM9 9h6v6H9zM12 9V7M12 17v-2M9 12H7M17 12h-2",
1359
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",
1360
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",
@@ -1503,7 +1528,7 @@ window.__ModuleLoader__.load({
1503
1528
  className: "dshr-cardHead",
1504
1529
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1505
1530
  className: "dshr-cardIcon",
1506
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, { d: I$1.api })
1531
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, { d: I$2.api })
1507
1532
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1508
1533
  className: "dshr-cardTitle",
1509
1534
  children: "API Endpoint"
@@ -1527,8 +1552,8 @@ window.__ModuleLoader__.load({
1527
1552
  className: "dshr-iconBtn",
1528
1553
  title: "复制",
1529
1554
  onClick: () => void copyText(endpointBase, "__endpoint"),
1530
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1531
- d: copiedKeyId === "__endpoint" ? I$1.check : I$1.copy,
1555
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1556
+ d: copiedKeyId === "__endpoint" ? I$2.check : I$2.copy,
1532
1557
  size: 16
1533
1558
  })
1534
1559
  })
@@ -1548,7 +1573,7 @@ window.__ModuleLoader__.load({
1548
1573
  children: [
1549
1574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1550
1575
  className: "dshr-cardIcon",
1551
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, { d: I$1.key })
1576
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, { d: I$2.key })
1552
1577
  }),
1553
1578
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1554
1579
  className: "dshr-cardTitle",
@@ -1558,8 +1583,8 @@ window.__ModuleLoader__.load({
1558
1583
  type: "button",
1559
1584
  className: "dshr-primaryButton dshr-cardAction",
1560
1585
  onClick: openCreate,
1561
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1562
- d: I$1.add,
1586
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1587
+ d: I$2.add,
1563
1588
  size: 14
1564
1589
  }), "创建 Key"]
1565
1590
  })
@@ -1594,8 +1619,8 @@ window.__ModuleLoader__.load({
1594
1619
  children: [
1595
1620
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1596
1621
  className: "dshr-keyEmptyIcon",
1597
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1598
- d: I$1.key,
1622
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1623
+ d: I$2.key,
1599
1624
  size: 30
1600
1625
  })
1601
1626
  }),
@@ -1611,8 +1636,8 @@ window.__ModuleLoader__.load({
1611
1636
  type: "button",
1612
1637
  className: "dshr-primaryButton",
1613
1638
  onClick: openCreate,
1614
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1615
- d: I$1.add,
1639
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1640
+ d: I$2.add,
1616
1641
  size: 14
1617
1642
  }), "创建 Key"]
1618
1643
  })
@@ -1634,8 +1659,8 @@ window.__ModuleLoader__.load({
1634
1659
  className: "dshr-iconBtn dshr-iconBtn-sm dshr-keyDelete",
1635
1660
  title: "删除",
1636
1661
  onClick: () => setDeleteTarget(key),
1637
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1638
- d: I$1.delete,
1662
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1663
+ d: I$2.delete,
1639
1664
  size: 14
1640
1665
  })
1641
1666
  })]
@@ -1652,8 +1677,8 @@ window.__ModuleLoader__.load({
1652
1677
  className: "dshr-iconBtn dshr-iconBtn-sm",
1653
1678
  title: visibleKeys.has(key.id) ? "隐藏" : "显示",
1654
1679
  onClick: () => toggleVisible(key.id),
1655
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1656
- d: visibleKeys.has(key.id) ? I$1.visibilityOff : I$1.visibility,
1680
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1681
+ d: visibleKeys.has(key.id) ? I$2.visibilityOff : I$2.visibility,
1657
1682
  size: 14
1658
1683
  })
1659
1684
  }),
@@ -1662,8 +1687,8 @@ window.__ModuleLoader__.load({
1662
1687
  className: "dshr-iconBtn dshr-iconBtn-sm",
1663
1688
  title: "复制",
1664
1689
  onClick: () => void copyText(key.key, key.id),
1665
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1666
- d: copiedKeyId === key.id ? I$1.check : I$1.copy,
1690
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
1691
+ d: copiedKeyId === key.id ? I$2.check : I$2.copy,
1667
1692
  size: 14
1668
1693
  })
1669
1694
  })
@@ -1840,7 +1865,7 @@ window.__ModuleLoader__.load({
1840
1865
  * 组合 = 命名的一组模型(完整名 alias/id),请求 model 传组合名时,
1841
1866
  * 按策略(回退/轮询)命中其中一个。可创建/编辑/删除。
1842
1867
  */
1843
- function Icon({ d, size = 18 }) {
1868
+ function Icon$1({ d, size = 18 }) {
1844
1869
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1845
1870
  viewBox: "0 0 24 24",
1846
1871
  width: size,
@@ -1854,7 +1879,7 @@ window.__ModuleLoader__.load({
1854
1879
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
1855
1880
  });
1856
1881
  }
1857
- var I = {
1882
+ var I$1 = {
1858
1883
  layers: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5M3 17l9 5 9-5",
1859
1884
  add: "M12 5v14M5 12h14",
1860
1885
  copy: "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
@@ -1865,12 +1890,22 @@ window.__ModuleLoader__.load({
1865
1890
  search: "M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM21 21l-4.35-4.35"
1866
1891
  };
1867
1892
  function CombosTab({ combos, aliases, onRefresh }) {
1868
- /** 当前供应商前缀(单供应商取第一个)。 */
1869
- const primaryAlias = aliases[0]?.alias ?? "";
1870
- /** 模型 → 所属供应商前缀(按 groups 分组匹配;找不到兜底 primaryAlias)。 */
1871
- const aliasOf = (model) => {
1872
- for (const g of groups) if (g.models.some((m) => m.id === model)) return g.supplier.alias;
1873
- return primaryAlias;
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}`;
1874
1909
  };
1875
1910
  const [showForm, setShowForm] = (0, react.useState)(false);
1876
1911
  const [editing, setEditing] = (0, react.useState)(null);
@@ -1945,8 +1980,10 @@ window.__ModuleLoader__.load({
1945
1980
  ...patch
1946
1981
  }));
1947
1982
  };
1948
- const toggleModel = (id) => {
1949
- 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] });
1950
1987
  };
1951
1988
  const handleDragStart = (index) => {
1952
1989
  setDragIndex(index);
@@ -2064,8 +2101,8 @@ window.__ModuleLoader__.load({
2064
2101
  children: [
2065
2102
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2066
2103
  className: "dshr-cardIcon",
2067
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2068
- d: I.layers,
2104
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2105
+ d: I$1.layers,
2069
2106
  size: 16
2070
2107
  })
2071
2108
  }),
@@ -2081,8 +2118,8 @@ window.__ModuleLoader__.load({
2081
2118
  type: "button",
2082
2119
  className: "dshr-primaryButton dshr-cardAction",
2083
2120
  onClick: openCreate,
2084
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2085
- d: I.add,
2121
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2122
+ d: I$1.add,
2086
2123
  size: 14
2087
2124
  }), "创建组合"]
2088
2125
  })
@@ -2108,8 +2145,8 @@ window.__ModuleLoader__.load({
2108
2145
  children: [
2109
2146
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2110
2147
  className: "dshr-keyEmptyIcon",
2111
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2112
- d: I.layers,
2148
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2149
+ d: I$1.layers,
2113
2150
  size: 30
2114
2151
  })
2115
2152
  }),
@@ -2125,8 +2162,8 @@ window.__ModuleLoader__.load({
2125
2162
  type: "button",
2126
2163
  className: "dshr-primaryButton",
2127
2164
  onClick: openCreate,
2128
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2129
- d: I.add,
2165
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2166
+ d: I$1.add,
2130
2167
  size: 14
2131
2168
  }), "创建组合"]
2132
2169
  })
@@ -2140,8 +2177,8 @@ window.__ModuleLoader__.load({
2140
2177
  children: [
2141
2178
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2142
2179
  className: "dshr-cardIcon",
2143
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2144
- d: I.layers,
2180
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2181
+ d: I$1.layers,
2145
2182
  size: 16
2146
2183
  })
2147
2184
  }),
@@ -2174,8 +2211,8 @@ window.__ModuleLoader__.load({
2174
2211
  className: "dshr-iconBtn dshr-iconBtn-sm",
2175
2212
  title: "复制组合名",
2176
2213
  onClick: () => void copyName(combo.name, combo.id),
2177
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2178
- d: copied === combo.id ? I.check : I.copy,
2214
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2215
+ d: copied === combo.id ? I$1.check : I$1.copy,
2179
2216
  size: 15
2180
2217
  })
2181
2218
  }),
@@ -2184,8 +2221,8 @@ window.__ModuleLoader__.load({
2184
2221
  className: "dshr-iconBtn dshr-iconBtn-sm",
2185
2222
  title: "编辑",
2186
2223
  onClick: () => openEdit(combo),
2187
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2188
- d: I.edit,
2224
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2225
+ d: I$1.edit,
2189
2226
  size: 15
2190
2227
  })
2191
2228
  }),
@@ -2194,8 +2231,8 @@ window.__ModuleLoader__.load({
2194
2231
  className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboOpBtn-danger",
2195
2232
  title: "删除",
2196
2233
  onClick: () => setDeleteTarget(combo),
2197
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2198
- d: I.delete,
2234
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2235
+ d: I$1.delete,
2199
2236
  size: 15
2200
2237
  })
2201
2238
  })
@@ -2216,7 +2253,7 @@ window.__ModuleLoader__.load({
2216
2253
  children: i + 1
2217
2254
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2218
2255
  className: "dshr-comboChipSupplier",
2219
- children: aliasOf(model) ? `${aliasOf(model)}/${model}` : model
2256
+ children: displayName(model)
2220
2257
  })]
2221
2258
  }, `${model}-${i}`)), combo.models.length > 4 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2222
2259
  className: "dshr-comboMore",
@@ -2291,8 +2328,8 @@ window.__ModuleLoader__.load({
2291
2328
  ]
2292
2329
  }), form.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2293
2330
  className: "dshr-comboEmptyStepsBox",
2294
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2295
- d: I.layers,
2331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2332
+ d: I$1.layers,
2296
2333
  size: 20
2297
2334
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2298
2335
  className: "dshr-muted",
@@ -2314,8 +2351,8 @@ window.__ModuleLoader__.load({
2314
2351
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2315
2352
  className: "dshr-comboGrip",
2316
2353
  title: "拖拽排序",
2317
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2318
- d: I.grip,
2354
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2355
+ d: I$1.grip,
2319
2356
  size: 14
2320
2357
  })
2321
2358
  }),
@@ -2325,15 +2362,15 @@ window.__ModuleLoader__.load({
2325
2362
  }),
2326
2363
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2327
2364
  className: "dshr-comboSelectedModel",
2328
- children: aliasOf(model) ? `${aliasOf(model)}/${model}` : model
2365
+ children: displayName(model)
2329
2366
  }),
2330
2367
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2331
2368
  type: "button",
2332
2369
  className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove",
2333
2370
  title: "移除",
2334
2371
  onClick: () => removeModel(index),
2335
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2336
- d: I.delete,
2372
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2373
+ d: I$1.delete,
2337
2374
  size: 14
2338
2375
  })
2339
2376
  })
@@ -2353,8 +2390,8 @@ window.__ModuleLoader__.load({
2353
2390
  className: "dshr-modelSearch dshr-comboModelSearch",
2354
2391
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2355
2392
  className: "dshr-modelSearchIcon",
2356
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2357
- d: I.search,
2393
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
2394
+ d: I$1.search,
2358
2395
  size: 13
2359
2396
  })
2360
2397
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
@@ -2373,7 +2410,7 @@ window.__ModuleLoader__.load({
2373
2410
  className: "dshr-muted dshr-comboPickEmpty",
2374
2411
  children: "没有可添加的模型"
2375
2412
  }) : groups.map((g) => {
2376
- 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())));
2377
2414
  if (items.length === 0) return null;
2378
2415
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2379
2416
  className: "dshr-comboPickGroup",
@@ -2387,7 +2424,7 @@ window.__ModuleLoader__.load({
2387
2424
  }), items.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2388
2425
  type: "button",
2389
2426
  className: "dshr-comboPickItem",
2390
- onClick: () => toggleModel(m.id),
2427
+ onClick: () => toggleModel(g.supplier.id, m.id),
2391
2428
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2392
2429
  className: "dshr-comboPickName",
2393
2430
  children: m.id
@@ -2458,6 +2495,636 @@ window.__ModuleLoader__.load({
2458
2495
  });
2459
2496
  }
2460
2497
  //#endregion
2498
+ //#region src/client/StatsTab.tsx
2499
+ /**
2500
+ * 概览 —— 仿 9router `Dashboard → Usage → Overview` 的统计看板。
2501
+ *
2502
+ * 布局/交互跟其它 tab 一致(同一套 `.dshr-card` / `.dshr-tabBody`):
2503
+ * 周期切换 → 汇总卡 → 趋势折线图 → Top 榜 → 最近请求。
2504
+ *
2505
+ * 两个 9router 有而我们刻意不做的(天花板,非遗漏):
2506
+ * - 成本估算:9router 有 pricingRepo 全表,dsh-router 没有价格数据,
2507
+ * 硬造一个只会显示假钱。宁可不显示。
2508
+ * - 实时推送:9router 有 `/api/usage/stream` 的 SSE 看板。这里刷新才更新
2509
+ * (面板本身有刷新按钮)。要实时就加一条 SSE 路由,看板不用改。
2510
+ *
2511
+ * token 口径见 `router/usage-tokens.ts`:上游不发 usage 时按字符数估算,
2512
+ * 估算过的字段标 `~`,鼠标悬停会说明。
2513
+ */
2514
+ function Icon({ d, size = 18 }) {
2515
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2516
+ viewBox: "0 0 24 24",
2517
+ width: size,
2518
+ height: size,
2519
+ fill: "none",
2520
+ stroke: "currentColor",
2521
+ strokeWidth: "1.8",
2522
+ strokeLinecap: "round",
2523
+ strokeLinejoin: "round",
2524
+ "aria-hidden": "true",
2525
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
2526
+ });
2527
+ }
2528
+ var I = {
2529
+ chart: "M3 3v18h18M7 15l4-5 3 3 5-7",
2530
+ bolt: "M13 2L4 14h6l-1 8 9-12h-6z",
2531
+ refresh: "M21 12a9 9 0 1 1-3-6.7M21 4v5h-5",
2532
+ empty: "M4 4h16v16H4zM8 12h8M12 8v8"
2533
+ };
2534
+ /**
2535
+ * 周期选项(9router 的 PERIODS)。
2536
+ * `value` 是 `/stats?period=` 的参数值,别为了显示去动它;
2537
+ * 显示文案统一中文(跟「今日」一致)。
2538
+ */
2539
+ var PERIODS = [
2540
+ {
2541
+ value: "today",
2542
+ label: "今日"
2543
+ },
2544
+ {
2545
+ value: "24h",
2546
+ label: "24 小时"
2547
+ },
2548
+ {
2549
+ value: "7d",
2550
+ label: "7 天"
2551
+ },
2552
+ {
2553
+ value: "30d",
2554
+ label: "30 天"
2555
+ }
2556
+ ];
2557
+ /** 「最近请求」显示条数(后端给了 20 条,面板只看最近的这些)。 */
2558
+ var RECENT_LIMIT = 10;
2559
+ /** 千分位。 */
2560
+ function fmt(n) {
2561
+ return new Intl.NumberFormat().format(n ?? 0);
2562
+ }
2563
+ /** 大数缩写:1.2K / 3.4M(卡片和柱图上放不下完整数字时用)。 */
2564
+ function fmtShort(n) {
2565
+ const v = n ?? 0;
2566
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
2567
+ if (v >= 1e4) return `${(v / 1e3).toFixed(1)}K`;
2568
+ return fmt(v);
2569
+ }
2570
+ function fmtDuration(ms) {
2571
+ const v = ms ?? 0;
2572
+ if (v <= 0) return "—";
2573
+ if (v < 1e3) return `${v}ms`;
2574
+ return `${(v / 1e3).toFixed(1)}s`;
2575
+ }
2576
+ function timeAgo(ts) {
2577
+ const diff = Math.max(0, Math.floor((Date.now() - ts) / 1e3));
2578
+ if (diff < 60) return `${diff}秒前`;
2579
+ if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`;
2580
+ if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`;
2581
+ return `${Math.floor(diff / 86400)}天前`;
2582
+ }
2583
+ function clockTime(ts) {
2584
+ const d = new Date(ts);
2585
+ const p = (n) => String(n).padStart(2, "0");
2586
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
2587
+ }
2588
+ function StatCard({ label, value, hint, tone = "default", title }) {
2589
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2590
+ className: `dshr-statCard dshr-statCard-${tone}`,
2591
+ title,
2592
+ children: [
2593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2594
+ className: "dshr-statLabel",
2595
+ children: label
2596
+ }),
2597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2598
+ className: "dshr-statValue",
2599
+ children: value
2600
+ }),
2601
+ hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2602
+ className: "dshr-statHint",
2603
+ children: hint
2604
+ })
2605
+ ]
2606
+ });
2607
+ }
2608
+ /**
2609
+ * 视口坐标固定,SVG 用 `preserveAspectRatio="none"` 横向拉满容器:
2610
+ * 线宽靠 `vector-effect="non-scaling-stroke"` 抵消拉伸,数值点/悬停辅助线
2611
+ * 用 HTML 绝对定位叠在 SVG 上(否则圆点会被拉成椭圆)。
2612
+ *
2613
+ * 交互:指针(鼠标/触摸/笔)取最近桶,键盘 ←/→/Home/End 逐格走,Esc 取消。
2614
+ * 天花板:只画一条 tokens 折线(桶里没有输入/输出分项),要分线得先改
2615
+ * `/stats/chart` 的桶结构。
2616
+ */
2617
+ var TREND_W = 640;
2618
+ var TREND_H = 150;
2619
+ var TREND_PAD_X = 12;
2620
+ var TREND_PAD_T = 12;
2621
+ var TREND_PAD_B = 8;
2622
+ function LineChart({ buckets, busy }) {
2623
+ const [hover, setHover] = (0, react.useState)(null);
2624
+ const n = buckets.length;
2625
+ const max = Math.max(1, ...buckets.map((b) => b.tokens));
2626
+ const hasData = buckets.some((b) => b.requests > 0 || b.tokens > 0);
2627
+ const active = hover === null ? void 0 : buckets[hover];
2628
+ const innerW = TREND_W - TREND_PAD_X * 2;
2629
+ const innerH = TREND_H - TREND_PAD_T - TREND_PAD_B;
2630
+ const xOf = (i) => TREND_PAD_X + (n <= 1 ? innerW / 2 : i * innerW / (n - 1));
2631
+ const yOf = (v) => TREND_PAD_T + (1 - v / max) * innerH;
2632
+ const pctX = (i) => `${xOf(i) / TREND_W * 100}%`;
2633
+ const pctY = (v) => `${yOf(v) / TREND_H * 100}%`;
2634
+ const pts = buckets.map((b, i) => `${xOf(i)},${yOf(b.tokens)}`);
2635
+ const linePath = pts.length > 0 ? `M${pts.join("L")}` : "";
2636
+ const areaPath = pts.length > 0 ? `${linePath}L${xOf(n - 1)},${TREND_H - TREND_PAD_B}L${xOf(0)},${TREND_H - TREND_PAD_B}Z` : "";
2637
+ const labelStep = Math.max(1, Math.ceil(n / 8));
2638
+ const showLabel = (i) => i === n - 1 || i % labelStep === 0 && n - 1 - i >= labelStep / 2;
2639
+ /** 取离指针最近的桶:比给每桶铺一个透明 rect 省 DOM,30 天也只有一层。 */
2640
+ const pick = (e) => {
2641
+ const rect = e.currentTarget.getBoundingClientRect();
2642
+ const x = (e.clientX - rect.left) / rect.width * TREND_W;
2643
+ const i = n <= 1 ? 0 : Math.round((x - TREND_PAD_X) / innerW * (n - 1));
2644
+ setHover(Math.min(n - 1, Math.max(0, i)));
2645
+ };
2646
+ /** 键盘逐格走:函数式更新,连按不会读到过期的 hover。 */
2647
+ const step = (delta) => {
2648
+ setHover((cur) => n === 0 ? null : Math.min(n - 1, Math.max(0, (cur ?? n - 1) + delta)));
2649
+ };
2650
+ const onKeyDown = (e) => {
2651
+ const act = {
2652
+ ArrowLeft: () => step(-1),
2653
+ ArrowRight: () => step(1),
2654
+ Home: () => setHover(0),
2655
+ End: () => setHover(n - 1),
2656
+ Escape: () => setHover(null)
2657
+ }[e.key];
2658
+ if (act === void 0) return;
2659
+ e.preventDefault();
2660
+ act();
2661
+ };
2662
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2663
+ className: "dshr-trendWrap",
2664
+ children: [busy && n > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2665
+ className: "dshr-trendBusy",
2666
+ "aria-hidden": "true"
2667
+ }), !hasData ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2668
+ className: "dshr-trendEmpty",
2669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2670
+ className: "dshr-trendEmptyIcon",
2671
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2672
+ d: I.empty,
2673
+ size: 26
2674
+ })
2675
+ }), "这个周期还没有请求"]
2676
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2678
+ className: "dshr-linePlot",
2679
+ role: "img",
2680
+ "aria-label": `Token 趋势:${n} 个时段,峰值 ${fmtShort(max)} tokens`,
2681
+ tabIndex: 0,
2682
+ onPointerMove: pick,
2683
+ onPointerDown: pick,
2684
+ onPointerLeave: (e) => {
2685
+ if (e.pointerType === "mouse") setHover(null);
2686
+ },
2687
+ onBlur: () => setHover(null),
2688
+ onKeyDown,
2689
+ children: [
2690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
2691
+ className: "dshr-lineSvg",
2692
+ viewBox: `0 0 ${TREND_W} ${TREND_H}`,
2693
+ preserveAspectRatio: "none",
2694
+ "aria-hidden": "true",
2695
+ children: [
2696
+ [
2697
+ 0,
2698
+ .5,
2699
+ 1
2700
+ ].map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
2701
+ className: "dshr-lineGrid",
2702
+ x1: 0,
2703
+ x2: TREND_W,
2704
+ y1: yOf(max * f),
2705
+ y2: yOf(max * f),
2706
+ vectorEffect: "non-scaling-stroke"
2707
+ }, f)),
2708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2709
+ className: "dshr-lineArea",
2710
+ d: areaPath
2711
+ }),
2712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2713
+ className: "dshr-linePath",
2714
+ d: linePath,
2715
+ vectorEffect: "non-scaling-stroke"
2716
+ })
2717
+ ]
2718
+ }),
2719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2720
+ className: "dshr-linePeak",
2721
+ children: ["峰值 ", fmtShort(max)]
2722
+ }),
2723
+ n <= 24 && buckets.map((b, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2724
+ className: "dshr-linePoint",
2725
+ style: {
2726
+ left: pctX(i),
2727
+ top: pctY(b.tokens)
2728
+ }
2729
+ }, i)),
2730
+ hover !== null && active !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2731
+ className: "dshr-lineGuide",
2732
+ style: { left: pctX(hover) }
2733
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2734
+ className: "dshr-lineDot",
2735
+ style: {
2736
+ left: pctX(hover),
2737
+ top: pctY(active.tokens)
2738
+ }
2739
+ })] })
2740
+ ]
2741
+ }),
2742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2743
+ className: "dshr-lineLabels",
2744
+ children: buckets.map((b, i) => showLabel(i) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2745
+ className: `dshr-lineLabel${hover === i ? " dshr-lineLabel-on" : ""}`,
2746
+ style: { left: pctX(i) },
2747
+ children: b.label
2748
+ }, i))
2749
+ }),
2750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2751
+ className: "dshr-trendReadout",
2752
+ title: active === void 0 ? void 0 : `${fmt(active.requests)} 请求 · ${fmt(active.tokens)} tokens`,
2753
+ children: active === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2754
+ className: "dshr-muted",
2755
+ children: "悬停或按 ←/→ 看每个时段"
2756
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: active.label }),
2758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2759
+ className: "dshr-muted",
2760
+ children: [
2761
+ " · ",
2762
+ fmtShort(active.requests),
2763
+ " 请求"
2764
+ ]
2765
+ }),
2766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2767
+ className: "dshr-muted",
2768
+ children: [
2769
+ " · ",
2770
+ fmtShort(active.tokens),
2771
+ " tokens"
2772
+ ]
2773
+ })
2774
+ ] })
2775
+ })
2776
+ ] })]
2777
+ });
2778
+ }
2779
+ function RankTable({ title, rows, empty }) {
2780
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2781
+ className: "dshr-card",
2782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2783
+ className: "dshr-cardHead",
2784
+ children: [
2785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2786
+ className: "dshr-cardIcon",
2787
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.chart })
2788
+ }),
2789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2790
+ className: "dshr-cardTitle",
2791
+ children: title
2792
+ }),
2793
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2794
+ className: "dshr-cardMeta",
2795
+ children: rows.length > 0 ? `Top ${rows.length}` : ""
2796
+ })
2797
+ ]
2798
+ }), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2799
+ className: "dshr-empty",
2800
+ children: empty
2801
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2802
+ className: "dshr-rankList",
2803
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2804
+ className: "dshr-rankRow dshr-rankHead",
2805
+ children: [
2806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2807
+ className: "dshr-rankName",
2808
+ children: "名称"
2809
+ }),
2810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2811
+ className: "dshr-rankNum",
2812
+ children: "请求"
2813
+ }),
2814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2815
+ className: "dshr-rankNum",
2816
+ children: "输入"
2817
+ }),
2818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2819
+ className: "dshr-rankNum",
2820
+ children: "输出"
2821
+ }),
2822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2823
+ className: "dshr-rankNum",
2824
+ children: "最近"
2825
+ })
2826
+ ]
2827
+ }), rows.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2828
+ className: "dshr-rankRow",
2829
+ children: [
2830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2831
+ className: "dshr-rankName dshr-mono",
2832
+ title: r.name,
2833
+ children: r.name
2834
+ }),
2835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2836
+ className: "dshr-rankNum",
2837
+ children: [fmt(r.requests), r.failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2838
+ className: "dshr-rankFail",
2839
+ title: `${r.failed} 次失败`,
2840
+ children: [
2841
+ " (",
2842
+ r.failed,
2843
+ ")"
2844
+ ]
2845
+ })]
2846
+ }),
2847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2848
+ className: "dshr-rankNum",
2849
+ children: fmtShort(r.promptTokens)
2850
+ }),
2851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2852
+ className: "dshr-rankNum",
2853
+ children: fmtShort(r.completionTokens)
2854
+ }),
2855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2856
+ className: "dshr-rankNum dshr-muted",
2857
+ children: r.lastTs > 0 ? timeAgo(r.lastTs) : "—"
2858
+ })
2859
+ ]
2860
+ }, r.name))]
2861
+ })]
2862
+ });
2863
+ }
2864
+ function RecentTable({ rows }) {
2865
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2866
+ className: "dshr-card",
2867
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2868
+ className: "dshr-cardHead",
2869
+ children: [
2870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2871
+ className: "dshr-cardIcon",
2872
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.bolt })
2873
+ }),
2874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2875
+ className: "dshr-cardTitle",
2876
+ children: "最近请求"
2877
+ }),
2878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2879
+ className: "dshr-cardMeta",
2880
+ children: [
2881
+ "最近 ",
2882
+ rows.length,
2883
+ " 条"
2884
+ ]
2885
+ })
2886
+ ]
2887
+ }), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2888
+ className: "dshr-empty",
2889
+ children: "还没有请求记录"
2890
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2891
+ className: "dshr-recentList",
2892
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2893
+ className: "dshr-recentRow dshr-rankHead",
2894
+ children: [
2895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2896
+ className: "dshr-recentTime",
2897
+ children: "时间"
2898
+ }),
2899
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2900
+ className: "dshr-recentModel",
2901
+ children: "模型"
2902
+ }),
2903
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2904
+ className: "dshr-recentSupplier",
2905
+ children: "供应商"
2906
+ }),
2907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2908
+ className: "dshr-rankNum",
2909
+ children: "In / Out"
2910
+ }),
2911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2912
+ className: "dshr-rankNum",
2913
+ children: "耗时"
2914
+ })
2915
+ ]
2916
+ }), rows.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2917
+ className: `dshr-recentRow${r.ok ? "" : " dshr-recentRow-fail"}`,
2918
+ children: [
2919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2920
+ className: "dshr-recentTime",
2921
+ title: new Date(r.ts).toLocaleString(),
2922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `dshr-dot ${r.ok ? "dshr-dot-ok" : "dshr-dot-fail"}` }), clockTime(r.ts)]
2923
+ }),
2924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2925
+ className: "dshr-recentModel dshr-mono",
2926
+ title: `请求 ${r.requested}${r.error === void 0 ? "" : ` · ${r.error}`}`,
2927
+ children: r.model === "" ? r.requested : r.model
2928
+ }),
2929
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2930
+ className: "dshr-recentSupplier",
2931
+ children: r.supplier === "" ? "—" : r.supplier
2932
+ }),
2933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2934
+ className: "dshr-rankNum",
2935
+ children: [
2936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2937
+ className: "dshr-tokenIn",
2938
+ children: [fmtShort(r.promptTokens), r.inputEstimated ? "~" : ""]
2939
+ }),
2940
+ " / ",
2941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2942
+ className: "dshr-tokenOut",
2943
+ children: [fmtShort(r.completionTokens), r.outputEstimated ? "~" : ""]
2944
+ })
2945
+ ]
2946
+ }),
2947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2948
+ className: "dshr-rankNum dshr-muted",
2949
+ children: fmtDuration(r.durationMs)
2950
+ })
2951
+ ]
2952
+ }, `${r.ts}-${r.model}-${i}`))]
2953
+ })]
2954
+ });
2955
+ }
2956
+ function StatsTab({ onRefresh, refreshing, active }) {
2957
+ const [period, setPeriod] = (0, react.useState)("today");
2958
+ const [stats, setStats] = (0, react.useState)(null);
2959
+ const [chart, setChart] = (0, react.useState)([]);
2960
+ const [loading, setLoading] = (0, react.useState)(true);
2961
+ const [error, setError] = (0, react.useState)("");
2962
+ const mounted = (0, react.useRef)(true);
2963
+ (0, react.useEffect)(() => {
2964
+ mounted.current = true;
2965
+ return () => {
2966
+ mounted.current = false;
2967
+ };
2968
+ }, []);
2969
+ const load = async (p) => {
2970
+ try {
2971
+ 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" })]);
2972
+ const s = await sRes.json();
2973
+ const c = await cRes.json();
2974
+ if (!mounted.current) return;
2975
+ setStats(s.ok ? s : null);
2976
+ setChart(c.ok ? c.chart ?? [] : []);
2977
+ setError(s.ok ? c.ok ? "" : c.error ?? "" : s.error ?? "加载统计失败");
2978
+ } catch (err) {
2979
+ if (mounted.current) setError(err.message);
2980
+ } finally {
2981
+ if (mounted.current) setLoading(false);
2982
+ }
2983
+ };
2984
+ (0, react.useEffect)(() => {
2985
+ if (!active) return;
2986
+ setLoading(true);
2987
+ load(period);
2988
+ }, [period, active]);
2989
+ const s = stats;
2990
+ const successRate = s !== null && s.requests !== void 0 && s.requests > 0 ? Math.round((s.okCount ?? 0) / s.requests * 100) : null;
2991
+ const estimated = s !== null && ((s.estimatedInputs ?? 0) > 0 || (s.estimatedOutputs ?? 0) > 0);
2992
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2993
+ className: "dshr-tabBody",
2994
+ children: [
2995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2996
+ className: "dshr-statsBar",
2997
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2998
+ className: "dshr-periods",
2999
+ role: "tablist",
3000
+ "aria-label": "统计周期",
3001
+ children: PERIODS.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3002
+ type: "button",
3003
+ role: "tab",
3004
+ "aria-selected": period === p.value,
3005
+ className: `dshr-period${period === p.value ? " dshr-period-on" : ""}`,
3006
+ onClick: () => setPeriod(p.value),
3007
+ children: p.label
3008
+ }, p.value))
3009
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3010
+ className: "dshr-statsBarRight",
3011
+ children: [s?.lifetime !== void 0 && s.lifetime > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3012
+ className: "dshr-muted dshr-lifetime",
3013
+ title: "累计请求数(不受周期影响)",
3014
+ children: ["累计 ", fmt(s.lifetime)]
3015
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3016
+ type: "button",
3017
+ className: "dshr-iconBtn",
3018
+ onClick: () => {
3019
+ load(period);
3020
+ onRefresh();
3021
+ },
3022
+ disabled: refreshing,
3023
+ title: refreshing ? "刷新中…" : "刷新统计",
3024
+ "aria-label": refreshing ? "刷新中" : "刷新统计",
3025
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3026
+ className: refreshing ? "dshr-spin" : void 0,
3027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
3028
+ d: I.refresh,
3029
+ size: 16
3030
+ })
3031
+ })
3032
+ })]
3033
+ })]
3034
+ }),
3035
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3036
+ className: "dshr-alert",
3037
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "统计加载失败" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
3038
+ }),
3039
+ loading && s === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3040
+ className: "dshr-empty",
3041
+ children: "加载中…"
3042
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3044
+ className: "dshr-statGrid",
3045
+ children: [
3046
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
3047
+ label: "总请求",
3048
+ value: fmt(s?.requests),
3049
+ hint: successRate === null ? void 0 : `成功率 ${successRate}%`,
3050
+ tone: "default"
3051
+ }),
3052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
3053
+ label: "输入 Tokens",
3054
+ value: fmtShort(s?.promptTokens),
3055
+ hint: (s?.estimatedInputs ?? 0) > 0 ? "~ 含估算" : void 0,
3056
+ tone: "primary",
3057
+ title: "包含缓存命中的输入"
3058
+ }),
3059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
3060
+ label: "输出 Tokens",
3061
+ value: fmtShort(s?.completionTokens),
3062
+ hint: (s?.estimatedOutputs ?? 0) > 0 ? "~ 含估算" : void 0,
3063
+ tone: "ok"
3064
+ }),
3065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
3066
+ label: "缓存 Tokens",
3067
+ value: fmtShort(s?.cachedTokens),
3068
+ hint: "命中缓存的输入部分",
3069
+ tone: "default"
3070
+ }),
3071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatCard, {
3072
+ label: "平均耗时",
3073
+ value: fmtDuration(s?.avgDurationMs),
3074
+ hint: (s?.avgTtfbMs ?? 0) > 0 ? `首字节 ${fmtDuration(s?.avgTtfbMs)}` : void 0,
3075
+ tone: "warn"
3076
+ })
3077
+ ]
3078
+ }),
3079
+ estimated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3080
+ className: "dshr-estimateNote",
3081
+ children: [
3082
+ "带 ",
3083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "~" }),
3084
+ " 的数字为估算:部分上游不返回用量,按 ~4 字符/token 推算。请求数与耗时不受影响。"
3085
+ ]
3086
+ }),
3087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3088
+ className: "dshr-card",
3089
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3090
+ className: "dshr-cardHead",
3091
+ children: [
3092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3093
+ className: "dshr-cardIcon",
3094
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { d: I.chart })
3095
+ }),
3096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3097
+ className: "dshr-cardTitle",
3098
+ children: "Token 趋势"
3099
+ }),
3100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3101
+ className: "dshr-cardMeta",
3102
+ children: PERIODS.find((p) => p.value === period)?.label
3103
+ })
3104
+ ]
3105
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineChart, {
3106
+ buckets: chart,
3107
+ busy: loading
3108
+ })]
3109
+ }),
3110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3111
+ className: "dshr-rankPair",
3112
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankTable, {
3113
+ title: "按供应商",
3114
+ rows: s?.bySupplier ?? [],
3115
+ empty: "这个周期还没有请求"
3116
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankTable, {
3117
+ title: "按模型",
3118
+ rows: s?.byModel ?? [],
3119
+ empty: "这个周期还没有请求"
3120
+ })]
3121
+ }),
3122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecentTable, { rows: (s?.recent ?? []).slice(0, RECENT_LIMIT) })
3123
+ ] })
3124
+ ]
3125
+ });
3126
+ }
3127
+ //#endregion
2461
3128
  //#region src/client/RouterView.tsx
2462
3129
  /**
2463
3130
  * 路由系统 (Routing System) panel — a simplified 9router.
@@ -2475,7 +3142,7 @@ window.__ModuleLoader__.load({
2475
3142
  return response.json();
2476
3143
  }
2477
3144
  function RouterView({ onBack }) {
2478
- const [tab, setTab] = (0, react.useState)("suppliers");
3145
+ const [tab, setTab] = (0, react.useState)("overview");
2479
3146
  const [detailSupplier, setDetailSupplier] = (0, react.useState)(null);
2480
3147
  const [snapshot, setSnapshot] = (0, react.useState)({
2481
3148
  health: null,
@@ -2593,6 +3260,12 @@ window.__ModuleLoader__.load({
2593
3260
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
2594
3261
  className: "dshr-nav",
2595
3262
  children: [
3263
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3264
+ type: "button",
3265
+ "aria-current": tab === "overview" ? "page" : void 0,
3266
+ onClick: () => setTab("overview"),
3267
+ children: "概览"
3268
+ }),
2596
3269
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2597
3270
  type: "button",
2598
3271
  "aria-current": tab === "suppliers" ? "page" : void 0,
@@ -2622,6 +3295,11 @@ window.__ModuleLoader__.load({
2622
3295
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2623
3296
  className: "dshr-content",
2624
3297
  children: [
3298
+ tab === "overview" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTab, {
3299
+ active: true,
3300
+ refreshing,
3301
+ onRefresh: () => void refresh()
3302
+ }),
2625
3303
  tab === "suppliers" && (detailSupplier !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SupplierDetail, {
2626
3304
  supplier: detailSupplier,
2627
3305
  accounts,
@@ -2900,13 +3578,6 @@ window.__ModuleLoader__.load({
2900
3578
  }
2901
3579
  function mountPanel(controller) {
2902
3580
  function Panel() {
2903
- (0, react.useEffect)(() => {
2904
- const original = document.title;
2905
- document.title = ROUTER_TITLE;
2906
- return () => {
2907
- document.title = original;
2908
- };
2909
- }, []);
2910
3581
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, { onBack: () => controller.close() });
2911
3582
  }
2912
3583
  let root;