dsh-router-core 0.3.21 → 0.4.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 (49) hide show
  1. package/README.md +77 -34
  2. package/docs/pool-sticky-block.md +158 -0
  3. package/lib/client-registry.js +351 -407
  4. package/lib/client.js +351 -407
  5. package/lib/index.js +383 -170
  6. package/lib/types/ext/contract.d.ts +0 -7
  7. package/lib/types/host-version.d.ts +15 -0
  8. package/lib/types/index.d.ts +6 -0
  9. package/lib/types/router/account-pool.d.ts +53 -18
  10. package/lib/types/router/index.d.ts +4 -7
  11. package/lib/types/router/prefix-affinity.d.ts +18 -1
  12. package/lib/types/router/types.d.ts +7 -4
  13. package/lib/types/router/usage-store.d.ts +12 -0
  14. package/lib/types/shared.d.ts +13 -21
  15. package/lib/types/supplier-config.d.ts +0 -3
  16. package/lib/types/suppliers/contract.d.ts +2 -2
  17. package/lib/types/suppliers/registry.d.ts +0 -1
  18. package/package.json +12 -13
  19. package/src/client/CombosTab.tsx +8 -53
  20. package/src/client/ExtDetail.tsx +15 -178
  21. package/src/client/LastHitDock.tsx +141 -0
  22. package/src/client/StatsTab.tsx +8 -6
  23. package/src/client/SupplierDetail.tsx +2 -34
  24. package/src/client/index.tsx +45 -0
  25. package/src/client/last-hit-store.ts +89 -0
  26. package/src/client/model-hint-copy.test.ts +7 -2
  27. package/src/client/model-hint-copy.ts +2 -2
  28. package/src/client/router.css +2106 -1630
  29. package/src/ext/contract.ts +0 -7
  30. package/src/host-version.test.ts +61 -0
  31. package/src/host-version.ts +101 -0
  32. package/src/index.ts +133 -59
  33. package/src/llm/adapter.test.ts +74 -0
  34. package/src/llm/adapter.ts +33 -0
  35. package/src/router/account-pool.test.ts +189 -83
  36. package/src/router/account-pool.ts +120 -31
  37. package/src/router/index.ts +22 -28
  38. package/src/router/models-cache.test.ts +9 -9
  39. package/src/router/precise-dispatch.test.ts +6 -6
  40. package/src/router/prefix-affinity.ts +22 -1
  41. package/src/router/types.ts +7 -5
  42. package/src/router/usage-store.ts +16 -0
  43. package/src/router/usage.test.ts +1 -1
  44. package/src/shared.ts +13 -24
  45. package/src/supplier-config.ts +2 -13
  46. package/src/suppliers/contract.ts +2 -2
  47. package/src/suppliers/loader.test.ts +2 -2
  48. package/src/suppliers/registry.ts +1 -20
  49. package/src/types.d.ts +2 -1
