dsh-router-core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +58 -13
  2. package/lib/client-registry.js +824 -153
  3. package/lib/client.js +824 -153
  4. package/lib/index.js +1606 -227
  5. package/lib/types/llm/adapter.d.ts +2 -0
  6. package/lib/types/router/account-pool.d.ts +41 -0
  7. package/lib/types/router/index.d.ts +72 -4
  8. package/lib/types/router/types.d.ts +12 -10
  9. package/lib/types/router/usage-store.d.ts +93 -0
  10. package/lib/types/router/usage-tokens.d.ts +118 -0
  11. package/lib/types/shared.d.ts +69 -0
  12. package/lib/types/supplier-config.d.ts +15 -4
  13. package/lib/types/suppliers/contract.d.ts +70 -21
  14. package/lib/types/suppliers/registry.d.ts +4 -3
  15. package/package.json +2 -1
  16. package/src/client/CombosTab.tsx +28 -16
  17. package/src/client/RouterView.tsx +15 -2
  18. package/src/client/StatsTab.tsx +521 -0
  19. package/src/client/SupplierDetail.tsx +58 -34
  20. package/src/client/router.css +406 -0
  21. package/src/client/workspace-mount.tsx +2 -9
  22. package/src/index.ts +52 -4
  23. package/src/llm/adapter.test.ts +108 -0
  24. package/src/llm/adapter.ts +18 -6
  25. package/src/router/account-pool.test.ts +120 -0
  26. package/src/router/account-pool.ts +161 -0
  27. package/src/router/index.ts +652 -26
  28. package/src/router/models-cache.test.ts +455 -0
  29. package/src/router/precise-dispatch.test.ts +198 -0
  30. package/src/router/types.ts +12 -7
  31. package/src/router/usage-store.ts +499 -0
  32. package/src/router/usage-tokens.ts +327 -0
  33. package/src/router/usage.test.ts +558 -0
  34. package/src/shared.ts +77 -0
  35. package/src/supplier-config.ts +31 -14
  36. package/src/suppliers/contract.ts +71 -8
  37. package/src/suppliers/loader.ts +21 -13
  38. package/src/suppliers/nvidia/plugin.ts +161 -119
  39. package/src/suppliers/opencode/plugin.ts +47 -95
  40. package/src/suppliers/openrouter/plugin.ts +67 -122
  41. package/src/suppliers/registry.test.ts +194 -0
  42. package/src/suppliers/registry.ts +79 -59
@@ -17,10 +17,11 @@
17
17
  * POST /suppliers/:id/login 生成登录链接
18
18
  * POST /suppliers/:id/login/callback {callbackUrl}
19
19
  * POST /suppliers/:id/links/remove {uid}
20
- * GET /suppliers/:id/checkin/rule + POST {rule}
21
- * POST /suppliers/:id/checkin 触发签到
20
+ * POST /suppliers/:id/links/refresh 刷新链接池(积分,核心调 status() 等落地)
21
+ * POST /suppliers/:id/checkin 签到所有链接(核心遍历 + 汇总)
22
22
  */
23
23
  import type { IncomingMessage, ServerResponse } from 'node:http';
24
+ import type { Router } from '../router/index.ts';
24
25
  import type { SupplierConfigStore } from '../supplier-config.ts';
25
26
  import type { LoadedSupplier } from './loader.ts';
26
27
  /** webServer 路由形状(与 index.ts 的 WebServerRoute 一致)。 */
@@ -30,4 +31,4 @@ export interface WebServerRoute {
30
31
  handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
31
32
  }
32
33
  /** 为一个供应商生成端点(通用 + 差异化,返回 route 列表)。 */
