dsh-router-core 0.1.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 +151 -0
  2. package/cordis.patch.yml +9 -0
  3. package/lib/client-registry.js +3006 -0
  4. package/lib/client.js +3006 -0
  5. package/lib/index.js +1756 -0
  6. package/lib/types/credential-store.d.ts +15 -0
  7. package/lib/types/index.d.ts +6 -0
  8. package/lib/types/keys.d.ts +39 -0
  9. package/lib/types/llm/adapter.d.ts +31 -0
  10. package/lib/types/router/index.d.ts +73 -0
  11. package/lib/types/router/types.d.ts +80 -0
  12. package/lib/types/shared.d.ts +138 -0
  13. package/lib/types/supplier-config.d.ts +28 -0
  14. package/lib/types/suppliers/contract.d.ts +98 -0
  15. package/lib/types/suppliers/loader.d.ts +41 -0
  16. package/lib/types/suppliers/registry.d.ts +33 -0
  17. package/package.json +86 -0
  18. package/src/client/CombosTab.tsx +458 -0
  19. package/src/client/EndpointTab.tsx +360 -0
  20. package/src/client/Modal.tsx +46 -0
  21. package/src/client/RouterView.tsx +186 -0
  22. package/src/client/SupplierDetail.tsx +943 -0
  23. package/src/client/index.tsx +18 -0
  24. package/src/client/router.css +2096 -0
  25. package/src/client/sidebar-entry.ts +167 -0
  26. package/src/client/workspace-controller.ts +26 -0
  27. package/src/client/workspace-mount.tsx +117 -0
  28. package/src/credential-store.ts +64 -0
  29. package/src/index.ts +468 -0
  30. package/src/keys.ts +143 -0
  31. package/src/llm/adapter.ts +273 -0
  32. package/src/router/index.ts +273 -0
  33. package/src/router/types.ts +85 -0
  34. package/src/shared.ts +152 -0
  35. package/src/supplier-config.ts +144 -0
  36. package/src/suppliers/contract.ts +73 -0
  37. package/src/suppliers/loader.ts +191 -0
  38. package/src/suppliers/nvidia/plugin.ts +268 -0
  39. package/src/suppliers/opencode/plugin.ts +200 -0
  40. package/src/suppliers/openrouter/plugin.ts +282 -0
  41. package/src/suppliers/registry.ts +365 -0
  42. package/src/types.d.ts +45 -0