package/src/shared.ts CHANGED
@@ -64,6 +64,10 @@ export interface RouterCheckinResponse {
64
64
  /** `/router/api/health` response. */
65
65
  export interface RouterHealthResponse {
66
66
  ok: boolean
67
+ /** 宿主 DSH 版本(读不到为空串)。 */
68
+ hostVersion?: string
69
+ /** 「最近命中」徽章是否受支持(宿主 >= 0.1.7 才有 composer.dock 正确位置)。 */
70
+ lastHitDock?: boolean
67
71
  suppliers?: Array<{
68
72
  id: string
69
73
  name: string
@@ -79,14 +83,11 @@ export interface RouterHealthResponse {
79
83
  error?: string
80
84
  }
81
85
 
82
- /** 组合策略。 */
83
- export type RouterComboStrategy = 'fallback' | 'round-robin'
84
-
85
- /** 组合。models = 该组合命中的模型列表(裸模型 id,展示时动态拼前缀)。 */
86
+ /** 组合。models = 该组合命中的模型列表(裸模型 id,展示时动态拼前缀)。
87
+ * 路由时作为降级链按序尝试,无策略字段。 */
86
88
  export interface RouterCombo {
87
89
  id: string
88
90
  name: string
89
- strategy: RouterComboStrategy
90
91
  models: string[]
91
92
  }
92
93
 
@@ -160,25 +161,6 @@ export interface RouterExtResponse {
160
161
  enhancers?: RouterExtItem[]
161
162
  }
162
163
 
163
- /** 一行 watch 进程(`/router/api/ext/watch/processes`)。 */
164
- export interface RouterWatchProcess {
165
- pid: number
166
- ppid: number
167
- /** %cpu 快照 */ cpu: number
168
- /** %mem 快照 */ mem: number
169
- command: string
170
- /** 命中的已记录 agent 命令。 */
171
- match: string
172
- recordedAt: number
173
- }
174
-
175
- /** `/router/api/ext/{id}/processes` response。 */
176
- export interface RouterWatchResponse {
177
- ok: boolean
178
- error?: string
179
- processes?: RouterWatchProcess[]
180
- }
181
-
182
164
  /** 供应商模型(含启用状态)。 */
183
165
  export interface RouterSupplierModel {
184
166
  id: string
@@ -227,6 +209,13 @@ export interface RouterUsageRecord {
227
209
  durationMs: number
228
210
  /** 首字节延迟(ms);非流式为 0。 */
229
211
  ttfbMs: number
212
+ /**
213
+ * 连接名(uid → 显示别名,后端解析好)。空串 = 无连接(失败/未服务)。
214
+ * 「最近请求」的「连接」列用它,而不是 supplier。
215
+ */
216
+ connection?: string
217
+ /** 实际服务的连接 uid(成功且有账号时)。 */
218
+ uid?: string
230
219
  error?: string
231
220
  }
232
221
 
@@ -4,7 +4,7 @@
4
4
  * 通用能力(所有供应商共享,供应商 js 不需要实现):
5
5
  * - 别名 alias(前缀,模型全名 = alias/id)
6
6
  * - 模型管理:启用/禁用 disabled、自定义 custom
7
- * - 连接池:poolOrder(拖动排序)、poolStrategy(回退/轮询)
7
+ * - 连接池:poolOrder(拖动排序) —— 选号本身是会话亲和,无策略旋钮
8
8
  * - 积分缓存 credits:按 supplier/uid 缓存最后一次拿到的剩余额度
9
9
  *
10
10
  * 持久化到 data/supplier-config.json。
@@ -12,8 +12,6 @@
12
12
  import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs'
13
13
  import { dirname, join } from 'node:path'
14
14
 
15
- export type PoolStrategy = 'fallback' | 'round-robin'
16
-
17
15
  /**
18
16
  * 积分哨兵:插件还**没拿到过**真实额度(刚重启、积分拉取失败、或压根不支持
19
17
  * 积分)时报 `-1`,核心据此保留上一次的缓存值,而不是把 0 写进去。
@@ -29,7 +27,6 @@ export interface SupplierConfig {
29
27
  disabled: string[]
30
28
  custom: string[]
31
29
  poolOrder: string[]
32
- poolStrategy: PoolStrategy
33
30
  /** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
34
31
  credits: Record<string, number>
35
32
  /** 每连接显示别名:uid → 显示名(空/缺省用供应商原始昵称,纯展示不改路由键)。 */
@@ -90,7 +87,7 @@ export class SupplierConfigStore {
90
87
  get(supplierId: string): SupplierConfig {
91
88
  let cfg = this.bySupplier.get(supplierId)
92
89
  if (!cfg) {
93
- cfg = { alias: '', disabled: [], custom: [], poolOrder: [], poolStrategy: 'fallback', credits: {}, accountNames: {} }
90
+ cfg = { alias: '', disabled: [], custom: [], poolOrder: [], credits: {}, accountNames: {} }
94
91
  this.bySupplier.set(supplierId, cfg)
95
92
  }
96
93
  return cfg
@@ -145,12 +142,6 @@ export class SupplierConfigStore {
145
142
  this.saveLocked()
146
143
  }
147
144
 
148
- setPoolStrategy(supplierId: string, strategy: string): void {
149
- if (strategy !== 'fallback' && strategy !== 'round-robin') return
150
- this.get(supplierId).poolStrategy = strategy
151
- this.saveLocked()
152
- }
153
-
154
145
  // ---- 每连接显示别名(连接池改名,纯展示;uid 仍是路由/匹配键) ----
155
146
 
156
147
  /** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
@@ -221,7 +212,6 @@ export class SupplierConfigStore {
221
212
  disabled: Array.isArray(c.disabled) ? c.disabled.filter((m) => typeof m === 'string') : [],
222
213
  custom: Array.isArray(c.custom) ? c.custom.filter((m) => typeof m === 'string') : [],
223
214
  poolOrder: Array.isArray(c.poolOrder) ? c.poolOrder.filter((u) => typeof u === 'string') : [],
224
- poolStrategy: c.poolStrategy === 'round-robin' ? 'round-robin' : 'fallback',
225
215
  credits: readCredits(c.credits),
226
216
  accountNames: readNames(c.accountNames),
227
217
  })
@@ -237,7 +227,6 @@ export class SupplierConfigStore {
237
227
  disabled: [...cfg.disabled],
238
228
  custom: [...cfg.custom],
239
229
  poolOrder: [...cfg.poolOrder],
240
- poolStrategy: cfg.poolStrategy,
241
230
  credits: { ...cfg.credits },
242
231
  accountNames: { ...cfg.accountNames },
243
232
  }
@@ -32,8 +32,8 @@ export interface SupplierEnv {
32
32
  *
33
33
  * 核心实现 = `pool.noteFailure(uid, model, state, message)`,按该状态的规则
34
34
  * 冷却。真实案例:TRAE 免费通道对某账号拒绝某模型时发 HTTP 200 +
35
- * 流内 `code=4008`,三个号里两个如此,round-robin 下 2/3 请求直接失败
36
- * (2026-09-08 实测)——有了这条通道,坏号被冷一次就退出轮转。
35
+ * 流内 `code=4008`,三个号里两个如此,选号一旦落到坏号就 2/3 请求直接失败
36
+ * (2026-09-08 实测)——有了这条通道,坏号被冷一次就退出选号。
37
37
  *
38
38
  * 插件拿不到也不用兜底:不调用就退化成今天的行为,不影响正确性。
39
39
  */
@@ -159,8 +159,8 @@ test('onLateFailure 挂到 env 上,且冷却作用于本供应商的池(坏
159
159
  assert.equal(after.get('u1'), true, '上报后该号应进入冷却')
160
160
  assert.equal(after.get('u2'), false, '冷却是 (模型,号) 粒度,不能连坐同供应商其它号')
161
161
  // 且真的退出选号:只剩 u2
162
- assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'fallback', 'm1'), 'u2')
163
- assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'fallback', 'm2'), 'u1', '换个模型不受影响')
162
+ assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'm1'), 'u2')
163
+ assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], 'm2'), 'u1', '换个模型不受影响')
164
164
  })