33
- export declare function supplierRoutes(base: string, loaded: LoadedSupplier, store: SupplierConfigStore): WebServerRoute[];
34
+ export declare function supplierRoutes(base: string, loaded: LoadedSupplier, store: SupplierConfigStore, router: Router): WebServerRoute[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-router-core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -52,6 +52,7 @@
52
52
  "build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown && npm run build:client",
53
53
  "build:client": "DSH_CLIENT_ID=dsh-router DSH_CLIENT_FILE=client.js vite build -c client/vite.config.ts && DSH_CLIENT_ID=dsh-external/dsh-router DSH_CLIENT_FILE=client-registry.js vite build -c client/vite.config.ts",
54
54
  "typecheck": "tsc --noEmit",
55
+ "test": "node --test \"src/**/*.test.ts\"",
55
56
  "bundle": "tsdown",
56
57
  "watch": "tsdown --watch"
57
58
  },
@@ -49,15 +49,25 @@ interface ComboForm {
49
49
  }
50
50
 
51
51
  export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.Element {
52
- /** 当前供应商前缀(单供应商取第一个)。 */
53
- const primaryAlias = aliases[0]?.alias ?? ''
54
-
55
- /** 模型 → 所属供应商前缀(按 groups 分组匹配;找不到兜底 primaryAlias)。 */
56
- const aliasOf = (model: string): string => {
57
- for (const g of groups) {
58
- if (g.models.some(m => m.id === model)) return g.supplier.alias
52
+ /**
53
+ * 存储值 → 展示全名 alias/model。
54
+ * 存储格式 = `supplierId,modelId`:精准,同名模型不会串台。
55
+ * 兼容旧数据的裸 modelId:查不到归属就显示裸名——不能兜底套第一个供应商的
56
+ * 前缀,那样前缀是假的。
57
+ */
58
+ const displayName = (stored: string): string => {
59
+ const comma = stored.indexOf(',')
60
+ if (comma > 0) {
61
+ const sid = stored.slice(0, comma)
62
+ const mid = stored.slice(comma + 1)
63
+ const g = groups.find(x => x.supplier.id === sid)
64
+ const alias = g?.supplier.alias ?? aliases.find(a => a.id === sid)?.alias ?? ''
65
+ return alias === '' ? mid : `${alias}/${mid}`
59
66
  }
60
- return primaryAlias
67
+ // 旧数据裸 id:按分组找归属(优先已启用的),找不到显示裸名
68
+ const hit = groups.find(g => g.models.some(m => m.id === stored && m.enabled))
69
+ ?? groups.find(g => g.models.some(m => m.id === stored))
70
+ return hit === undefined ? stored : `${hit.supplier.alias}/${stored}`
61
71
  }
62
72
  const [showForm, setShowForm] = useState(false)
63
73
  const [editing, setEditing] = useState<RouterCombo | null>(null)
@@ -132,11 +142,13 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
132
142
  setForm(prev => ({ ...prev, ...patch }))
133
143
  }
134
144
 
135
- const toggleModel = (id: string): void => {
145
+ /** 勾选:存 `supplierId,modelId`(精准归属,展示时才拼 alias)。 */
146
+ const toggleModel = (supplierId: string, id: string): void => {
147
+ const stored = `${supplierId},${id}`
136
148
  patchForm({
137
- models: form.models.includes(id)
138
- ? form.models.filter(m => m !== id)
139
- : [...form.models, id],
149
+ models: form.models.includes(stored)
150
+ ? form.models.filter(m => m !== stored)
151
+ : [...form.models, stored],
140
152
  })
141
153
  }
142
154
 
@@ -311,7 +323,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
311
323
  {combo.models.slice(0, 4).map((model, i) => (
312
324
  <span key={`${model}-${i}`} className="dshr-comboChip">
313
325
  <span className="dshr-comboChipNum">{i + 1}</span>
314
- <span className="dshr-comboChipSupplier">{aliasOf(model) ? `${aliasOf(model)}/${model}` : model}</span>
326
+ <span className="dshr-comboChipSupplier">{displayName(model)}</span>
315
327
  </span>
316
328
  ))}
317
329
  {combo.models.length > 4 && (
@@ -382,7 +394,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
382
394
  >
383
395
  <span className="dshr-comboGrip" title="拖拽排序"><Icon d={I.grip} size={14} /></span>
384
396
  <span className="dshr-comboStepNum">{index + 1}</span>
385
- <code className="dshr-comboSelectedModel">{aliasOf(model) ? `${aliasOf(model)}/${model}` : model}</code>
397
+ <code className="dshr-comboSelectedModel">{displayName(model)}</code>
386
398
  <button type="button" className="dshr-iconBtn dshr-iconBtn-sm dshr-comboStepRemove" title="移除" onClick={() => removeModel(index)}>
387
399
  <Icon d={I.delete} size={14} />
388
400
  </button>
@@ -411,14 +423,14 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
411
423
  ? <p className="dshr-muted dshr-comboPickEmpty">没有可添加的模型</p>
412
424
  : groups.map(g => {
413
425
  const items = g.models.filter(m =>
414
- !form.models.includes(m.id) &&
426
+ !form.models.includes(`${g.supplier.id},${m.id}`) &&
415
427
  (modelQuery === '' || m.id.toLowerCase().includes(modelQuery.toLowerCase())))
416
428
  if (items.length === 0) return null
417
429
  return (
418
430
  <div key={g.supplier.id} className="dshr-comboPickGroup">
419
431
  <p className="dshr-comboPickGroupTitle">{g.supplier.name} · {g.supplier.alias}</p>
420
432
  {items.map(m => (
421
- <button key={m.id} type="button" className="dshr-comboPickItem" onClick={() => toggleModel(m.id)}>
433
+ <button key={m.id} type="button" className="dshr-comboPickItem" onClick={() => toggleModel(g.supplier.id, m.id)}>
422
434
  <span className="dshr-comboPickName">{m.id}</span>
423
435
  </button>
424
436
  ))}
@@ -19,8 +19,9 @@ import {
19
19
  import { SupplierDetail } from './SupplierDetail.tsx'
20
20
  import { EndpointTab } from './EndpointTab.tsx'
21
21
  import { CombosTab } from './CombosTab.tsx'
22
+ import { StatsTab } from './StatsTab.tsx'
22
23
 
23
- type TabId = 'suppliers' | 'combos' | 'endpoint'
24
+ type TabId = 'overview' | 'suppliers' | 'combos' | 'endpoint'
24
25
 
25
26
  interface Snapshot {
26
27
  health: RouterHealthResponse | null
@@ -42,7 +43,7 @@ interface RouterViewProps {
42
43
  }
43
44
 
44
45
  export function RouterView({ onBack }: RouterViewProps): JSX.Element {
45
- const [tab, setTab] = useState<TabId>('suppliers')
46
+ const [tab, setTab] = useState<TabId>('overview')
46
47
  const [detailSupplier, setDetailSupplier] = useState<{ id: string; name: string; icon?: string } | null>(null)
47
48
  const [snapshot, setSnapshot] = useState<Snapshot>({ health: null, status: null, combos: null })
48
49
  const [refreshing, setRefreshing] = useState(false)
@@ -125,6 +126,9 @@ export function RouterView({ onBack }: RouterViewProps): JSX.Element {
125
126
  </header>
126
127
 
127
128
  <nav className="dshr-nav">
129
+ <button type="button" aria-current={tab === 'overview' ? 'page' : undefined} onClick={() => setTab('overview')}>
130
+ 概览
131
+ </button>
128
132
  <button type="button" aria-current={tab === 'suppliers' ? 'page' : undefined} onClick={() => setTab('suppliers')}>
129
133
  供应商 <span className="dshr-navCount">{suppliers.length}</span>
130
134
  </button>
@@ -137,6 +141,15 @@ export function RouterView({ onBack }: RouterViewProps): JSX.Element {
137
141
  </nav>
138
142
 
139
143
  <div className="dshr-content">
144
+ {/* ---------------- 概览(用量看板) ---------------- */}
145
+ {tab === 'overview' && (
146
+ <StatsTab
147
+ active
148
+ refreshing={refreshing}
149
+ onRefresh={() => void refresh()}
150
+ />
151
+ )}
152
+
140
153
  {/* ---------------- 供应商 ---------------- */}
141
154
  {tab === 'suppliers' && (
142
155
  detailSupplier !== null