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.
- package/README.md +77 -34
- package/docs/pool-sticky-block.md +158 -0
- package/lib/client-registry.js +351 -407
- package/lib/client.js +351 -407
- package/lib/index.js +383 -170
- package/lib/types/ext/contract.d.ts +0 -7
- package/lib/types/host-version.d.ts +15 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/router/account-pool.d.ts +53 -18
- package/lib/types/router/index.d.ts +4 -7
- package/lib/types/router/prefix-affinity.d.ts +18 -1
- package/lib/types/router/types.d.ts +7 -4
- package/lib/types/router/usage-store.d.ts +12 -0
- package/lib/types/shared.d.ts +13 -21
- package/lib/types/supplier-config.d.ts +0 -3
- package/lib/types/suppliers/contract.d.ts +2 -2
- package/lib/types/suppliers/registry.d.ts +0 -1
- package/package.json +12 -13
- package/src/client/CombosTab.tsx +8 -53
- package/src/client/ExtDetail.tsx +15 -178
- package/src/client/LastHitDock.tsx +141 -0
- package/src/client/StatsTab.tsx +8 -6
- package/src/client/SupplierDetail.tsx +2 -34
- package/src/client/index.tsx +45 -0
- package/src/client/last-hit-store.ts +89 -0
- package/src/client/model-hint-copy.test.ts +7 -2
- package/src/client/model-hint-copy.ts +2 -2
- package/src/client/router.css +2106 -1630
- package/src/ext/contract.ts +0 -7
- package/src/host-version.test.ts +61 -0
- package/src/host-version.ts +101 -0
- package/src/index.ts +133 -59
- package/src/llm/adapter.test.ts +74 -0
- package/src/llm/adapter.ts +33 -0
- package/src/router/account-pool.test.ts +189 -83
- package/src/router/account-pool.ts +120 -31
- package/src/router/index.ts +22 -28
- package/src/router/models-cache.test.ts +9 -9
- package/src/router/precise-dispatch.test.ts +6 -6
- package/src/router/prefix-affinity.ts +22 -1
- package/src/router/types.ts +7 -5
- package/src/router/usage-store.ts +16 -0
- package/src/router/usage.test.ts +1 -1
- package/src/shared.ts +13 -24
- package/src/supplier-config.ts +2 -13
- package/src/suppliers/contract.ts +2 -2
- package/src/suppliers/loader.test.ts +2 -2
- package/src/suppliers/registry.ts +1 -20
- 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
|
-
|
|
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
|
|
package/src/supplier-config.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 通用能力(所有供应商共享,供应商 js 不需要实现):
|
|
5
5
|
* - 别名 alias(前缀,模型全名 = alias/id)
|
|
6
6
|
* - 模型管理:启用/禁用 disabled、自定义 custom
|
|
7
|
-
* - 连接池:poolOrder(拖动排序)
|
|
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: [],
|
|
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
|
|
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(), [], '
|
|
163
|
-
assert.equal(loaded.supplier.pool.pick(loaded.supplier.accounts(), [], '
|
|
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
|
|
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>
|