165
165
 
166
166
  test('onLateFailure 不串供应商:各自落到自己的池', () => {
@@ -9,7 +9,6 @@
9
9
  * POST /suppliers/:id/models/bulk {enabled}
10
10
  * POST /suppliers/:id/alias {alias}
11
11
  * GET /suppliers/:id/pool/order + POST {uids}
12
- * GET /suppliers/:id/pool/strategy + POST {strategy}
13
12
  *
14
13
  * 差异化能力(供应商 js 实现,按存在性注册;未实现返回 404):
15
14
  * POST /suppliers/:id/models/fetch 拉取上游模型
@@ -205,7 +204,7 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
205
204
  },
206
205
  })
207
206
 
208
- // ---- 通用: pool/order + pool/strategy ----
207
+ // ---- 通用: pool/order ----
209
208
  routes.push({
210
209
  kind: 'exact',
211
210
  path: `${p}/pool/order`,
@@ -224,24 +223,6 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
224
223
  },
225
224
  })
226
225
 
227
- routes.push({
228
- kind: 'exact',
229
- path: `${p}/pool/strategy`,
230
- handler: async (req, res) => {
231
- if (req.method === 'GET') {
232
- writeJson(res, 200, { ok: true, strategy: store.get(s.id).poolStrategy })
233
- return
234
- }
235
- const body = JSON.parse(await readBody(req)) as { strategy?: string }
236
- if (body.strategy !== 'fallback' && body.strategy !== 'round-robin') {
237
- writeJson(res, 400, { ok: false, error: '策略无效' })
238
- return
239
- }
240
- store.setPoolStrategy(s.id, body.strategy)
241
- writeJson(res, 200, { ok: true })
242
- },
243
- })
244
-
245
226
  // ---- 通用 UI: 获取模型(= 强制刷新 + 核心缓存更新) ----
246
227
  routes.push({
247
228
  kind: 'exact',
package/src/types.d.ts CHANGED
@@ -22,7 +22,7 @@ declare module '@deepseek-ai/dsh-settings' {
22
22
  }
23
23
 
24
24
  declare module '@deepseek-ai/schemastery' {
25
- interface Schema<T> {
25
+ export interface Schema<T> {
26
26
  default(value: T): Schema<T>
27
27
  optional(): Schema<T | undefined>
28
28
  required(): Schema<T>
@@ -30,6 +30,7 @@ declare module '@deepseek-ai/schemastery' {
30
30
  min(n: number): Schema<T>
31
31
  max(n: number): Schema<T>
32
32
  role(name: string): Schema<T>
33
+ volatile(): Schema<T>
33
34
  }
34
35
  interface Z {
35
36
  string(): Schema<string>