dsh-router-core 0.1.0 → 0.2.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 +36 -12
- package/lib/client-registry.js +92 -66
- package/lib/client.js +92 -66
- package/lib/index.js +567 -204
- package/lib/types/router/account-pool.d.ts +41 -0
- package/lib/types/router/index.d.ts +43 -4
- package/lib/types/router/types.d.ts +12 -10
- package/lib/types/supplier-config.d.ts +0 -3
- package/lib/types/suppliers/contract.d.ts +70 -21
- package/lib/types/suppliers/registry.d.ts +4 -3
- package/package.json +2 -1
- package/src/client/CombosTab.tsx +7 -5
- package/src/client/SupplierDetail.tsx +58 -34
- package/src/client/router.css +9 -0
- package/src/index.ts +2 -2
- package/src/router/account-pool.test.ts +120 -0
- package/src/router/account-pool.ts +161 -0
- package/src/router/index.ts +233 -19
- package/src/router/models-cache.test.ts +186 -0
- package/src/router/types.ts +12 -7
- package/src/supplier-config.ts +1 -12
- package/src/suppliers/contract.ts +71 -8
- package/src/suppliers/loader.ts +19 -12
- package/src/suppliers/nvidia/plugin.ts +161 -118
- package/src/suppliers/opencode/plugin.ts +47 -94
- package/src/suppliers/openrouter/plugin.ts +67 -121
- package/src/suppliers/registry.test.ts +194 -0
- package/src/suppliers/registry.ts +72 -57
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
* POST /suppliers/:id/login 生成登录链接
|
|
18
18
|
* POST /suppliers/:id/login/callback {callbackUrl}
|
|
19
19
|
* POST /suppliers/:id/links/remove {uid}
|
|
20
|
-
*
|
|
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 {
|
|
24
|
+
import type { Router } from '../router/index.ts'
|
|
25
|
+
import type { ModelWithEnabled, SupplierStatus } from '../router/types.ts'
|
|
25
26
|
import type { SupplierConfigStore } from '../supplier-config.ts'
|
|
26
27
|
import type { LoadedSupplier } from './loader.ts'
|
|
27
28
|
|
|
@@ -63,32 +64,31 @@ function cap(set: Set<string>, key: string): boolean {
|
|
|
63
64
|
return set.has(key)
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
/** 账号状态指纹(积分 + 健康),用于判断刷新是否落地。 */
|
|
68
|
+
function fingerprint(accounts: SupplierStatus['accounts']): string {
|
|
69
|
+
return accounts.map((a) => `${a.uid}:${a.credits}:${a.cooling}:${a.disabled}`).join('|')
|
|
70
|
+
}
|
|
69
71
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
72
|
+
/**
|
|
73
|
+
* 反复调插件已有的 status(),直到快照稳定再返回(是否有变化 = changed)。
|
|
74
|
+
* 插件的积分刷新是 fire-and-forget(status() 内部异步拉),核心拿不到句柄,
|
|
75
|
+
* 只能按指纹变化等它落地。天花板:缓存未过期时插件不会真拉上游,此时按钮
|
|
76
|
+
* 退化为「重读一次状态」(冷却/禁用这类健康状态仍是 status() 实时算的)。
|
|
77
|
+
* 升级路径:插件暴露可 await 的刷新能力,这里就不用轮询了。
|
|
78
|
+
*/
|
|
79
|
+
async function settleStatus(s: LoadedSupplier['supplier'], timeoutMs = 3000): Promise<boolean> {
|
|
80
|
+
const first = fingerprint(s.status().accounts)
|
|
81
|
+
const deadline = Date.now() + timeoutMs
|
|
82
|
+
let last = first
|
|
83
|
+
let stable = 0
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
86
|
+
const now = fingerprint(s.status().accounts)
|
|
87
|
+
stable = now === last ? stable + 1 : 0
|
|
88
|
+
last = now
|
|
89
|
+
if (stable >= 2) break // 连续两次没变 → 认为刷新已落地
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
return models
|
|
91
|
+
return last !== first
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function mod(s: LoadedSupplier): Record<string, unknown> {
|
|
@@ -96,7 +96,7 @@ function mod(s: LoadedSupplier): Record<string, unknown> {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/** 为一个供应商生成端点(通用 + 差异化,返回 route 列表)。 */
|
|
99
|
-
export function supplierRoutes(base: string, loaded: LoadedSupplier, store: SupplierConfigStore): WebServerRoute[] {
|
|
99
|
+
export function supplierRoutes(base: string, loaded: LoadedSupplier, store: SupplierConfigStore, router: Router): WebServerRoute[] {
|
|
100
100
|
const s = loaded.supplier
|
|
101
101
|
const m = mod(loaded)
|
|
102
102
|
const c = loaded.capabilities
|
|
@@ -108,7 +108,7 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
108
108
|
kind: 'exact',
|
|
109
109
|
path: `${p}/models`,
|
|
110
110
|
handler: async (_req, res) => {
|
|
111
|
-
const models = await
|
|
111
|
+
const models = await router.modelsOf(s.id)
|
|
112
112
|
writeJson(res, 200, { ok: true, alias: s.getAlias(), models })
|
|
113
113
|
},
|
|
114
114
|
})
|
|
@@ -137,13 +137,13 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
137
137
|
handler: async (req, res) => {
|
|
138
138
|
const body = JSON.parse(await readBody(req)) as { id?: string; enabled?: boolean }
|
|
139
139
|
const cfg = store.get(s.id)
|
|
140
|
-
const models = await
|
|
140
|
+
const models = await router.modelsOf(s.id)
|
|
141
141
|
if (!body.id || !models.some((mm) => mm.id === body.id)) {
|
|
142
142
|
writeJson(res, 400, { ok: false, error: '模型不存在' })
|
|
143
143
|
return
|
|
144
144
|
}
|
|
145
145
|
store.setModelEnabled(s.id, body.id, !!body.enabled)
|
|
146
|
-
|
|
146
|
+
router.invalidateModels(s.id)
|
|
147
147
|
writeJson(res, 200, { ok: true })
|
|
148
148
|
},
|
|
149
149
|
})
|
|
@@ -159,13 +159,13 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
159
159
|
writeJson(res, 400, { ok: false, error: '模型 id 不能为空' })
|
|
160
160
|
return
|
|
161
161
|
}
|
|
162
|
-
const models = await
|
|
162
|
+
const models = await router.modelsOf(s.id)
|
|
163
163
|
if (models.some((mm) => mm.id === id)) {
|
|
164
164
|
writeJson(res, 400, { ok: false, error: `模型 ${id} 已存在` })
|
|
165
165
|
return
|
|
166
166
|
}
|
|
167
167
|
store.addCustomModel(s.id, id)
|
|
168
|
-
|
|
168
|
+
router.invalidateModels(s.id)
|
|
169
169
|
writeJson(res, 200, { ok: true })
|
|
170
170
|
},
|
|
171
171
|
})
|
|
@@ -182,7 +182,7 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
182
182
|
return
|
|
183
183
|
}
|
|
184
184
|
store.removeCustomModel(s.id, body.id)
|
|
185
|
-
|
|
185
|
+
router.invalidateModels(s.id)
|
|
186
186
|
writeJson(res, 200, { ok: true })
|
|
187
187
|
},
|
|
188
188
|
})
|
|
@@ -193,9 +193,9 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
193
193
|
path: `${p}/models/bulk`,
|
|
194
194
|
handler: async (req, res) => {
|
|
195
195
|
const body = JSON.parse(await readBody(req)) as { enabled?: boolean }
|
|
196
|
-
const models = await
|
|
196
|
+
const models = await router.modelsOf(s.id)
|
|
197
197
|
store.setAllModelsEnabled(s.id, !!body.enabled, models.map((mm) => mm.id))
|
|
198
|
-
|
|
198
|
+
router.invalidateModels(s.id)
|
|
199
199
|
writeJson(res, 200, { ok: true })
|
|
200
200
|
},
|
|
201
201
|
})
|
|
@@ -242,18 +242,18 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
242
242
|
kind: 'exact',
|
|
243
243
|
path: `${p}/models/fetch`,
|
|
244
244
|
handler: async (_req, res) => {
|
|
245
|
-
const models = await
|
|
245
|
+
const models = await router.modelsOf(s.id, true)
|
|
246
246
|
writeJson(res, 200, { ok: true, models })
|
|
247
247
|
},
|
|
248
248
|
})
|
|
249
249
|
|
|
250
|
-
// ----
|
|
250
|
+
// ---- 通用: 测试模型(核心统一走 chatOnce 路径,账号池回退/冷却自动生效) ----
|
|
251
251
|
routes.push({
|
|
252
252
|
kind: 'exact',
|
|
253
253
|
path: `${p}/models/test`,
|
|
254
254
|
handler: async (req, res) => {
|
|
255
255
|
const body = JSON.parse(await readBody(req)) as { id?: string }
|
|
256
|
-
const result = await
|
|
256
|
+
const result = await router.testModel(s.id, body.id ?? '')
|
|
257
257
|
writeJson(res, result.ok ? 200 : 400, result)
|
|
258
258
|
},
|
|
259
259
|
})
|
|
@@ -327,22 +327,19 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
327
327
|
},
|
|
328
328
|
})
|
|
329
329
|
|
|
330
|
-
// ---- 通用:
|
|
330
|
+
// ---- 通用: 刷新链接池(积分 + 健康) ----
|
|
331
|
+
// 刷新是核心的活,但只调插件已有的 status()——插件在 status() 里自带积分
|
|
332
|
+
// 异步刷新(缓存过期才真拉上游),冷却/禁用(健康)也是 status() 实时算的。
|
|
331
333
|
routes.push({
|
|
332
334
|
kind: 'exact',
|
|
333
|
-
path: `${p}/
|
|
334
|
-
handler: async (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (body.rule !== 'all' && body.rule !== 'first') {
|
|
341
|
-
writeJson(res, 400, { ok: false, error: 'rule 必须是 all 或 first' })
|
|
342
|
-
return
|
|
335
|
+
path: `${p}/links/refresh`,
|
|
336
|
+
handler: async (_req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const changed = await settleStatus(s)
|
|
339
|
+
writeJson(res, 200, { ok: true, changed, accounts: s.status().accounts })
|
|
340
|
+
} catch (err) {
|
|
341
|
+
writeJson(res, 500, { ok: false, error: (err as Error).message })
|
|
343
342
|
}
|
|
344
|
-
store.setCheckinRule(s.id, body.rule)
|
|
345
|
-
writeJson(res, 200, { ok: true })
|
|
346
343
|
},
|
|
347
344
|
})
|
|
348
345
|
|
|
@@ -351,12 +348,30 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
351
348
|
kind: 'exact',
|
|
352
349
|
path: `${p}/checkin`,
|
|
353
350
|
handler: async (_req, res) => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
// 签到 = 所有链接逐个签一次:checkinNow 是单账号能力,
|
|
352
|
+
// 遍历与汇总是核心的活(插件不负责连接池顺序/范围)。
|
|
353
|
+
// 禁用与否由插件自己判定(如 traework 返回 status:'disabled'),核心不替它筛。
|
|
354
|
+
const checkinOne = m.checkinNow as
|
|
355
|
+
((uid: string) => Promise<{ ok: boolean; status: string; message?: string }>) | undefined
|
|
356
|
+
if (!checkinOne) {
|
|
357
|
+
writeJson(res, 400, { ok: false, error: '该供应商不支持签到' })
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
type CheckinResult = { uid: string; ok: boolean; status: string; message?: string }
|
|
361
|
+
const uids = s.status().accounts.map((a) => a.uid)
|
|
362
|
+
const results: CheckinResult[] = []
|
|
363
|
+
// 单个链接抛错不能带垮整批:记成 error 继续下一个
|
|
364
|
+
for (const uid of uids) {
|
|
365
|
+
try {
|
|
366
|
+
results.push({ uid, ...(await checkinOne.call(loaded.supplier, uid)) })
|
|
367
|
+
} catch (err) {
|
|
368
|
+
results.push({ uid, ok: false, status: 'error', message: (err as Error).message })
|
|
369
|
+
}
|
|
359
370
|
}
|
|
371
|
+
const succeeded = results.filter((r) => r.status === 'ok').length
|
|
372
|
+
const already = results.filter((r) => r.status === 'already').length
|
|
373
|
+
const payload = { ok: succeeded + already > 0, total: uids.length, succeeded, already, results }
|
|
374
|
+
writeJson(res, payload.ok ? 200 : 400, payload)
|
|
360
375
|
},
|
|
361
376
|
})
|
|
362
377
|
}
|