package/lib/client.js ADDED
@@ -0,0 +1,3006 @@
1
+ (function() {
2
+ //#region \0vite/all-css
3
+ try {
4
+ if (typeof document != "undefined") {
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*/"));
7
+ document.head.appendChild(elementStyle);
8
+ }
9
+ } catch (e) {
10
+ console.error("vite-plugin-css-injected-by-js", e);
11
+ }
12
+ //#endregion
13
+ })();
14
+ window.__ModuleLoader__.load({
15
+ id: "dsh-router",
16
+ factory: (require) => {
17
+ var module = { exports: {} };
18
+ var exports = module.exports;
19
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
20
+ let react = require("react");
21
+ let react_dom_client = require("react-dom/client");
22
+ let react_jsx_runtime = require("react/jsx-runtime");
23
+ //#region src/shared.ts
24
+ /** Active-state attribute on <html> that shows the panel. */
25
+ var ROUTER_ACTIVE_ATTR = "data-dsh-router-active";
26
+ /** The panel's visible title (also the sidebar entry label). */
27
+ var ROUTER_TITLE = "路由系统";
28
+ /** Same-origin API base the client panel fetches (host half serves it). */
29
+ var ROUTER_API_BASE = "/router/api";
30
+ //#endregion
31
+ //#region src/client/Modal.tsx
32
+ /**
33
+ * 通用弹窗:遮罩 + 居中对话框,ESC / 点遮罩关闭。9router 风格。
34
+ */
35
+ function Modal({ title, onClose, children, dismissable = true, className = "" }) {
36
+ (0, react.useEffect)(() => {
37
+ if (!dismissable) return;
38
+ const onKey = (e) => {
39
+ if (e.key === "Escape") onClose();
40
+ };
41
+ document.addEventListener("keydown", onKey);
42
+ return () => document.removeEventListener("keydown", onKey);
43
+ }, [onClose, dismissable]);
44
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
45
+ className: "dshr-modalOverlay",
46
+ onClick: (e) => {
47
+ if (dismissable && e.target === e.currentTarget) onClose();
48
+ },
49
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50
+ className: `dshr-modal${className ? ` ${className}` : ""}`,
51
+ role: "dialog",
52
+ "aria-modal": "true",
53
+ "aria-label": title,
54
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
+ className: "dshr-modalHead",
56
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
57
+ className: "dshr-modalTitle",
58
+ children: title
59
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
60
+ type: "button",
61
+ className: "dshr-modalClose",
62
+ onClick: onClose,
63
+ "aria-label": "关闭",
64
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
65
+ viewBox: "0 0 16 16",
66
+ width: "14",
67
+ height: "14",
68
+ fill: "none",
69
+ stroke: "currentColor",
70
+ strokeWidth: "1.5",
71
+ strokeLinecap: "round",
72
+ "aria-hidden": "true",
73
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
74
+ })
75
+ })]
76
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
77
+ className: "dshr-modalBody",
78
+ children
79
+ })]
80
+ })
81
+ });
82
+ }
83
+ //#endregion
84
+ //#region src/client/SupplierDetail.tsx
85
+ /**
86
+ * 供应商详情 — 布局交互贴近 9router 供应商详情页。
87
+ * Header(返回 + 图标 + 名称 + 链接数)+ 链接池卡片(空状态 / 行列表 + 删除)
88
+ * + 可用模型卡片(flex-wrap chips,禁用模型单独列出可恢复)。
89
+ * 加链接流程由供应商能力驱动(登录链接 / API key / 轮询登录)。
90
+ */
91
+ function Icon$2({ d, size = 18 }) {
92
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
93
+ viewBox: "0 0 24 24",
94
+ width: size,
95
+ height: size,
96
+ fill: "none",
97
+ stroke: "currentColor",
98
+ strokeWidth: "1.8",
99
+ strokeLinecap: "round",
100
+ strokeLinejoin: "round",
101
+ "aria-hidden": "true",
102
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
103
+ });
104
+ }
105
+ var I$2 = {
106
+ back: "M19 12H5M12 19l-7-7 7-7",
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
+ add: "M12 5v14M5 12h14",
109
+ delete: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M10 11v6M14 11v6",
110
+ key: "M14 7a4 4 0 1 1-1.4 7.6L9 18H6v-3l4.4-4.4A4 4 0 0 1 14 7zM18 7h.01",
111
+ checkCircle: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM8.5 12l2.5 2.5 4.5-5",
112
+ cancel: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM9 9l6 6M15 9l-6 6",
113
+ robot: "M12 8V4M8 4h8M4 12a8 8 0 0 1 16 0v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6zM9 14h.01M15 14h.01",
114
+ test: "M9 3h6M10 3v6.3L4.7 18a2 2 0 0 0 1.8 3h11a2 2 0 0 0 1.8-3L14 9.3V3M7.5 15h9",
115
+ copy: "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
116
+ check: "M4 12.5l5 5L20 6.5",
117
+ edit: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3zM13.5 6.5l3 3",
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"
120
+ };
121
+ function untilLabel(account) {
122
+ if (!account.cooling || account.until === void 0) return "";
123
+ const remaining = new Date(account.until).getTime() - Date.now();
124
+ if (remaining <= 0) return "即将恢复";
125
+ const minutes = Math.ceil(remaining / 6e4);
126
+ if (minutes < 60) return `剩余 ${minutes} 分钟`;
127
+ return `剩余 ${Math.ceil(minutes / 60)} 小时`;
128
+ }
129
+ function statusBadge(account) {
130
+ if (account.disabled) return {
131
+ text: "已禁用",
132
+ cls: "dshr-badge-danger"
133
+ };
134
+ if (account.cooling) return {
135
+ text: "冷却中",
136
+ cls: "dshr-badge-warn"
137
+ };
138
+ return {
139
+ text: "健康",
140
+ cls: "dshr-badge-ok"
141
+ };
142
+ }
143
+ function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRefresh }) {
144
+ const hasCap = (name) => supplier.capabilities?.includes(name) ?? false;
145
+ const canLogin = hasCap("generateLoginUrl") || hasCap("addApiKey");
146
+ const canApiKey = hasCap("addApiKey");
147
+ const canPoll = hasCap("pollLogin");
148
+ const canPool = canLogin;
149
+ const canCheckin = hasCap("checkinNow");
150
+ const [showLogin, setShowLogin] = (0, react.useState)(false);
151
+ const [loginUrl, setLoginUrl] = (0, react.useState)(null);
152
+ const [callbackUrl, setCallbackUrl] = (0, react.useState)("");
153
+ const [keyName, setKeyName] = (0, react.useState)("");
154
+ const [apiKey, setApiKey] = (0, react.useState)("");
155
+ const [busy, setBusy] = (0, react.useState)(false);
156
+ const [error, setError] = (0, react.useState)("");
157
+ const [removeTarget, setRemoveTarget] = (0, react.useState)(null);
158
+ const [models, setModels] = (0, react.useState)(null);
159
+ const [alias, setAlias] = (0, react.useState)("");
160
+ const [modelsError, setModelsError] = (0, react.useState)("");
161
+ const [toast, setToast] = (0, react.useState)(null);
162
+ const [toggling, setToggling] = (0, react.useState)(false);
163
+ const [showAddModel, setShowAddModel] = (0, react.useState)(false);
164
+ const [newModelId, setNewModelId] = (0, react.useState)("");
165
+ const [addModelError, setAddModelError] = (0, react.useState)("");
166
+ const [addTesting, setAddTesting] = (0, react.useState)(false);
167
+ const [addTestResult, setAddTestResult] = (0, react.useState)(null);
168
+ const [showAlias, setShowAlias] = (0, react.useState)(false);
169
+ const [aliasDraft, setAliasDraft] = (0, react.useState)("");
170
+ const [aliasError, setAliasError] = (0, react.useState)("");
171
+ const [testingIds, setTestingIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
172
+ const [testResults, setTestResults] = (0, react.useState)({});
173
+ const [copiedModel, setCopiedModel] = (0, react.useState)(null);
174
+ const [modelQuery, setModelQuery] = (0, react.useState)("");
175
+ const [fetchingModels, setFetchingModels] = (0, react.useState)(false);
176
+ const [removeModelTarget, setRemoveModelTarget] = (0, react.useState)(null);
177
+ const [poolStrategy, setPoolStrategy] = (0, react.useState)("fallback");
178
+ const [poolOrder, setPoolOrder] = (0, react.useState)([]);
179
+ const [checkinRule, setCheckinRule] = (0, react.useState)("all");
180
+ const [checkingIn, setCheckingIn] = (0, react.useState)(false);
181
+ const [dragIndex, setDragIndex] = (0, react.useState)(null);
182
+ const mounted = (0, react.useRef)(true);
183
+ (0, react.useEffect)(() => {
184
+ mounted.current = true;
185
+ return () => {
186
+ mounted.current = false;
187
+ };
188
+ }, []);
189
+ const showToast = (msg) => {
190
+ setToast(msg);
191
+ window.setTimeout(() => {
192
+ if (mounted.current) setToast(null);
193
+ }, 2500);
194
+ };
195
+ const loadModels = (0, react.useCallback)(async () => {
196
+ try {
197
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models`, { cache: "no-store" })).json();
198
+ if (mounted.current) if (data.ok && data.models) {
199
+ setModels(data.models);
200
+ if (data.alias) setAlias(data.alias);
201
+ } else setModelsError(data.error ?? "加载失败");
202
+ } catch (err) {
203
+ if (mounted.current) setModelsError(err.message);
204
+ }
205
+ }, []);
206
+ (0, react.useEffect)(() => {
207
+ loadModels();
208
+ }, [loadModels]);
209
+ const loadPoolConfig = (0, react.useCallback)(async () => {
210
+ 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
+ ]);
221
+ if (mounted.current) {
222
+ if (strategyData.ok && strategyData.strategy) setPoolStrategy(strategyData.strategy);
223
+ if (orderData.ok && Array.isArray(orderData.order)) setPoolOrder(orderData.order);
224
+ if (ruleData.ok && ruleData.rule) setCheckinRule(ruleData.rule);
225
+ }
226
+ } catch {}
227
+ }, []);
228
+ (0, react.useEffect)(() => {
229
+ loadPoolConfig();
230
+ }, [loadPoolConfig]);
231
+ const savePoolOrder = async (uids) => {
232
+ setPoolOrder(uids);
233
+ try {
234
+ await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/order`, {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({ uids }),
238
+ cache: "no-store"
239
+ });
240
+ showToast("连接池顺序已保存");
241
+ } catch {
242
+ showToast("保存顺序失败");
243
+ }
244
+ };
245
+ const runCheckin = async () => {
246
+ if (checkingIn) return;
247
+ setCheckingIn(true);
248
+ try {
249
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin`, {
250
+ method: "POST",
251
+ cache: "no-store"
252
+ })).json();
253
+ if (data.ok && data.total > 0) {
254
+ const failed = (data.results ?? []).filter((r) => r.status !== "ok" && r.status !== "already");
255
+ let msg = `签到完成:${data.succeeded}/${data.total} 成功`;
256
+ const already = data.already ?? 0;
257
+ if (already > 0) msg += ` · ${already} 今日已签`;
258
+ if (failed.length > 0) {
259
+ const first = failed[0];
260
+ msg += ` · ${first?.message ?? "有失败"}`;
261
+ }
262
+ showToast(msg);
263
+ } else showToast(data.error ?? (data.total === 0 ? "没有可签到的链接" : "签到失败"));
264
+ onRefresh();
265
+ } catch (err) {
266
+ showToast(`签到失败:${err.message}`);
267
+ } finally {
268
+ setCheckingIn(false);
269
+ }
270
+ };
271
+ const startLogin = async () => {
272
+ if (canApiKey) {
273
+ setShowLogin(true);
274
+ setBusy(false);
275
+ setError("");
276
+ setLoginUrl(null);
277
+ setCallbackUrl("");
278
+ setKeyName("");
279
+ setApiKey("");
280
+ return;
281
+ }
282
+ setShowLogin(true);
283
+ setBusy(true);
284
+ setError("");
285
+ setLoginUrl(null);
286
+ setCallbackUrl("");
287
+ try {
288
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/login`, {
289
+ method: "POST",
290
+ cache: "no-store"
291
+ })).json();
292
+ if (data.ok && data.loginUrl) setLoginUrl(data.loginUrl);
293
+ else setError(data.error ?? "生成登录链接失败");
294
+ } catch (err) {
295
+ setError(err.message);
296
+ } finally {
297
+ setBusy(false);
298
+ }
299
+ };
300
+ const completeLogin = async () => {
301
+ if (callbackUrl.trim() === "") return;
302
+ setBusy(true);
303
+ setError("");
304
+ try {
305
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/login/callback`, {
306
+ method: "POST",
307
+ headers: { "Content-Type": "application/json" },
308
+ body: JSON.stringify({ callbackUrl: callbackUrl.trim() }),
309
+ cache: "no-store"
310
+ })).json();
311
+ if (data.ok && data.account) {
312
+ showToast(`已添加链接 ${data.account.nickname || data.account.uid}`);
313
+ setShowLogin(false);
314
+ setLoginUrl(null);
315
+ setCallbackUrl("");
316
+ onRefresh();
317
+ } else setError(data.error ?? "添加失败");
318
+ } catch (err) {
319
+ setError(err.message);
320
+ } finally {
321
+ setBusy(false);
322
+ }
323
+ };
324
+ const addApiKey = async () => {
325
+ if (apiKey.trim() === "") return;
326
+ setBusy(true);
327
+ setError("");
328
+ try {
329
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/links/add`, {
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
332
+ body: JSON.stringify({
333
+ name: keyName.trim(),
334
+ apiKey: apiKey.trim()
335
+ }),
336
+ cache: "no-store"
337
+ })).json();
338
+ if (data.ok && data.account) {
339
+ showToast(`已添加链接 ${data.account.nickname || data.account.uid}`);
340
+ setShowLogin(false);
341
+ setKeyName("");
342
+ setApiKey("");
343
+ onRefresh();
344
+ } else setError(data.error ?? "添加失败");
345
+ } catch (err) {
346
+ setError(err.message);
347
+ } finally {
348
+ setBusy(false);
349
+ }
350
+ };
351
+ const doRemove = async (uid) => {
352
+ if ((await (await fetch(`/router/api/suppliers/${supplier.id}/links/remove`, {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
355
+ body: JSON.stringify({ uid }),
356
+ cache: "no-store"
357
+ })).json()).ok) onRefresh();
358
+ setRemoveTarget(null);
359
+ };
360
+ const toggleModel = async (id, enabled) => {
361
+ setToggling(true);
362
+ try {
363
+ if ((await (await fetch(`/router/api/suppliers/${supplier.id}/models/toggle`, {
364
+ method: "POST",
365
+ headers: { "Content-Type": "application/json" },
366
+ body: JSON.stringify({
367
+ id,
368
+ enabled
369
+ }),
370
+ cache: "no-store"
371
+ })).json()).ok) {
372
+ setModels((prev) => prev ? prev.map((m) => m.id === id ? {
373
+ ...m,
374
+ enabled
375
+ } : m) : prev);
376
+ onRefresh();
377
+ }
378
+ } finally {
379
+ setToggling(false);
380
+ }
381
+ };
382
+ const testNewModel = async () => {
383
+ if (newModelId.trim() === "") return;
384
+ setAddTesting(true);
385
+ setAddTestResult(null);
386
+ setAddModelError("");
387
+ try {
388
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/test`, {
389
+ method: "POST",
390
+ headers: { "Content-Type": "application/json" },
391
+ body: JSON.stringify({ id: newModelId.trim() }),
392
+ cache: "no-store"
393
+ })).json();
394
+ setAddTestResult(data.ok ? { ok: true } : {
395
+ ok: false,
396
+ error: data.error ?? "测试失败"
397
+ });
398
+ } catch (err) {
399
+ setAddTestResult({
400
+ ok: false,
401
+ error: err.message
402
+ });
403
+ } finally {
404
+ setAddTesting(false);
405
+ }
406
+ };
407
+ const addCustomModel = async () => {
408
+ if (newModelId.trim() === "") return;
409
+ setBusy(true);
410
+ setAddModelError("");
411
+ try {
412
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/add`, {
413
+ method: "POST",
414
+ headers: { "Content-Type": "application/json" },
415
+ body: JSON.stringify({ id: newModelId.trim() }),
416
+ cache: "no-store"
417
+ })).json();
418
+ if (data.ok) {
419
+ setShowAddModel(false);
420
+ setNewModelId("");
421
+ await loadModels();
422
+ } else setAddModelError(data.error ?? "添加失败");
423
+ } catch (err) {
424
+ setAddModelError(err.message);
425
+ } finally {
426
+ setBusy(false);
427
+ }
428
+ };
429
+ const bulkModels = async (enabled) => {
430
+ setToggling(true);
431
+ try {
432
+ await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/bulk`, {
433
+ method: "POST",
434
+ headers: { "Content-Type": "application/json" },
435
+ body: JSON.stringify({ enabled }),
436
+ cache: "no-store"
437
+ });
438
+ await loadModels();
439
+ } finally {
440
+ setToggling(false);
441
+ }
442
+ };
443
+ const fetchModels = async () => {
444
+ setFetchingModels(true);
445
+ setModelsError("");
446
+ try {
447
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/fetch`, {
448
+ method: "POST",
449
+ cache: "no-store"
450
+ })).json();
451
+ if (data.ok) {
452
+ await loadModels();
453
+ const added = data.added ?? 0;
454
+ const removed = data.removed ?? 0;
455
+ if (added === 0 && removed === 0) showToast("模型已是最新");
456
+ else showToast(`已更新模型:+${added} 新增${removed > 0 ? `,${removed} 移除` : ""}`);
457
+ } else setModelsError(data.error ?? "获取失败");
458
+ } catch (err) {
459
+ setModelsError(err.message);
460
+ } finally {
461
+ setFetchingModels(false);
462
+ }
463
+ };
464
+ const removeModel = async (id) => {
465
+ setToggling(true);
466
+ try {
467
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/remove`, {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify({ id }),
471
+ cache: "no-store"
472
+ })).json();
473
+ if (data.ok) await loadModels();
474
+ else setModelsError(data.error ?? "删除失败");
475
+ } finally {
476
+ setToggling(false);
477
+ }
478
+ };
479
+ const testModel = async (id) => {
480
+ if (testingIds.has(id)) return;
481
+ setTestingIds((prev) => new Set(prev).add(id));
482
+ try {
483
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/models/test`, {
484
+ method: "POST",
485
+ headers: { "Content-Type": "application/json" },
486
+ body: JSON.stringify({ id }),
487
+ cache: "no-store"
488
+ })).json();
489
+ setTestResults((prev) => ({
490
+ ...prev,
491
+ [id]: data.ok ? "ok" : "error"
492
+ }));
493
+ if (!data.ok) setModelsError(data.error ?? "测试失败");
494
+ } catch (err) {
495
+ setTestResults((prev) => ({
496
+ ...prev,
497
+ [id]: "error"
498
+ }));
499
+ setModelsError(err.message);
500
+ } finally {
501
+ setTestingIds((prev) => {
502
+ const n = new Set(prev);
503
+ n.delete(id);
504
+ return n;
505
+ });
506
+ }
507
+ };
508
+ const copyModel = async (id) => {
509
+ try {
510
+ await navigator.clipboard.writeText(`${alias}/${id}`);
511
+ setCopiedModel(id);
512
+ window.setTimeout(() => setCopiedModel(null), 1500);
513
+ } catch {}
514
+ };
515
+ const saveAlias = async () => {
516
+ setBusy(true);
517
+ setAliasError("");
518
+ try {
519
+ const data = await (await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/alias`, {
520
+ method: "POST",
521
+ headers: { "Content-Type": "application/json" },
522
+ body: JSON.stringify({ alias: aliasDraft.trim() }),
523
+ cache: "no-store"
524
+ })).json();
525
+ if (data.ok) {
526
+ setAlias(aliasDraft.trim());
527
+ try {
528
+ await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/pool/strategy`, {
529
+ method: "POST",
530
+ headers: { "Content-Type": "application/json" },
531
+ body: JSON.stringify({ strategy: poolStrategy }),
532
+ cache: "no-store"
533
+ });
534
+ } 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
+ setShowAlias(false);
544
+ } else setAliasError(data.error ?? "保存失败");
545
+ } catch (err) {
546
+ setAliasError(err.message);
547
+ } finally {
548
+ setBusy(false);
549
+ }
550
+ };
551
+ const supplierAccounts = accounts.filter((a) => a.supplier === supplier.id);
552
+ const orderedAccounts = (() => {
553
+ const byUid = new Map(supplierAccounts.map((a) => [a.uid, a]));
554
+ const out = [];
555
+ const seen = /* @__PURE__ */ new Set();
556
+ for (const uid of poolOrder) {
557
+ const a = byUid.get(uid);
558
+ if (a && !seen.has(uid)) {
559
+ out.push(a);
560
+ seen.add(uid);
561
+ }
562
+ }
563
+ for (const a of supplierAccounts) if (!seen.has(a.uid)) out.push(a);
564
+ return out;
565
+ })();
566
+ const q = modelQuery.trim().toLowerCase();
567
+ const matchQuery = (m) => q === "" || m.id.toLowerCase().includes(q);
568
+ const enabledModels = models?.filter((m) => m.enabled && matchQuery(m)) ?? [];
569
+ const disabledModels = models?.filter((m) => !m.enabled && matchQuery(m)) ?? [];
570
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
571
+ className: "dshr-tabBody",
572
+ children: [
573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
574
+ className: "dshr-providerHead",
575
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
576
+ type: "button",
577
+ className: "dshr-backLink",
578
+ onClick: onBack,
579
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
580
+ d: I$2.back,
581
+ size: 16
582
+ }), "返回"]
583
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
584
+ className: "dshr-providerTitleRow",
585
+ children: [
586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
587
+ className: "dshr-providerIcon",
588
+ children: supplier.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
589
+ className: "dshr-providerImg",
590
+ src: supplier.icon,
591
+ alt: ""
592
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
593
+ d: I$2.link,
594
+ size: 22
595
+ })
596
+ }),
597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
598
+ className: "dshr-providerMeta",
599
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
600
+ className: "dshr-providerName",
601
+ children: supplier.name
602
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
603
+ className: "dshr-providerCount",
604
+ children: [
605
+ supplierAccounts.length,
606
+ " 链接",
607
+ canPool && ` · ${poolStrategy === "round-robin" ? "轮询" : "回退"}`,
608
+ ` · 前缀 ${alias}`
609
+ ]
610
+ })]
611
+ }),
612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
613
+ type: "button",
614
+ className: "dshr-iconBtn dshr-providerEdit",
615
+ title: "编辑供应商",
616
+ onClick: () => {
617
+ setAliasDraft(alias);
618
+ setAliasError("");
619
+ setShowAlias(true);
620
+ },
621
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
622
+ d: I$2.edit,
623
+ size: 15
624
+ })
625
+ })
626
+ ]
627
+ })]
628
+ }),
629
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
630
+ className: "dshr-alert",
631
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
632
+ }),
633
+ canPool ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
634
+ className: "dshr-card",
635
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
636
+ className: "dshr-cardHead",
637
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
638
+ className: "dshr-cardTitle",
639
+ children: "链接池"
640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
+ 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
+ })]
658
+ })]
659
+ }), statusLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
660
+ className: "dshr-empty",
661
+ children: "加载中…"
662
+ }) : supplierAccounts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
663
+ className: "dshr-linkEmpty",
664
+ children: [
665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
+ className: "dshr-linkEmptyIcon",
667
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
668
+ d: I$2.link,
669
+ size: 28
670
+ })
671
+ }),
672
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
673
+ className: "dshr-linkEmptyText",
674
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
675
+ className: "dshr-linkEmptyTitle",
676
+ children: "暂无链接"
677
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
678
+ className: "dshr-linkEmptyDesc",
679
+ children: "添加第一个登录链接开始使用"
680
+ })]
681
+ }),
682
+ canLogin && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
683
+ type: "button",
684
+ className: "dshr-primaryButton",
685
+ onClick: () => void startLogin(),
686
+ disabled: busy,
687
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
688
+ d: I$2.add,
689
+ size: 14
690
+ }), "添加链接"]
691
+ })
692
+ ]
693
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
694
+ className: "dshr-linkList",
695
+ children: orderedAccounts.map((account, index) => {
696
+ const badge = statusBadge(account);
697
+ const coolingText = account.cooling || account.disabled ? `${account.reason ?? ""}${untilLabel(account) !== "" ? ` · ${untilLabel(account)}` : ""}` : "";
698
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
699
+ className: `dshr-linkRow${dragIndex === index ? " dshr-linkRowDragging" : ""}`,
700
+ draggable: true,
701
+ onDragStart: (e) => {
702
+ setDragIndex(index);
703
+ e.dataTransfer.effectAllowed = "move";
704
+ },
705
+ onDragOver: (e) => {
706
+ e.preventDefault();
707
+ e.dataTransfer.dropEffect = "move";
708
+ },
709
+ onDrop: (e) => {
710
+ e.preventDefault();
711
+ if (dragIndex === null || dragIndex === index) {
712
+ setDragIndex(null);
713
+ return;
714
+ }
715
+ const next = [...orderedAccounts];
716
+ const [moved] = next.splice(dragIndex, 1);
717
+ if (moved) next.splice(index, 0, moved);
718
+ setDragIndex(null);
719
+ savePoolOrder(next.map((a) => a.uid));
720
+ },
721
+ onDragEnd: () => setDragIndex(null),
722
+ children: [
723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
724
+ className: "dshr-linkGrip",
725
+ title: "拖动排序",
726
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
727
+ viewBox: "0 0 24 24",
728
+ width: "14",
729
+ height: "14",
730
+ fill: "currentColor",
731
+ "aria-hidden": "true",
732
+ children: [
733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
734
+ cx: "9",
735
+ cy: "6",
736
+ r: "1.5"
737
+ }),
738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
739
+ cx: "15",
740
+ cy: "6",
741
+ r: "1.5"
742
+ }),
743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
744
+ cx: "9",
745
+ cy: "12",
746
+ r: "1.5"
747
+ }),
748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
749
+ cx: "15",
750
+ cy: "12",
751
+ r: "1.5"
752
+ }),
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
754
+ cx: "9",
755
+ cy: "18",
756
+ r: "1.5"
757
+ }),
758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
759
+ cx: "15",
760
+ cy: "18",
761
+ r: "1.5"
762
+ })
763
+ ]
764
+ })
765
+ }),
766
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
767
+ className: "dshr-linkMain",
768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
769
+ className: "dshr-linkTitleLine",
770
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
771
+ className: "dshr-linkName",
772
+ children: account.nickname || account.uid
773
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
774
+ className: `dshr-badge ${badge.cls}`,
775
+ children: badge.text
776
+ })]
777
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
778
+ className: "dshr-linkSub",
779
+ children: [
780
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
781
+ className: "dshr-mono dshr-linkUid",
782
+ children: account.uid
783
+ }),
784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
785
+ className: "dshr-linkDot",
786
+ children: "·"
787
+ }),
788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
789
+ className: "dshr-mono dshr-linkCredits",
790
+ children: [Math.ceil(account.credits), " 积分"]
791
+ }),
792
+ account.err_count !== void 0 && account.err_count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
793
+ className: "dshr-linkDot",
794
+ children: "·"
795
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
796
+ className: "dshr-linkErr",
797
+ children: ["连续错误 ", account.err_count]
798
+ })] }),
799
+ coolingText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
800
+ className: "dshr-reason dshr-linkCooling",
801
+ children: coolingText
802
+ })
803
+ ]
804
+ })]
805
+ }),
806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
807
+ className: "dshr-linkOps",
808
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
809
+ type: "button",
810
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-linkDelete",
811
+ title: "删除链接",
812
+ onClick: () => setRemoveTarget(account),
813
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
814
+ d: I$2.delete,
815
+ size: 15
816
+ })
817
+ })
818
+ })
819
+ ]
820
+ }, account.uid);
821
+ })
822
+ })]
823
+ }) : null,
824
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
825
+ className: "dshr-card",
826
+ children: [
827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
828
+ className: "dshr-cardHead",
829
+ children: [
830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
831
+ className: "dshr-cardTitle",
832
+ children: "可用模型"
833
+ }),
834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
835
+ className: "dshr-muted dshr-cardMeta dshr-cardMetaAfter",
836
+ children: models ? `${enabledModels.length}/${models.length} 启用` : ""
837
+ }),
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
839
+ className: "dshr-cardActions",
840
+ children: [
841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
842
+ type: "button",
843
+ className: "dshr-miniButton",
844
+ onClick: () => void fetchModels(),
845
+ disabled: fetchingModels,
846
+ children: fetchingModels ? "获取中…" : "获取模型"
847
+ }),
848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
849
+ type: "button",
850
+ className: "dshr-miniButton",
851
+ onClick: () => {
852
+ setAddModelError("");
853
+ setAddTestResult(null);
854
+ setNewModelId("");
855
+ setShowAddModel(true);
856
+ },
857
+ disabled: toggling,
858
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
859
+ d: I$2.add,
860
+ size: 13
861
+ }), "添加模型"]
862
+ }),
863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
864
+ type: "button",
865
+ className: "dshr-miniButton",
866
+ onClick: () => void bulkModels(true),
867
+ disabled: toggling,
868
+ children: "全部启用"
869
+ }),
870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
871
+ type: "button",
872
+ className: "dshr-miniButton",
873
+ onClick: () => void bulkModels(false),
874
+ disabled: toggling,
875
+ children: "全部禁用"
876
+ })
877
+ ]
878
+ })
879
+ ]
880
+ }),
881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
882
+ className: "dshr-modelSearch",
883
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
884
+ className: "dshr-modelSearchIcon",
885
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
886
+ d: I$2.search,
887
+ size: 13
888
+ })
889
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
890
+ className: "dshr-input",
891
+ placeholder: "搜索模型…",
892
+ value: modelQuery,
893
+ onChange: (e) => setModelQuery(e.target.value)
894
+ })]
895
+ }),
896
+ modelsError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
897
+ className: "dshr-alert",
898
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: modelsError })]
899
+ }),
900
+ models === null && modelsError === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
901
+ className: "dshr-empty",
902
+ children: "加载中…"
903
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
904
+ className: "dshr-modelChips",
905
+ children: [enabledModels.map((model) => {
906
+ const status = testResults[model.id];
907
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
+ className: `dshr-modelChipWrap${status === "ok" ? " dshr-modelChip-ok" : status === "error" ? " dshr-modelChip-err" : ""}`,
909
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
910
+ type: "button",
911
+ className: "dshr-modelChip",
912
+ title: "点击禁用",
913
+ disabled: toggling,
914
+ onClick: () => void toggleModel(model.id, false),
915
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
916
+ 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,
919
+ size: 13
920
+ })
921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
922
+ className: "dshr-modelChipId",
923
+ children: model.id
924
+ })]
925
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
926
+ className: "dshr-modelChipOps",
927
+ children: [
928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
929
+ type: "button",
930
+ className: "dshr-iconBtn dshr-iconBtn-sm",
931
+ title: testingIds.has(model.id) ? "测试中…" : "测试",
932
+ onClick: () => void testModel(model.id),
933
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
934
+ 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,
937
+ size: 13
938
+ })
939
+ })
940
+ }),
941
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
942
+ type: "button",
943
+ className: "dshr-iconBtn dshr-iconBtn-sm",
944
+ title: "复制模型名",
945
+ 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,
948
+ size: 13
949
+ })
950
+ }),
951
+ model.custom && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
952
+ type: "button",
953
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-modelDelete",
954
+ title: "删除自定义模型",
955
+ onClick: () => setRemoveModelTarget(model.id),
956
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$2, {
957
+ d: I$2.delete,
958
+ size: 13
959
+ })
960
+ })
961
+ ]
962
+ })]
963
+ }, model.id);
964
+ }), disabledModels.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
965
+ className: "dshr-disabledModels",
966
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
967
+ className: "dshr-disabledLabel",
968
+ children: [
969
+ "已禁用(",
970
+ disabledModels.length,
971
+ ")"
972
+ ]
973
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
974
+ className: "dshr-modelChips",
975
+ children: disabledModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
976
+ type: "button",
977
+ className: "dshr-modelChip dshr-modelChip-off",
978
+ title: "点击恢复",
979
+ disabled: toggling,
980
+ onClick: () => void toggleModel(model.id, true),
981
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
982
+ className: "dshr-modelChipId",
983
+ children: model.id
984
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
985
+ className: "dshr-modelChipRestore",
986
+ children: "+ 恢复"
987
+ })]
988
+ }, model.id))
989
+ })]
990
+ })]
991
+ })
992
+ ]
993
+ }),
994
+ showAddModel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
995
+ title: "添加模型",
996
+ onClose: () => setShowAddModel(false),
997
+ dismissable: false,
998
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
999
+ className: "dshr-modalForm",
1000
+ children: [
1001
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1002
+ className: "dshr-muted",
1003
+ children: "输入模型 id,自定义模型会加入可用列表。"
1004
+ }),
1005
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1006
+ className: "dshr-input",
1007
+ placeholder: "模型 id",
1008
+ value: newModelId,
1009
+ onChange: (e) => setNewModelId(e.target.value),
1010
+ autoFocus: true
1011
+ }),
1012
+ addTestResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1013
+ className: addTestResult.ok ? "dshr-alert dshr-alert-ok" : "dshr-alert",
1014
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: addTestResult.ok ? "测试通过" : "测试失败" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: addTestResult.ok ? "该模型可用。" : addTestResult.error })]
1015
+ }),
1016
+ addModelError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1017
+ className: "dshr-alert",
1018
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: addModelError })]
1019
+ }),
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1021
+ className: "dshr-modalActions",
1022
+ children: [
1023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1024
+ type: "button",
1025
+ className: "dshr-miniButton",
1026
+ onClick: () => void testNewModel(),
1027
+ disabled: addTesting || newModelId.trim() === "",
1028
+ children: addTesting ? "测试中…" : "测试"
1029
+ }),
1030
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1031
+ type: "button",
1032
+ className: "dshr-miniButton",
1033
+ onClick: () => setShowAddModel(false),
1034
+ children: "取消"
1035
+ }),
1036
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1037
+ type: "button",
1038
+ className: "dshr-primaryButton",
1039
+ onClick: () => void addCustomModel(),
1040
+ disabled: busy || newModelId.trim() === "",
1041
+ children: busy ? "添加中…" : "添加"
1042
+ })
1043
+ ]
1044
+ })
1045
+ ]
1046
+ })
1047
+ }),
1048
+ showAlias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1049
+ title: "编辑供应商",
1050
+ onClose: () => setShowAlias(false),
1051
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1052
+ className: "dshr-modalForm",
1053
+ children: [
1054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1056
+ className: "dshr-fieldLabel",
1057
+ children: "前缀"
1058
+ }),
1059
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1060
+ className: "dshr-muted",
1061
+ children: "前缀用于模型全名(复制模型名时 = 前缀/模型id),只含字母、数字、- 和 _。"
1062
+ }),
1063
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1064
+ className: "dshr-input dshr-mono",
1065
+ placeholder: "如 oc、or、nv",
1066
+ value: aliasDraft,
1067
+ onChange: (e) => setAliasDraft(e.target.value)
1068
+ })
1069
+ ] }),
1070
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1071
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1072
+ className: "dshr-fieldLabel",
1073
+ children: "连接池策略"
1074
+ }),
1075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1076
+ className: "dshr-muted",
1077
+ children: "回退 = 按连接池顺序取第一个健康链接;轮询 = 轮流使用健康链接。"
1078
+ }),
1079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1080
+ className: "dshr-select dshr-input",
1081
+ value: poolStrategy,
1082
+ onChange: (e) => setPoolStrategy(e.target.value),
1083
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1084
+ value: "fallback",
1085
+ children: "回退"
1086
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1087
+ value: "round-robin",
1088
+ children: "轮询"
1089
+ })]
1090
+ })
1091
+ ] }),
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
+ aliasError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ className: "dshr-alert",
1114
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: aliasError })]
1115
+ }),
1116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1117
+ className: "dshr-modalActions",
1118
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1119
+ type: "button",
1120
+ className: "dshr-miniButton",
1121
+ onClick: () => setShowAlias(false),
1122
+ children: "取消"
1123
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1124
+ type: "button",
1125
+ className: "dshr-primaryButton",
1126
+ onClick: () => void saveAlias(),
1127
+ disabled: busy || aliasDraft.trim() === "",
1128
+ children: busy ? "保存中…" : "保存"
1129
+ })]
1130
+ })
1131
+ ]
1132
+ })
1133
+ }),
1134
+ removeModelTarget !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1135
+ title: "删除自定义模型",
1136
+ onClose: () => setRemoveModelTarget(null),
1137
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1138
+ className: "dshr-modalForm",
1139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1140
+ className: "dshr-muted",
1141
+ children: [
1142
+ "确定删除自定义模型「",
1143
+ removeModelTarget,
1144
+ "」吗?将从可用列表移除。"
1145
+ ]
1146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1147
+ className: "dshr-modalActions",
1148
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1149
+ type: "button",
1150
+ className: "dshr-miniButton",
1151
+ onClick: () => setRemoveModelTarget(null),
1152
+ children: "取消"
1153
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1154
+ type: "button",
1155
+ className: "dshr-dangerButton",
1156
+ onClick: () => {
1157
+ const id = removeModelTarget;
1158
+ setRemoveModelTarget(null);
1159
+ removeModel(id);
1160
+ },
1161
+ children: "删除"
1162
+ })]
1163
+ })]
1164
+ })
1165
+ }),
1166
+ showLogin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1167
+ title: `添加链接 · ${supplier.name}`,
1168
+ onClose: () => {
1169
+ if (!busy) {
1170
+ setShowLogin(false);
1171
+ setLoginUrl(null);
1172
+ setCallbackUrl("");
1173
+ setKeyName("");
1174
+ setApiKey("");
1175
+ }
1176
+ },
1177
+ children: canApiKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1178
+ className: "dshr-modalForm",
1179
+ children: [
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1181
+ className: "dshr-muted",
1182
+ children: "填入 OpenRouter API key(https://openrouter.ai/settings/keys),可起名字区分多个 key。"
1183
+ }),
1184
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1185
+ className: "dshr-alert",
1186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
1187
+ }),
1188
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1189
+ className: "dshr-input",
1190
+ placeholder: "名字(可选,如 主key)",
1191
+ value: keyName,
1192
+ onChange: (e) => setKeyName(e.target.value)
1193
+ }),
1194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1195
+ className: "dshr-input",
1196
+ placeholder: "sk-or-v1-...",
1197
+ type: "password",
1198
+ value: apiKey,
1199
+ onChange: (e) => setApiKey(e.target.value)
1200
+ }),
1201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1202
+ className: "dshr-modalActions",
1203
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1204
+ type: "button",
1205
+ className: "dshr-miniButton",
1206
+ onClick: () => setShowLogin(false),
1207
+ disabled: busy,
1208
+ children: "取消"
1209
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1210
+ type: "button",
1211
+ className: "dshr-primaryButton",
1212
+ onClick: () => void addApiKey(),
1213
+ disabled: busy || apiKey.trim() === "",
1214
+ children: busy ? "验证中…" : "添加"
1215
+ })]
1216
+ })
1217
+ ]
1218
+ }) : busy && loginUrl === null && error === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1219
+ className: "dshr-muted",
1220
+ children: "正在生成登录链接…"
1221
+ }) : loginUrl === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1222
+ className: "dshr-empty",
1223
+ children: error !== "" ? error : "登录链接生成失败"
1224
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1225
+ className: "dshr-loginBox",
1226
+ children: [
1227
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1228
+ className: "dshr-loginStep",
1229
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1230
+ className: "dshr-loginNum",
1231
+ children: "1"
1232
+ }), "在浏览器打开下面链接,完成登录"]
1233
+ }),
1234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1235
+ className: "dshr-loginUrl",
1236
+ href: loginUrl,
1237
+ target: "_blank",
1238
+ rel: "noreferrer",
1239
+ children: loginUrl
1240
+ }),
1241
+ canPoll ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1243
+ className: "dshr-loginStep",
1244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1245
+ className: "dshr-loginNum",
1246
+ children: "2"
1247
+ }), "登录成功后点「完成添加」,自动获取凭证"]
1248
+ }),
1249
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1250
+ className: "dshr-alert",
1251
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
1252
+ }),
1253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1254
+ className: "dshr-modalActions",
1255
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1256
+ type: "button",
1257
+ className: "dshr-primaryButton",
1258
+ onClick: () => void completeLogin(),
1259
+ disabled: busy,
1260
+ children: busy ? "轮询中…" : "完成添加"
1261
+ })
1262
+ })
1263
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1265
+ className: "dshr-loginStep",
1266
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1267
+ className: "dshr-loginNum",
1268
+ children: "2"
1269
+ }), "登录成功后浏览器会跳到打不开的 127.0.0.1 地址"]
1270
+ }),
1271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1272
+ className: "dshr-loginStep",
1273
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1274
+ className: "dshr-loginNum",
1275
+ children: "3"
1276
+ }), "复制地址栏完整链接,粘贴到下面,点「完成添加」"]
1277
+ }),
1278
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1279
+ className: "dshr-alert",
1280
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
1281
+ }),
1282
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1283
+ className: "dshr-endpointRow",
1284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1285
+ className: "dshr-input",
1286
+ placeholder: "粘贴回调链接 http://127.0.0.1:18080/authorize?...",
1287
+ value: callbackUrl,
1288
+ onChange: (e) => setCallbackUrl(e.target.value)
1289
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1290
+ type: "button",
1291
+ className: "dshr-primaryButton",
1292
+ onClick: () => void completeLogin(),
1293
+ disabled: busy || callbackUrl.trim() === "",
1294
+ children: busy ? "处理中…" : "完成添加"
1295
+ })]
1296
+ })
1297
+ ] })
1298
+ ]
1299
+ })
1300
+ }),
1301
+ removeTarget !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1302
+ title: "删除链接",
1303
+ onClose: () => setRemoveTarget(null),
1304
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1305
+ className: "dshr-modalForm",
1306
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1307
+ className: "dshr-muted",
1308
+ children: [
1309
+ "确定删除链接「",
1310
+ removeTarget.nickname || removeTarget.uid,
1311
+ "」吗?该账号将立即从路由池移除。"
1312
+ ]
1313
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1314
+ className: "dshr-modalActions",
1315
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1316
+ type: "button",
1317
+ className: "dshr-miniButton",
1318
+ onClick: () => setRemoveTarget(null),
1319
+ children: "取消"
1320
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1321
+ type: "button",
1322
+ className: "dshr-dangerButton",
1323
+ onClick: () => void doRemove(removeTarget.uid),
1324
+ children: "删除"
1325
+ })]
1326
+ })]
1327
+ })
1328
+ }),
1329
+ toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1330
+ className: "dshr-toast",
1331
+ children: toast
1332
+ })
1333
+ ]
1334
+ });
1335
+ }
1336
+ //#endregion
1337
+ //#region src/client/EndpointTab.tsx
1338
+ /**
1339
+ * 端点与密钥 — 布局交互贴近 9router endpoint 页(无隧道/Tailscale)。
1340
+ * API Endpoint 卡片 + API Keys 卡片(Require API key + flex 行 key 列表 +
1341
+ * Create/暂停确认/Created 弹窗)。
1342
+ */
1343
+ function Icon$1({ d, size = 18 }) {
1344
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1345
+ viewBox: "0 0 24 24",
1346
+ width: size,
1347
+ height: size,
1348
+ fill: "none",
1349
+ stroke: "currentColor",
1350
+ strokeWidth: "1.8",
1351
+ strokeLinecap: "round",
1352
+ strokeLinejoin: "round",
1353
+ "aria-hidden": "true",
1354
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
1355
+ });
1356
+ }
1357
+ var I$1 = {
1358
+ api: "M4 4h16v16H4zM9 9h6v6H9zM12 9V7M12 17v-2M9 12H7M17 12h-2",
1359
+ 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
+ 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",
1361
+ visibilityOff: "M4 4l16 16M10.6 5.2A10 10 0 0 1 12 5c6.5 0 10 6 10 6a17 17 0 0 1-3.3 4M6.1 6.6A17 17 0 0 0 2 12s3.5 6 10 6a9.6 9.6 0 0 0 4-.8M9.9 9.9a3 3 0 0 0 4.2 4.2",
1362
+ copy: "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
1363
+ check: "M4 12.5l5 5L20 6.5",
1364
+ add: "M12 5v14M5 12h14",
1365
+ delete: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M10 11v6M14 11v6"
1366
+ };
1367
+ function maskKey(full) {
1368
+ if (!full || full.length <= 10) return full;
1369
+ return `${full.slice(0, 6)}${"•".repeat(Math.min(full.length - 10, 12))}${full.slice(-4)}`;
1370
+ }
1371
+ function EndpointTab() {
1372
+ const [keys, setKeys] = (0, react.useState)(null);
1373
+ const [requireApiKey, setRequireApiKey] = (0, react.useState)(false);
1374
+ const [showCreate, setShowCreate] = (0, react.useState)(false);
1375
+ const [newKeyName, setNewKeyName] = (0, react.useState)("");
1376
+ const [createdKey, setCreatedKey] = (0, react.useState)(null);
1377
+ const [pauseTarget, setPauseTarget] = (0, react.useState)(null);
1378
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
1379
+ const [visibleKeys, setVisibleKeys] = (0, react.useState)(() => /* @__PURE__ */ new Set());
1380
+ const [copiedKeyId, setCopiedKeyId] = (0, react.useState)(null);
1381
+ const [error, setError] = (0, react.useState)("");
1382
+ const [busy, setBusy] = (0, react.useState)(false);
1383
+ const mounted = (0, react.useRef)(true);
1384
+ const endpointBase = `${window.location.origin}/v1`;
1385
+ (0, react.useEffect)(() => {
1386
+ mounted.current = true;
1387
+ return () => {
1388
+ mounted.current = false;
1389
+ };
1390
+ }, []);
1391
+ const load = (0, react.useCallback)(async () => {
1392
+ try {
1393
+ const [keysRes, settingsRes] = await Promise.all([fetch(`${ROUTER_API_BASE}/keys`, { cache: "no-store" }), fetch(`${ROUTER_API_BASE}/settings`, { cache: "no-store" })]);
1394
+ const keysData = await keysRes.json();
1395
+ const settingsData = await settingsRes.json();
1396
+ if (mounted.current) {
1397
+ if (keysData.ok && keysData.keys) setKeys(keysData.keys);
1398
+ else setError(keysData.error ?? "加载密钥失败");
1399
+ if (settingsData.ok && settingsData.requireApiKey !== void 0) setRequireApiKey(settingsData.requireApiKey);
1400
+ }
1401
+ } catch (err) {
1402
+ if (mounted.current) setError(err.message);
1403
+ }
1404
+ }, []);
1405
+ (0, react.useEffect)(() => {
1406
+ load();
1407
+ }, [load]);
1408
+ const openCreate = () => {
1409
+ setNewKeyName("");
1410
+ setError("");
1411
+ setShowCreate(true);
1412
+ };
1413
+ const createKey = async () => {
1414
+ setBusy(true);
1415
+ setError("");
1416
+ try {
1417
+ const data = await (await fetch(`${ROUTER_API_BASE}/keys`, {
1418
+ method: "POST",
1419
+ headers: { "Content-Type": "application/json" },
1420
+ body: JSON.stringify({ name: newKeyName }),
1421
+ cache: "no-store"
1422
+ })).json();
1423
+ if (data.ok && data.key) {
1424
+ setShowCreate(false);
1425
+ setNewKeyName("");
1426
+ setCreatedKey(data.key);
1427
+ await load();
1428
+ } else setError(data.error ?? "创建失败");
1429
+ } catch (err) {
1430
+ setError(err.message);
1431
+ } finally {
1432
+ setBusy(false);
1433
+ }
1434
+ };
1435
+ const doDelete = async (id) => {
1436
+ if ((await (await fetch(`/router/api/keys/delete`, {
1437
+ method: "POST",
1438
+ headers: { "Content-Type": "application/json" },
1439
+ body: JSON.stringify({ id }),
1440
+ cache: "no-store"
1441
+ })).json()).ok) {
1442
+ setKeys((prev) => prev ? prev.filter((k) => k.id !== id) : prev);
1443
+ setVisibleKeys((prev) => {
1444
+ const next = new Set(prev);
1445
+ next.delete(id);
1446
+ return next;
1447
+ });
1448
+ }
1449
+ setDeleteTarget(null);
1450
+ };
1451
+ const toggleKey = async (id, isActive) => {
1452
+ if ((await (await fetch(`/router/api/keys/toggle`, {
1453
+ method: "POST",
1454
+ headers: { "Content-Type": "application/json" },
1455
+ body: JSON.stringify({
1456
+ id,
1457
+ isActive
1458
+ }),
1459
+ cache: "no-store"
1460
+ })).json()).ok) setKeys((prev) => prev ? prev.map((k) => k.id === id ? {
1461
+ ...k,
1462
+ isActive
1463
+ } : k) : prev);
1464
+ };
1465
+ const toggleRequire = async (value) => {
1466
+ setRequireApiKey(value);
1467
+ if (!(await (await fetch(`/router/api/settings`, {
1468
+ method: "PATCH",
1469
+ headers: { "Content-Type": "application/json" },
1470
+ body: JSON.stringify({ requireApiKey: value }),
1471
+ cache: "no-store"
1472
+ })).json()).ok) setRequireApiKey(!value);
1473
+ };
1474
+ const copyText = async (text, id) => {
1475
+ try {
1476
+ await navigator.clipboard.writeText(text);
1477
+ setCopiedKeyId(id);
1478
+ window.setTimeout(() => setCopiedKeyId(null), 1500);
1479
+ } catch {}
1480
+ };
1481
+ const toggleVisible = (id) => {
1482
+ setVisibleKeys((prev) => {
1483
+ const next = new Set(prev);
1484
+ if (next.has(id)) next.delete(id);
1485
+ else next.add(id);
1486
+ return next;
1487
+ });
1488
+ };
1489
+ const onKeySwitch = (key, next) => {
1490
+ if (key.isActive && !next) setPauseTarget(key);
1491
+ else toggleKey(key.id, next);
1492
+ };
1493
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1494
+ className: "dshr-tabBody",
1495
+ children: [
1496
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1497
+ className: "dshr-alert",
1498
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
1499
+ }),
1500
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1501
+ className: "dshr-card",
1502
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1503
+ className: "dshr-cardHead",
1504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1505
+ className: "dshr-cardIcon",
1506
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, { d: I$1.api })
1507
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1508
+ className: "dshr-cardTitle",
1509
+ children: "API Endpoint"
1510
+ })]
1511
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1512
+ className: "dshr-endpointList",
1513
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1514
+ className: "dshr-endpointRow",
1515
+ children: [
1516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1517
+ className: "dshr-endpointLabel",
1518
+ children: "Local"
1519
+ }),
1520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1521
+ className: "dshr-input dshr-mono",
1522
+ value: endpointBase,
1523
+ readOnly: true
1524
+ }),
1525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1526
+ type: "button",
1527
+ className: "dshr-iconBtn",
1528
+ title: "复制",
1529
+ 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,
1532
+ size: 16
1533
+ })
1534
+ })
1535
+ ]
1536
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1537
+ className: "dshr-muted dshr-reason dshr-endpointNote",
1538
+ children: "OpenAI 兼容。任何工具把 baseURL 指向它即可使用。鉴权由下方「Require API key」控制。"
1539
+ })]
1540
+ })]
1541
+ }),
1542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1543
+ className: "dshr-card",
1544
+ id: "require-api-key",
1545
+ children: [
1546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1547
+ className: "dshr-cardHead",
1548
+ children: [
1549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1550
+ className: "dshr-cardIcon",
1551
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, { d: I$1.key })
1552
+ }),
1553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1554
+ className: "dshr-cardTitle",
1555
+ children: "API Keys"
1556
+ }),
1557
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1558
+ type: "button",
1559
+ className: "dshr-primaryButton dshr-cardAction",
1560
+ onClick: openCreate,
1561
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1562
+ d: I$1.add,
1563
+ size: 14
1564
+ }), "创建 Key"]
1565
+ })
1566
+ ]
1567
+ }),
1568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1569
+ className: "dshr-requireRow",
1570
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1571
+ className: "dshr-requireInfo",
1572
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1573
+ className: "dshr-requireTitle",
1574
+ children: "Require API key"
1575
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1576
+ className: "dshr-requireDesc",
1577
+ children: "不带有效 key 的请求将被拒绝"
1578
+ })]
1579
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1580
+ type: "button",
1581
+ className: `dshr-toggle ${requireApiKey ? "dshr-toggle-on" : ""}`,
1582
+ role: "switch",
1583
+ "aria-checked": requireApiKey,
1584
+ onClick: () => void toggleRequire(!requireApiKey),
1585
+ title: requireApiKey ? "关闭鉴权" : "开启鉴权",
1586
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshr-toggleKnob" })
1587
+ })]
1588
+ }),
1589
+ keys === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1590
+ className: "dshr-empty",
1591
+ children: "加载中…"
1592
+ }) : keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1593
+ className: "dshr-keyEmpty",
1594
+ children: [
1595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1596
+ className: "dshr-keyEmptyIcon",
1597
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1598
+ d: I$1.key,
1599
+ size: 30
1600
+ })
1601
+ }),
1602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1603
+ className: "dshr-keyEmptyTitle",
1604
+ children: "还没有 API Key"
1605
+ }),
1606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1607
+ className: "dshr-keyEmptyDesc",
1608
+ children: "创建第一个 key 开始使用"
1609
+ }),
1610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1611
+ type: "button",
1612
+ className: "dshr-primaryButton",
1613
+ onClick: openCreate,
1614
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1615
+ d: I$1.add,
1616
+ size: 14
1617
+ }), "创建 Key"]
1618
+ })
1619
+ ]
1620
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1621
+ className: "dshr-keyList",
1622
+ children: keys.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1623
+ className: `dshr-keyRow${key.isActive ? "" : " dshr-keyRow-paused"}`,
1624
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1625
+ className: "dshr-keyInfo",
1626
+ children: [
1627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1628
+ className: "dshr-keyNameRow",
1629
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1630
+ className: "dshr-keyName",
1631
+ children: key.name
1632
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1633
+ type: "button",
1634
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-keyDelete",
1635
+ title: "删除",
1636
+ onClick: () => setDeleteTarget(key),
1637
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$1, {
1638
+ d: I$1.delete,
1639
+ size: 14
1640
+ })
1641
+ })]
1642
+ }),
1643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1644
+ className: "dshr-keyValue",
1645
+ children: [
1646
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1647
+ className: "dshr-mono",
1648
+ children: visibleKeys.has(key.id) ? key.key : maskKey(key.key)
1649
+ }),
1650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1651
+ type: "button",
1652
+ className: "dshr-iconBtn dshr-iconBtn-sm",
1653
+ title: visibleKeys.has(key.id) ? "隐藏" : "显示",
1654
+ 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,
1657
+ size: 14
1658
+ })
1659
+ }),
1660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1661
+ type: "button",
1662
+ className: "dshr-iconBtn dshr-iconBtn-sm",
1663
+ title: "复制",
1664
+ 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,
1667
+ size: 14
1668
+ })
1669
+ })
1670
+ ]
1671
+ }),
1672
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1673
+ className: "dshr-keyCreated",
1674
+ children: [
1675
+ "创建于 ",
1676
+ new Date(key.createdAt).toLocaleDateString(),
1677
+ key.isActive === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1678
+ className: "dshr-keyPaused",
1679
+ children: " · 已暂停"
1680
+ })
1681
+ ]
1682
+ })
1683
+ ]
1684
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1685
+ className: "dshr-keyOps",
1686
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1687
+ type: "button",
1688
+ className: `dshr-toggle ${key.isActive ? "dshr-toggle-on" : ""}`,
1689
+ role: "switch",
1690
+ "aria-checked": key.isActive,
1691
+ title: key.isActive ? "暂停 key" : "恢复 key",
1692
+ onClick: () => onKeySwitch(key, !key.isActive),
1693
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dshr-toggleKnob" })
1694
+ })
1695
+ })]
1696
+ }, key.id))
1697
+ })
1698
+ ]
1699
+ }),
1700
+ showCreate && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1701
+ title: "创建 API Key",
1702
+ onClose: () => setShowCreate(false),
1703
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1704
+ className: "dshr-modalForm",
1705
+ children: [
1706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1707
+ className: "dshr-input",
1708
+ placeholder: "Key 名称(如 生产环境)",
1709
+ value: newKeyName,
1710
+ onChange: (e) => setNewKeyName(e.target.value),
1711
+ autoFocus: true
1712
+ }),
1713
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1714
+ className: "dshr-alert",
1715
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: error })]
1716
+ }),
1717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1718
+ className: "dshr-modalActions",
1719
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1720
+ type: "button",
1721
+ className: "dshr-miniButton",
1722
+ onClick: () => setShowCreate(false),
1723
+ children: "取消"
1724
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1725
+ type: "button",
1726
+ className: "dshr-primaryButton",
1727
+ onClick: () => void createKey(),
1728
+ disabled: busy || newKeyName.trim() === "",
1729
+ children: busy ? "创建中…" : "创建"
1730
+ })]
1731
+ })
1732
+ ]
1733
+ })
1734
+ }),
1735
+ createdKey !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1736
+ title: "API Key 已创建",
1737
+ onClose: () => setCreatedKey(null),
1738
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1739
+ className: "dshr-modalForm",
1740
+ children: [
1741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1742
+ className: "dshr-warnBox",
1743
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1744
+ className: "dshr-warnTitle",
1745
+ children: "立即保存这个 key!"
1746
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1747
+ className: "dshr-warnDesc",
1748
+ children: "这是你唯一一次看到完整 key,请妥善保管。"
1749
+ })]
1750
+ }),
1751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1752
+ className: "dshr-endpointRow dshr-createdRow",
1753
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1754
+ className: "dshr-input dshr-mono",
1755
+ value: createdKey,
1756
+ readOnly: true
1757
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1758
+ type: "button",
1759
+ className: "dshr-miniButton",
1760
+ onClick: () => void copyText(createdKey, "__created"),
1761
+ children: copiedKeyId === "__created" ? "已复制" : "复制"
1762
+ })]
1763
+ }),
1764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1765
+ type: "button",
1766
+ className: "dshr-primaryButton dshr-fullButton",
1767
+ onClick: () => setCreatedKey(null),
1768
+ children: "完成"
1769
+ })
1770
+ ]
1771
+ })
1772
+ }),
1773
+ pauseTarget !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1774
+ title: "暂停 API Key",
1775
+ onClose: () => setPauseTarget(null),
1776
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1777
+ className: "dshr-modalForm",
1778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1779
+ className: "dshr-muted",
1780
+ children: [
1781
+ "暂停 Key「",
1782
+ pauseTarget.name,
1783
+ "」?此 key 将立即失效,之后可以恢复。"
1784
+ ]
1785
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1786
+ className: "dshr-modalActions",
1787
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1788
+ type: "button",
1789
+ className: "dshr-miniButton",
1790
+ onClick: () => setPauseTarget(null),
1791
+ children: "取消"
1792
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1793
+ type: "button",
1794
+ className: "dshr-dangerButton",
1795
+ onClick: () => {
1796
+ const t = pauseTarget;
1797
+ setPauseTarget(null);
1798
+ toggleKey(t.id, false);
1799
+ },
1800
+ children: "暂停"
1801
+ })]
1802
+ })]
1803
+ })
1804
+ }),
1805
+ deleteTarget !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
1806
+ title: "删除 API Key",
1807
+ onClose: () => setDeleteTarget(null),
1808
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1809
+ className: "dshr-modalForm",
1810
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1811
+ className: "dshr-muted",
1812
+ children: [
1813
+ "确定删除 Key「",
1814
+ deleteTarget.name,
1815
+ "」吗?使用它的客户端将立即失效,无法恢复。"
1816
+ ]
1817
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1818
+ className: "dshr-modalActions",
1819
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1820
+ type: "button",
1821
+ className: "dshr-miniButton",
1822
+ onClick: () => setDeleteTarget(null),
1823
+ children: "取消"
1824
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1825
+ type: "button",
1826
+ className: "dshr-dangerButton",
1827
+ onClick: () => void doDelete(deleteTarget.id),
1828
+ children: "删除"
1829
+ })]
1830
+ })]
1831
+ })
1832
+ })
1833
+ ]
1834
+ });
1835
+ }
1836
+ //#endregion
1837
+ //#region src/client/CombosTab.tsx
1838
+ /**
1839
+ * 组合 — 布局交互贴近 9router combos 页。
1840
+ * 组合 = 命名的一组模型(完整名 alias/id),请求 model 传组合名时,
1841
+ * 按策略(回退/轮询)命中其中一个。可创建/编辑/删除。
1842
+ */
1843
+ function Icon({ d, size = 18 }) {
1844
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1845
+ viewBox: "0 0 24 24",
1846
+ width: size,
1847
+ height: size,
1848
+ fill: "none",
1849
+ stroke: "currentColor",
1850
+ strokeWidth: "1.8",
1851
+ strokeLinecap: "round",
1852
+ strokeLinejoin: "round",
1853
+ "aria-hidden": "true",
1854
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d })
1855
+ });
1856
+ }
1857
+ var I = {
1858
+ layers: "M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5M3 17l9 5 9-5",
1859
+ add: "M12 5v14M5 12h14",
1860
+ copy: "M9 9h11v11H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
1861
+ check: "M4 12.5l5 5L20 6.5",
1862
+ edit: "M4 20h4L19.5 8.5a2.1 2.1 0 0 0-3-3L5 17v3zM13.5 6.5l3 3",
1863
+ delete: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M10 11v6M14 11v6",
1864
+ grip: "M9 6h.01M9 12h.01M9 18h.01M15 6h.01M15 12h.01M15 18h.01",
1865
+ search: "M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16zM21 21l-4.35-4.35"
1866
+ };
1867
+ 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;
1874
+ };
1875
+ const [showForm, setShowForm] = (0, react.useState)(false);
1876
+ const [editing, setEditing] = (0, react.useState)(null);
1877
+ const [form, setForm] = (0, react.useState)({
1878
+ name: "",
1879
+ strategy: "fallback",
1880
+ models: []
1881
+ });
1882
+ const [formError, setFormError] = (0, react.useState)("");
1883
+ const [saving, setSaving] = (0, react.useState)(false);
1884
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
1885
+ const [copied, setCopied] = (0, react.useState)(null);
1886
+ const [toast, setToast] = (0, react.useState)(null);
1887
+ const [modelQuery, setModelQuery] = (0, react.useState)("");
1888
+ const [groups, setGroups] = (0, react.useState)([]);
1889
+ const [loadingGroups, setLoadingGroups] = (0, react.useState)(false);
1890
+ const [dragIndex, setDragIndex] = (0, react.useState)(null);
1891
+ const [dragOverIndex, setDragOverIndex] = (0, react.useState)(null);
1892
+ const mounted = (0, react.useRef)(true);
1893
+ (0, react.useEffect)(() => {
1894
+ mounted.current = true;
1895
+ loadGroups();
1896
+ return () => {
1897
+ mounted.current = false;
1898
+ };
1899
+ }, []);
1900
+ /** 打开弹窗时才拉取可用模型(按供应商分组)。 */
1901
+ const loadGroups = async () => {
1902
+ setLoadingGroups(true);
1903
+ try {
1904
+ const data = await (await fetch(`${ROUTER_API_BASE}/combos`, { cache: "no-store" })).json();
1905
+ if (mounted.current) setGroups(data.groups ?? []);
1906
+ } catch {
1907
+ if (mounted.current) setGroups([]);
1908
+ } finally {
1909
+ if (mounted.current) setLoadingGroups(false);
1910
+ }
1911
+ };
1912
+ const showToast = (msg) => {
1913
+ setToast(msg);
1914
+ window.setTimeout(() => {
1915
+ if (mounted.current) setToast(null);
1916
+ }, 2500);
1917
+ };
1918
+ const openCreate = () => {
1919
+ setForm({
1920
+ name: "",
1921
+ strategy: "fallback",
1922
+ models: []
1923
+ });
1924
+ setModelQuery("");
1925
+ setFormError("");
1926
+ setEditing(null);
1927
+ setShowForm(true);
1928
+ loadGroups();
1929
+ };
1930
+ const openEdit = (combo) => {
1931
+ setForm({
1932
+ name: combo.name,
1933
+ strategy: combo.strategy,
1934
+ models: [...combo.models]
1935
+ });
1936
+ setModelQuery("");
1937
+ setFormError("");
1938
+ setEditing(combo);
1939
+ setShowForm(true);
1940
+ loadGroups();
1941
+ };
1942
+ const patchForm = (patch) => {
1943
+ setForm((prev) => ({
1944
+ ...prev,
1945
+ ...patch
1946
+ }));
1947
+ };
1948
+ const toggleModel = (id) => {
1949
+ patchForm({ models: form.models.includes(id) ? form.models.filter((m) => m !== id) : [...form.models, id] });
1950
+ };
1951
+ const handleDragStart = (index) => {
1952
+ setDragIndex(index);
1953
+ };
1954
+ const handleDragOver = (e, index) => {
1955
+ e.preventDefault();
1956
+ if (dragOverIndex !== index) setDragOverIndex(index);
1957
+ };
1958
+ const handleDrop = (index) => {
1959
+ if (dragIndex === null || dragIndex === index) {
1960
+ setDragIndex(null);
1961
+ setDragOverIndex(null);
1962
+ return;
1963
+ }
1964
+ const next = [...form.models];
1965
+ const [moved] = next.splice(dragIndex, 1);
1966
+ if (moved === void 0) {
1967
+ setDragIndex(null);
1968
+ setDragOverIndex(null);
1969
+ return;
1970
+ }
1971
+ next.splice(index, 0, moved);
1972
+ patchForm({ models: next });
1973
+ setDragIndex(null);
1974
+ setDragOverIndex(null);
1975
+ };
1976
+ const removeModel = (index) => {
1977
+ patchForm({ models: form.models.filter((_, i) => i !== index) });
1978
+ };
1979
+ const save = async () => {
1980
+ if (form.name.trim() === "") {
1981
+ setFormError("组合名不能为空");
1982
+ return;
1983
+ }
1984
+ if (form.models.length === 0) {
1985
+ setFormError("至少需要一个模型");
1986
+ return;
1987
+ }
1988
+ setSaving(true);
1989
+ setFormError("");
1990
+ try {
1991
+ const isEdit = editing !== null;
1992
+ const data = await (await fetch(`${ROUTER_API_BASE}/combos/${isEdit ? "update" : "create"}`, {
1993
+ method: "POST",
1994
+ headers: { "Content-Type": "application/json" },
1995
+ body: JSON.stringify(isEdit ? {
1996
+ id: editing.id,
1997
+ name: form.name.trim(),
1998
+ strategy: form.strategy,
1999
+ models: form.models
2000
+ } : {
2001
+ name: form.name.trim(),
2002
+ strategy: form.strategy,
2003
+ models: form.models
2004
+ }),
2005
+ cache: "no-store"
2006
+ })).json();
2007
+ if (data.ok) {
2008
+ setShowForm(false);
2009
+ onRefresh();
2010
+ showToast(isEdit ? "组合已更新" : "组合已创建");
2011
+ } else setFormError(data.error ?? "保存失败");
2012
+ } catch (err) {
2013
+ setFormError(err.message);
2014
+ } finally {
2015
+ setSaving(false);
2016
+ }
2017
+ };
2018
+ const doDelete = async (id) => {
2019
+ const data = await (await fetch(`${ROUTER_API_BASE}/combos/remove`, {
2020
+ method: "POST",
2021
+ headers: { "Content-Type": "application/json" },
2022
+ body: JSON.stringify({ id }),
2023
+ cache: "no-store"
2024
+ })).json();
2025
+ setDeleteTarget(null);
2026
+ if (data.ok) {
2027
+ onRefresh();
2028
+ showToast("组合已删除");
2029
+ } else showToast(data.error ?? "删除失败");
2030
+ };
2031
+ const copyName = async (name, id) => {
2032
+ try {
2033
+ await navigator.clipboard.writeText(name);
2034
+ setCopied(id);
2035
+ window.setTimeout(() => setCopied(null), 1500);
2036
+ } catch {}
2037
+ };
2038
+ const updateStrategy = async (id, strategy) => {
2039
+ const combo = combos.find((c) => c.id === id);
2040
+ if (!combo) return;
2041
+ const data = await (await fetch(`${ROUTER_API_BASE}/combos/update`, {
2042
+ method: "POST",
2043
+ headers: { "Content-Type": "application/json" },
2044
+ body: JSON.stringify({
2045
+ id,
2046
+ name: combo.name,
2047
+ strategy,
2048
+ models: combo.models
2049
+ }),
2050
+ cache: "no-store"
2051
+ })).json();
2052
+ if (data.ok) {
2053
+ onRefresh();
2054
+ showToast(`策略已切换为${strategy === "round-robin" ? "轮询" : "回退"}`);
2055
+ } else showToast(data.error ?? "更新失败");
2056
+ };
2057
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2058
+ className: "dshr-tabBody",
2059
+ children: [
2060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2061
+ className: "dshr-card dshr-comboIntro",
2062
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2063
+ className: "dshr-cardHead",
2064
+ children: [
2065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2066
+ className: "dshr-cardIcon",
2067
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2068
+ d: I.layers,
2069
+ size: 16
2070
+ })
2071
+ }),
2072
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2073
+ className: "dshr-cardTitle",
2074
+ children: "组合"
2075
+ }),
2076
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2077
+ className: "dshr-muted dshr-comboIntroDesc",
2078
+ children: "请求传组合名,在一组模型中按策略命中"
2079
+ }),
2080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2081
+ type: "button",
2082
+ className: "dshr-primaryButton dshr-cardAction",
2083
+ onClick: openCreate,
2084
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2085
+ d: I.add,
2086
+ size: 14
2087
+ }), "创建组合"]
2088
+ })
2089
+ ]
2090
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2091
+ className: "dshr-comboIntroBody",
2092
+ children: [
2093
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2094
+ className: "dshr-comboHeaderKey",
2095
+ children: "回退"
2096
+ }),
2097
+ " 按顺序尝试,前面失败时用下一个",
2098
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2099
+ className: "dshr-comboHeaderKey dshr-comboHeaderKey-2",
2100
+ children: "轮询"
2101
+ }),
2102
+ " 在模型间轮转分配请求"
2103
+ ]
2104
+ })]
2105
+ }),
2106
+ combos.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2107
+ className: "dshr-keyEmpty",
2108
+ children: [
2109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2110
+ className: "dshr-keyEmptyIcon",
2111
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2112
+ d: I.layers,
2113
+ size: 30
2114
+ })
2115
+ }),
2116
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2117
+ className: "dshr-keyEmptyTitle",
2118
+ children: "暂无组合"
2119
+ }),
2120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2121
+ className: "dshr-keyEmptyDesc",
2122
+ children: "创建组合,在一组模型中按策略命中"
2123
+ }),
2124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2125
+ type: "button",
2126
+ className: "dshr-primaryButton",
2127
+ onClick: openCreate,
2128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2129
+ d: I.add,
2130
+ size: 14
2131
+ }), "创建组合"]
2132
+ })
2133
+ ]
2134
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2135
+ className: "dshr-comboList",
2136
+ children: combos.map((combo) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2137
+ className: "dshr-card dshr-comboCard",
2138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2139
+ className: "dshr-cardHead",
2140
+ children: [
2141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2142
+ className: "dshr-cardIcon",
2143
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2144
+ d: I.layers,
2145
+ size: 16
2146
+ })
2147
+ }),
2148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2149
+ className: "dshr-comboName",
2150
+ children: combo.name
2151
+ }),
2152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2153
+ className: "dshr-muted dshr-cardMeta",
2154
+ children: [combo.models.length, " 模型"]
2155
+ }),
2156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2157
+ className: "dshr-comboOps",
2158
+ children: [
2159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2160
+ className: "dshr-input dshr-comboStrategySelect",
2161
+ value: combo.strategy,
2162
+ title: "组合策略",
2163
+ onChange: (e) => void updateStrategy(combo.id, e.target.value),
2164
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2165
+ value: "fallback",
2166
+ children: "回退"
2167
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2168
+ value: "round-robin",
2169
+ children: "轮询"
2170
+ })]
2171
+ }),
2172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2173
+ type: "button",
2174
+ className: "dshr-iconBtn dshr-iconBtn-sm",
2175
+ title: "复制组合名",
2176
+ 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,
2179
+ size: 15
2180
+ })
2181
+ }),
2182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2183
+ type: "button",
2184
+ className: "dshr-iconBtn dshr-iconBtn-sm",
2185
+ title: "编辑",
2186
+ onClick: () => openEdit(combo),
2187
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2188
+ d: I.edit,
2189
+ size: 15
2190
+ })
2191
+ }),
2192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2193
+ type: "button",
2194
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboOpBtn-danger",
2195
+ title: "删除",
2196
+ onClick: () => setDeleteTarget(combo),
2197
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2198
+ d: I.delete,
2199
+ size: 15
2200
+ })
2201
+ })
2202
+ ]
2203
+ })
2204
+ ]
2205
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2206
+ className: "dshr-comboCardBody",
2207
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2208
+ className: "dshr-comboChips",
2209
+ children: combo.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2210
+ className: "dshr-muted dshr-comboEmptySteps",
2211
+ children: "无模型"
2212
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [combo.models.slice(0, 4).map((model, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2213
+ className: "dshr-comboChip",
2214
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2215
+ className: "dshr-comboChipNum",
2216
+ children: i + 1
2217
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2218
+ className: "dshr-comboChipSupplier",
2219
+ children: aliasOf(model) ? `${aliasOf(model)}/${model}` : model
2220
+ })]
2221
+ }, `${model}-${i}`)), combo.models.length > 4 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2222
+ className: "dshr-comboMore",
2223
+ children: [
2224
+ "+",
2225
+ combo.models.length - 4,
2226
+ " more"
2227
+ ]
2228
+ })] })
2229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2230
+ className: "dshr-muted dshr-reason",
2231
+ children: combo.strategy === "fallback" ? "按此顺序尝试模型;当前模型不可用时换下一个。" : "请求在模型间轮转分配。"
2232
+ })]
2233
+ })]
2234
+ }, combo.id))
2235
+ }),
2236
+ showForm && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
2237
+ title: editing !== null ? "编辑组合" : "创建组合",
2238
+ onClose: () => setShowForm(false),
2239
+ className: "dshr-modal-wide",
2240
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2241
+ className: "dshr-modalForm",
2242
+ children: [
2243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2244
+ className: "dshr-comboFormSplit",
2245
+ children: [
2246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2247
+ className: "dshr-comboFormDivider",
2248
+ "aria-hidden": "true"
2249
+ }),
2250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2251
+ className: "dshr-comboFormLeft",
2252
+ children: [
2253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
2254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2255
+ className: "dshr-formLabel",
2256
+ children: "组合名"
2257
+ }),
2258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2259
+ className: "dshr-input dshr-mono",
2260
+ placeholder: "my-combo",
2261
+ value: form.name,
2262
+ onChange: (e) => patchForm({ name: e.target.value }),
2263
+ autoFocus: true
2264
+ }),
2265
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2266
+ className: "dshr-formHint",
2267
+ children: "只能含字母、数字、-、_ 和 ."
2268
+ })
2269
+ ] }),
2270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2271
+ className: "dshr-formLabel",
2272
+ children: "策略"
2273
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2274
+ className: "dshr-input",
2275
+ value: form.strategy,
2276
+ onChange: (e) => patchForm({ strategy: e.target.value }),
2277
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2278
+ value: "fallback",
2279
+ children: "回退 — 按顺序尝试,失败换下一个"
2280
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2281
+ value: "round-robin",
2282
+ children: "轮询 — 在模型间轮转分配"
2283
+ })]
2284
+ })] }),
2285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2286
+ className: "dshr-formLabel",
2287
+ children: [
2288
+ "模型(已选 ",
2289
+ form.models.length,
2290
+ ")"
2291
+ ]
2292
+ }), form.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2293
+ className: "dshr-comboEmptyStepsBox",
2294
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2295
+ d: I.layers,
2296
+ size: 20
2297
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2298
+ className: "dshr-muted",
2299
+ children: "还没有模型"
2300
+ })]
2301
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2302
+ className: "dshr-comboStepList",
2303
+ children: form.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2304
+ className: `dshr-comboStepRow${dragIndex === index ? " dshr-comboStepRow-dragging" : ""}${dragOverIndex === index && dragIndex !== null && dragIndex !== index ? " dshr-comboStepRow-over" : ""}`,
2305
+ draggable: true,
2306
+ onDragStart: () => handleDragStart(index),
2307
+ onDragOver: (e) => handleDragOver(e, index),
2308
+ onDrop: () => handleDrop(index),
2309
+ onDragEnd: () => {
2310
+ setDragIndex(null);
2311
+ setDragOverIndex(null);
2312
+ },
2313
+ children: [
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2315
+ className: "dshr-comboGrip",
2316
+ title: "拖拽排序",
2317
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2318
+ d: I.grip,
2319
+ size: 14
2320
+ })
2321
+ }),
2322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2323
+ className: "dshr-comboStepNum",
2324
+ children: index + 1
2325
+ }),
2326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2327
+ className: "dshr-comboSelectedModel",
2328
+ children: aliasOf(model) ? `${aliasOf(model)}/${model}` : model
2329
+ }),
2330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2331
+ type: "button",
2332
+ className: "dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove",
2333
+ title: "移除",
2334
+ onClick: () => removeModel(index),
2335
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2336
+ d: I.delete,
2337
+ size: 14
2338
+ })
2339
+ })
2340
+ ]
2341
+ }, `${model}-${index}`))
2342
+ })] })
2343
+ ]
2344
+ }),
2345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2346
+ className: "dshr-comboFormRight",
2347
+ children: [
2348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2349
+ className: "dshr-formLabel",
2350
+ children: "添加模型"
2351
+ }),
2352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2353
+ className: "dshr-modelSearch dshr-comboModelSearch",
2354
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2355
+ className: "dshr-modelSearchIcon",
2356
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
2357
+ d: I.search,
2358
+ size: 13
2359
+ })
2360
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2361
+ className: "dshr-input",
2362
+ placeholder: "搜索模型…",
2363
+ value: modelQuery,
2364
+ onChange: (e) => setModelQuery(e.target.value)
2365
+ })]
2366
+ }),
2367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2368
+ className: "dshr-comboPickList",
2369
+ children: loadingGroups ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2370
+ className: "dshr-muted dshr-comboPickEmpty",
2371
+ children: "加载中…"
2372
+ }) : groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2373
+ className: "dshr-muted dshr-comboPickEmpty",
2374
+ children: "没有可添加的模型"
2375
+ }) : groups.map((g) => {
2376
+ const items = g.models.filter((m) => !form.models.includes(m.id) && (modelQuery === "" || m.id.toLowerCase().includes(modelQuery.toLowerCase())));
2377
+ if (items.length === 0) return null;
2378
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2379
+ className: "dshr-comboPickGroup",
2380
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2381
+ className: "dshr-comboPickGroupTitle",
2382
+ children: [
2383
+ g.supplier.name,
2384
+ " · ",
2385
+ g.supplier.alias
2386
+ ]
2387
+ }), items.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2388
+ type: "button",
2389
+ className: "dshr-comboPickItem",
2390
+ onClick: () => toggleModel(m.id),
2391
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2392
+ className: "dshr-comboPickName",
2393
+ children: m.id
2394
+ })
2395
+ }, m.id))]
2396
+ }, g.supplier.id);
2397
+ })
2398
+ })
2399
+ ]
2400
+ })
2401
+ ]
2402
+ }),
2403
+ formError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2404
+ className: "dshr-alert",
2405
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "出错了" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formError })]
2406
+ }),
2407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2408
+ className: "dshr-modalActions",
2409
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2410
+ type: "button",
2411
+ className: "dshr-miniButton",
2412
+ onClick: () => setShowForm(false),
2413
+ children: "取消"
2414
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2415
+ type: "button",
2416
+ className: "dshr-primaryButton",
2417
+ onClick: () => void save(),
2418
+ disabled: saving || form.name.trim() === "" || form.models.length === 0,
2419
+ children: saving ? "保存中…" : editing !== null ? "保存" : "创建"
2420
+ })]
2421
+ })
2422
+ ]
2423
+ })
2424
+ }),
2425
+ deleteTarget !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Modal, {
2426
+ title: "删除组合",
2427
+ onClose: () => setDeleteTarget(null),
2428
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2429
+ className: "dshr-modalForm",
2430
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2431
+ className: "dshr-muted",
2432
+ children: [
2433
+ "确定删除组合「",
2434
+ deleteTarget.name,
2435
+ "」吗?"
2436
+ ]
2437
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2438
+ className: "dshr-modalActions",
2439
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2440
+ type: "button",
2441
+ className: "dshr-miniButton",
2442
+ onClick: () => setDeleteTarget(null),
2443
+ children: "取消"
2444
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2445
+ type: "button",
2446
+ className: "dshr-dangerButton",
2447
+ onClick: () => void doDelete(deleteTarget.id),
2448
+ children: "删除"
2449
+ })]
2450
+ })]
2451
+ })
2452
+ }),
2453
+ toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2454
+ className: "dshr-toast",
2455
+ children: toast
2456
+ })
2457
+ ]
2458
+ });
2459
+ }
2460
+ //#endregion
2461
+ //#region src/client/RouterView.tsx
2462
+ /**
2463
+ * 路由系统 (Routing System) panel — a simplified 9router.
2464
+ *
2465
+ * Three tabs fed by the host half (/router/api/*), mirroring 9router's
2466
+ * dashboard: 供应商 (supplier cards → detail: accounts + add-account + models),
2467
+ * 组合 (combo fallback chains), 端点与密钥 (endpoint URL + API keys + auth).
2468
+ *
2469
+ * Masthead: 返回会话 button on the LEFT, refresh on the right.
2470
+ * No online/offline concept — suppliers render whatever state they have.
2471
+ */
2472
+ async function getJson(path) {
2473
+ const response = await fetch(`${ROUTER_API_BASE}${path}`, { cache: "no-store" });
2474
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
2475
+ return response.json();
2476
+ }
2477
+ function RouterView({ onBack }) {
2478
+ const [tab, setTab] = (0, react.useState)("suppliers");
2479
+ const [detailSupplier, setDetailSupplier] = (0, react.useState)(null);
2480
+ const [snapshot, setSnapshot] = (0, react.useState)({
2481
+ health: null,
2482
+ status: null,
2483
+ combos: null
2484
+ });
2485
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
2486
+ const mounted = (0, react.useRef)(true);
2487
+ const refresh = async () => {
2488
+ if (refreshing) return;
2489
+ setRefreshing(true);
2490
+ try {
2491
+ const [health, status, combos] = await Promise.all([
2492
+ getJson("/health").catch(() => null),
2493
+ getJson("/status").catch(() => null),
2494
+ getJson("/combos").catch(() => null)
2495
+ ]);
2496
+ if (mounted.current) setSnapshot({
2497
+ health,
2498
+ status,
2499
+ combos
2500
+ });
2501
+ } finally {
2502
+ if (mounted.current) setRefreshing(false);
2503
+ }
2504
+ };
2505
+ (0, react.useEffect)(() => {
2506
+ mounted.current = true;
2507
+ refresh();
2508
+ return () => {
2509
+ mounted.current = false;
2510
+ };
2511
+ }, []);
2512
+ const suppliers = snapshot.health?.suppliers ?? [];
2513
+ const accounts = snapshot.status?.accounts ?? [];
2514
+ const combos = snapshot.combos?.combos ?? [];
2515
+ const suppliersError = snapshot.status?.ok === false ? snapshot.status.error : void 0;
2516
+ const combosError = snapshot.combos?.ok === false ? snapshot.combos.error : void 0;
2517
+ /** 渲染一组供应商卡片(内置 / 插件分开)。 */
2518
+ const renderSupplierGroup = (title, group) => {
2519
+ if (group.length === 0) return null;
2520
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2521
+ className: "dshr-supplierGroup",
2522
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2523
+ className: "dshr-supplierGroupTitle",
2524
+ children: title
2525
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2526
+ className: "dshr-supplierGrid",
2527
+ children: group.map((supplier) => {
2528
+ const supplierAccounts = accounts.filter((a) => a.supplier === supplier.id);
2529
+ const healthy = supplierAccounts.filter((a) => !a.cooling && !a.disabled).length;
2530
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2531
+ className: "dshr-supplierCard",
2532
+ onClick: () => setDetailSupplier(supplier),
2533
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2534
+ className: "dshr-supplierRow",
2535
+ children: [supplier.icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2536
+ className: "dshr-supplierIcon",
2537
+ src: supplier.icon,
2538
+ alt: ""
2539
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2540
+ className: "dshr-supplierName",
2541
+ children: supplier.name
2542
+ })]
2543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2544
+ className: "dshr-supplierMeta",
2545
+ children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2546
+ className: "dshr-muted",
2547
+ children: [
2548
+ supplierAccounts.length,
2549
+ "/",
2550
+ healthy,
2551
+ " 健康"
2552
+ ]
2553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2554
+ className: "dshr-chevron",
2555
+ children: "›"
2556
+ })]
2557
+ })]
2558
+ }, supplier.id);
2559
+ })
2560
+ })]
2561
+ });
2562
+ };
2563
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2564
+ className: "dshr-shell",
2565
+ children: [
2566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2567
+ className: "dshr-masthead",
2568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2569
+ type: "button",
2570
+ className: "dshr-backButton",
2571
+ onClick: onBack,
2572
+ title: "返回会话",
2573
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2574
+ viewBox: "0 0 16 16",
2575
+ width: "15",
2576
+ height: "15",
2577
+ fill: "none",
2578
+ stroke: "currentColor",
2579
+ strokeWidth: "1.5",
2580
+ strokeLinecap: "round",
2581
+ strokeLinejoin: "round",
2582
+ "aria-hidden": "true",
2583
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10 3 5 8l5 5" })
2584
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "返回会话" })]
2585
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2586
+ className: "dshr-brand",
2587
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2588
+ className: "dshr-brandTitle",
2589
+ children: ROUTER_TITLE
2590
+ })
2591
+ })]
2592
+ }),
2593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
2594
+ className: "dshr-nav",
2595
+ children: [
2596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2597
+ type: "button",
2598
+ "aria-current": tab === "suppliers" ? "page" : void 0,
2599
+ onClick: () => setTab("suppliers"),
2600
+ children: ["供应商 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2601
+ className: "dshr-navCount",
2602
+ children: suppliers.length
2603
+ })]
2604
+ }),
2605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2606
+ type: "button",
2607
+ "aria-current": tab === "combos" ? "page" : void 0,
2608
+ onClick: () => setTab("combos"),
2609
+ children: ["组合 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2610
+ className: "dshr-navCount",
2611
+ children: combos.length
2612
+ })]
2613
+ }),
2614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2615
+ type: "button",
2616
+ "aria-current": tab === "endpoint" ? "page" : void 0,
2617
+ onClick: () => setTab("endpoint"),
2618
+ children: "端点与密钥"
2619
+ })
2620
+ ]
2621
+ }),
2622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2623
+ className: "dshr-content",
2624
+ children: [
2625
+ tab === "suppliers" && (detailSupplier !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SupplierDetail, {
2626
+ supplier: detailSupplier,
2627
+ accounts,
2628
+ statusLoading: snapshot.status === null,
2629
+ onBack: () => setDetailSupplier(null),
2630
+ onRefresh: () => void refresh()
2631
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2632
+ className: "dshr-tabBody",
2633
+ children: [
2634
+ suppliers.length === 0 && snapshot.health !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2635
+ className: "dshr-empty",
2636
+ children: "暂无供应商"
2637
+ }),
2638
+ renderSupplierGroup("内置", suppliers.filter((s) => s.source !== "external")),
2639
+ renderSupplierGroup("插件", suppliers.filter((s) => s.source === "external")),
2640
+ suppliersError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2641
+ className: "dshr-empty",
2642
+ children: suppliersError
2643
+ })
2644
+ ]
2645
+ })),
2646
+ tab === "combos" && (snapshot.combos === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2647
+ className: "dshr-tabBody",
2648
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2649
+ className: "dshr-empty",
2650
+ children: "加载中…"
2651
+ })
2652
+ }) : combosError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2653
+ className: "dshr-tabBody",
2654
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2655
+ className: "dshr-empty",
2656
+ children: combosError
2657
+ })
2658
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CombosTab, {
2659
+ combos,
2660
+ aliases: snapshot.combos?.aliases ?? [],
2661
+ onRefresh: () => void refresh()
2662
+ })),
2663
+ tab === "endpoint" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EndpointTab, {})
2664
+ ]
2665
+ })
2666
+ ]
2667
+ });
2668
+ }
2669
+ //#endregion
2670
+ //#region src/client/sidebar-entry.ts
2671
+ /**
2672
+ * Mount a self-healing sidebar entry for the 路由系统 (Routing System),
2673
+ * placed directly ABOVE dsh-mnemon's 记忆系统 entry when that is present.
2674
+ *
2675
+ * The DOM pattern mirrors dsh-mnemon's sidebar-entry.ts: an official-style
2676
+ * row under the New Session row, with a MutationObserver keeping it in place
2677
+ * as the shell re-renders. We deliberately order the family selector so the
2678
+ * router entry sits above the memory entry.
2679
+ */
2680
+ /** Other plugin families that occupy the same sidebar region. */
2681
+ var FAMILY_SELECTOR = "[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-mnemon-entry], [data-dsh-router-entry]";
2682
+ /** The entry we want to sit above (dsh-mnemon's 记忆系统), when present. */
2683
+ var ABOVE_TARGET_SELECTOR = "[data-dsh-mnemon-entry]";
2684
+ function sidebarRoot() {
2685
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"], .dshDesktopUpstreamSidebar");
2686
+ if (column === null) return void 0;
2687
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
2688
+ }
2689
+ function newSessionButton(root) {
2690
+ const nested = root.querySelector("button[class*=\"newSession\"]");
2691
+ if (nested !== null) return nested;
2692
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
2693
+ }
2694
+ function createIcon() {
2695
+ const namespace = "http://www.w3.org/2000/svg";
2696
+ const icon = document.createElementNS(namespace, "svg");
2697
+ icon.setAttribute("viewBox", "0 0 16 16");
2698
+ icon.setAttribute("width", "18");
2699
+ icon.setAttribute("height", "18");
2700
+ icon.setAttribute("fill", "none");
2701
+ icon.setAttribute("stroke", "currentColor");
2702
+ icon.setAttribute("stroke-width", "1.5");
2703
+ icon.setAttribute("stroke-linecap", "round");
2704
+ icon.setAttribute("stroke-linejoin", "round");
2705
+ icon.setAttribute("aria-hidden", "true");
2706
+ const c1 = document.createElementNS(namespace, "circle");
2707
+ c1.setAttribute("cx", "3.5");
2708
+ c1.setAttribute("cy", "3.5");
2709
+ c1.setAttribute("r", "1.6");
2710
+ const c2 = document.createElementNS(namespace, "circle");
2711
+ c2.setAttribute("cx", "12.5");
2712
+ c2.setAttribute("cy", "3.5");
2713
+ c2.setAttribute("r", "1.6");
2714
+ const c3 = document.createElementNS(namespace, "circle");
2715
+ c3.setAttribute("cx", "8");
2716
+ c3.setAttribute("cy", "12.5");
2717
+ c3.setAttribute("r", "1.6");
2718
+ const p1 = document.createElementNS(namespace, "path");
2719
+ p1.setAttribute("d", "M5.1 3.5h6.8");
2720
+ const p2 = document.createElementNS(namespace, "path");
2721
+ p2.setAttribute("d", "M3.5 5.1v4.3c0 1.7 2 2.2 3 2.4");
2722
+ const p3 = document.createElementNS(namespace, "path");
2723
+ p3.setAttribute("d", "M12.5 5.1v4.3c0 1.7-2 2.2-3 2.4");
2724
+ icon.append(c1, c2, c3, p1, p2, p3);
2725
+ return icon;
2726
+ }
2727
+ function createEntry(controller) {
2728
+ const entry = document.createElement("button");
2729
+ entry.type = "button";
2730
+ entry.dataset.dshRouterEntry = "";
2731
+ entry.className = "dshr-entry";
2732
+ const icon = document.createElement("span");
2733
+ icon.className = "dshr-entryIcon";
2734
+ icon.append(createIcon());
2735
+ const label = document.createElement("span");
2736
+ label.className = "dshr-entryLabel";
2737
+ entry.append(icon, label);
2738
+ entry.addEventListener("click", () => {
2739
+ controller.toggle();
2740
+ });
2741
+ return {
2742
+ entry,
2743
+ label
2744
+ };
2745
+ }
2746
+ function placeEntry(root, entry) {
2747
+ const button = newSessionButton(root);
2748
+ if (button === void 0) return false;
2749
+ if (entry.parentElement === root) return true;
2750
+ const row = button.closest("[class*=\"logoRow\"]");
2751
+ const base = row !== null && row.parentElement === root ? row : button;
2752
+ const family = Array.from(root.children).filter((element) => element instanceof HTMLElement && element.matches(FAMILY_SELECTOR));
2753
+ const aboveTarget = Array.from(root.children).find((element) => element instanceof HTMLElement && element.matches(ABOVE_TARGET_SELECTOR));
2754
+ let anchor;
2755
+ if (aboveTarget !== void 0) anchor = aboveTarget;
2756
+ else anchor = family.at(-1)?.nextElementSibling ?? base.nextElementSibling;
2757
+ root.insertBefore(entry, anchor);
2758
+ return true;
2759
+ }
2760
+ /** Mount a self-healing 路由系统 entry above the 记忆系统 row. */
2761
+ function mountRouterSidebarEntry(controller) {
2762
+ const { entry, label } = createEntry(controller);
2763
+ let root;
2764
+ let placed = false;
2765
+ const syncLabel = () => {
2766
+ if (entry.getAttribute("aria-label") !== "路由系统") entry.setAttribute("aria-label", ROUTER_TITLE);
2767
+ if (entry.title !== "路由系统") entry.title = ROUTER_TITLE;
2768
+ if (label.textContent !== "路由系统") label.textContent = ROUTER_TITLE;
2769
+ };
2770
+ const rootObserver = new MutationObserver(() => {
2771
+ if (root === void 0 || !root.isConnected) {
2772
+ placed = false;
2773
+ tryPlace();
2774
+ return;
2775
+ }
2776
+ if (!root.contains(entry)) placed = placeEntry(root, entry);
2777
+ });
2778
+ const tryPlace = () => {
2779
+ syncLabel();
2780
+ if (root !== void 0 && !root.isConnected) {
2781
+ rootObserver.disconnect();
2782
+ root = void 0;
2783
+ placed = false;
2784
+ }
2785
+ if (placed && document.body.contains(entry)) return;
2786
+ if (placed) {
2787
+ rootObserver.disconnect();
2788
+ root = void 0;
2789
+ placed = false;
2790
+ }
2791
+ root ?? (root = sidebarRoot());
2792
+ if (root === void 0) return;
2793
+ placed = placeEntry(root, entry);
2794
+ if (placed) rootObserver.observe(root, {
2795
+ childList: true,
2796
+ subtree: true
2797
+ });
2798
+ };
2799
+ const waitObserver = new MutationObserver(tryPlace);
2800
+ waitObserver.observe(document.body, {
2801
+ childList: true,
2802
+ subtree: true
2803
+ });
2804
+ const syncActive = () => {
2805
+ if (controller.getSnapshot().open) entry.dataset.active = "true";
2806
+ else delete entry.dataset.active;
2807
+ };
2808
+ const unsubscribe = controller.subscribe(syncActive);
2809
+ syncActive();
2810
+ tryPlace();
2811
+ return () => {
2812
+ waitObserver.disconnect();
2813
+ rootObserver.disconnect();
2814
+ unsubscribe();
2815
+ entry.remove();
2816
+ };
2817
+ }
2818
+ //#endregion
2819
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/typeof.js
2820
+ function _typeof(o) {
2821
+ "@babel/helpers - typeof";
2822
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
2823
+ return typeof o;
2824
+ } : function(o) {
2825
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
2826
+ }, _typeof(o);
2827
+ }
2828
+ //#endregion
2829
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPrimitive.js
2830
+ function toPrimitive(t, r) {
2831
+ if ("object" != _typeof(t) || !t) return t;
2832
+ var e = t[Symbol.toPrimitive];
2833
+ if (void 0 !== e) {
2834
+ var i = e.call(t, r || "default");
2835
+ if ("object" != _typeof(i)) return i;
2836
+ throw new TypeError("@@toPrimitive must return a primitive value.");
2837
+ }
2838
+ return ("string" === r ? String : Number)(t);
2839
+ }
2840
+ //#endregion
2841
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/toPropertyKey.js
2842
+ function toPropertyKey(t) {
2843
+ var i = toPrimitive(t, "string");
2844
+ return "symbol" == _typeof(i) ? i : i + "";
2845
+ }
2846
+ //#endregion
2847
+ //#region \0@oxc-project+runtime@0.139.0/helpers/esm/defineProperty.js
2848
+ function _defineProperty(e, r, t) {
2849
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
2850
+ value: t,
2851
+ enumerable: !0,
2852
+ configurable: !0,
2853
+ writable: !0
2854
+ }) : e[r] = t, e;
2855
+ }
2856
+ //#endregion
2857
+ //#region src/client/workspace-controller.ts
2858
+ /** Small framework-neutral state holder shared by the sidebar row and panel. */
2859
+ var RouterWorkspaceController = class {
2860
+ constructor() {
2861
+ _defineProperty(this, "snapshot", { open: false });
2862
+ _defineProperty(this, "listeners", /* @__PURE__ */ new Set());
2863
+ _defineProperty(this, "getSnapshot", () => this.snapshot);
2864
+ _defineProperty(this, "subscribe", (listener) => {
2865
+ this.listeners.add(listener);
2866
+ return () => {
2867
+ this.listeners.delete(listener);
2868
+ };
2869
+ });
2870
+ }
2871
+ open() {
2872
+ this.setOpen(true);
2873
+ }
2874
+ close() {
2875
+ this.setOpen(false);
2876
+ }
2877
+ toggle() {
2878
+ this.setOpen(!this.snapshot.open);
2879
+ }
2880
+ setOpen(open) {
2881
+ if (this.snapshot.open === open) return;
2882
+ this.snapshot = { open };
2883
+ for (const listener of this.listeners) listener();
2884
+ }
2885
+ };
2886
+ //#endregion
2887
+ //#region src/client/workspace-mount.tsx
2888
+ /**
2889
+ * Mount the 路由系统 center-column takeover panel, mirroring dsh-mnemon's
2890
+ * workspace-mount.tsx: a React root appended to the conversation column,
2891
+ * shown via `data-dsh-router-active` on <html>, and coordinated with the
2892
+ * other sidebar panels (taskboard / ssh / mnemon) through the shared
2893
+ * `dsh-panel-activate` event.
2894
+ */
2895
+ var CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"], .dshDesktopConversationSurface";
2896
+ var ACTIVATE_EVENT = "dsh-panel-activate";
2897
+ var SIDEBAR_CONTEXT_SELECTOR = "[class*=\"sessionRow\"], [class*=\"projectRow\"], [class*=\"searchResultRow\"], [class*=\"searchResultWorkspace\"], [class*=\"newSession\"]";
2898
+ function conversationColumn() {
2899
+ return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
2900
+ }
2901
+ function mountPanel(controller) {
2902
+ 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
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RouterView, { onBack: () => controller.close() });
2911
+ }
2912
+ let root;
2913
+ let container;
2914
+ const ensure = () => {
2915
+ if (container !== void 0 && container.isConnected) return;
2916
+ if (container !== void 0) {
2917
+ root?.unmount();
2918
+ root = void 0;
2919
+ container = void 0;
2920
+ }
2921
+ const column = conversationColumn();
2922
+ if (column === void 0) return;
2923
+ container = document.createElement("div");
2924
+ container.dataset.dshRouterView = "";
2925
+ container.className = "dshr-panelView";
2926
+ column.append(container);
2927
+ root = (0, react_dom_client.createRoot)(container);
2928
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Panel, {}));
2929
+ };
2930
+ const waitObserver = new MutationObserver(ensure);
2931
+ waitObserver.observe(document.body, {
2932
+ childList: true,
2933
+ subtree: true
2934
+ });
2935
+ let suppressCompatibilityClose = false;
2936
+ const applyActive = () => {
2937
+ if (!controller.getSnapshot().open) {
2938
+ document.documentElement.removeAttribute(ROUTER_ACTIVE_ATTR);
2939
+ return;
2940
+ }
2941
+ suppressCompatibilityClose = true;
2942
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "ssh" }));
2943
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "taskboard" }));
2944
+ suppressCompatibilityClose = false;
2945
+ document.documentElement.setAttribute(ROUTER_ACTIVE_ATTR, "");
2946
+ document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: "router" }));
2947
+ };
2948
+ const onOtherPanelActivate = (event) => {
2949
+ if (suppressCompatibilityClose || !controller.getSnapshot().open) return;
2950
+ const detail = event.detail;
2951
+ if (detail === "taskboard" || detail === "ssh" || detail === "mnemon") controller.close();
2952
+ };
2953
+ const onSidebarContextClick = (event) => {
2954
+ if (!controller.getSnapshot().open) return;
2955
+ const target = event.target;
2956
+ if (target instanceof Element && target.closest(SIDEBAR_CONTEXT_SELECTOR) !== null) controller.close();
2957
+ };
2958
+ document.addEventListener("click", onSidebarContextClick, true);
2959
+ document.addEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
2960
+ const unsubscribe = controller.subscribe(applyActive);
2961
+ applyActive();
2962
+ ensure();
2963
+ return () => {
2964
+ document.removeEventListener("click", onSidebarContextClick, true);
2965
+ document.removeEventListener(ACTIVATE_EVENT, onOtherPanelActivate);
2966
+ waitObserver.disconnect();
2967
+ unsubscribe();
2968
+ document.documentElement.removeAttribute(ROUTER_ACTIVE_ATTR);
2969
+ root?.unmount();
2970
+ root = void 0;
2971
+ container?.remove();
2972
+ container = void 0;
2973
+ };
2974
+ }
2975
+ /** Mount the sidebar row and its stateful center-column workspace as one unit. */
2976
+ function mountRouterWorkspace() {
2977
+ if (typeof document === "undefined" || typeof window === "undefined") return () => {};
2978
+ const controller = new RouterWorkspaceController();
2979
+ const disposeEntry = mountRouterSidebarEntry(controller);
2980
+ const disposePanel = mountPanel(controller);
2981
+ return () => {
2982
+ disposePanel();
2983
+ disposeEntry();
2984
+ };
2985
+ }
2986
+ //#endregion
2987
+ //#region src/client/index.tsx
2988
+ /**
2989
+ * Client half of dsh-router: mounts the 路由系统 sidebar entry and its
2990
+ * center-column panel. Pure DOM + fetch — no injected DSH services needed:
2991
+ * the panel reads /router/api/* (same-origin), which the host half serves.
2992
+ */
2993
+ /** No hard service dependencies; the web shell injects react/cordis. */
2994
+ var inject = [];
2995
+ /** Mount the routing-system workspace once the browser context is ready. */
2996
+ function apply(ctx) {
2997
+ ctx.effect(() => mountRouterWorkspace(), "dsh-router: routing system workspace");
2998
+ }
2999
+ //#endregion
3000
+ exports.apply = apply;
3001
+ exports.inject = inject;
3002
+ return module.exports;
3003
+ }
3004
+ });
3005
+
3006
+ //# sourceMappingURL=client.js.map