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
@@ -0,0 +1,282 @@
1
+ /**
2
+ * openrouter 供应商插件 —— 参考 9Router(open-sse) 的 openrouter 实现。
3
+ *
4
+ * 上游:https://openrouter.ai/api/v1(OpenAI 兼容)
5
+ * - chat: POST /api/v1/chat/completions(SSE 流式 / 非流式)
6
+ * - models: GET /api/v1/models(过滤免费模型:pricing 全 0 + context ≥ 200k)
7
+ *
8
+ * API key 账号:走「添加链接 + 连接池」(账号池模型),弹窗填名字+key,
9
+ * 一个供应商可有多个命名 key,按池顺序/策略尝试。凭证存通用 CredentialStore
10
+ * (SQLite:auths/credentials.sqlite,{ name, apiKey })。
11
+ */
12
+ import type { ServerResponse } from 'node:http'
13
+ import type { ChatRequest, ModelInfo, SupplierStatus } from '../../router/types.ts'
14
+ import type { SupplierEnv, SupplierModule } from '../contract.ts'
15
+
16
+ export const id = 'openrouter'
17
+ export const name = 'OpenRouter'
18
+ export const priority = 10 // 免费直连(opencode=0)之后
19
+ /** 面板图标。 */
20
+ export const icon = 'data:image/png;base64,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'
21
+
22
+ const BASE = 'https://openrouter.ai/api/v1'
23
+ const MODELS_URL = `${BASE}/models`
24
+ const CHAT_URL = `${BASE}/chat/completions`
25
+ const COOL_DOWN_MS = 60 * 1000 // 失败冷却 60s
26
+
27
+ /** 免费模型过滤(同 9Router openrouter-free):pricing 全 0 且 context ≥ 200k。 */
28
+ function isFreeModelMeta(m: { pricing?: { prompt?: string; completion?: string }; context_length?: number }): boolean {
29
+ return (
30
+ m.pricing?.prompt === '0' &&
31
+ m.pricing?.completion === '0' &&
32
+ (m.context_length ?? 0) >= 200000
33
+ )
34
+ }
35
+
36
+ /** 剥 alias 前缀(or/xxx → xxx)。 */
37
+ function stripAlias(model: string): string {
38
+ const slash = model.lastIndexOf('/')
39
+ return slash >= 0 ? model.slice(slash + 1) : model
40
+ }
41
+
42
+ interface ApiKeyAccount {
43
+ name: string
44
+ apiKey: string
45
+ }
46
+
47
+ export default function factory(env: SupplierEnv): SupplierModule {
48
+ // 模型缓存由 dsh-router 核心统一管;插件每次拉取,失败回退上次成功结果
49
+ let modelsCache: ModelInfo[] | undefined
50
+ let freeIds = new Set<string>()
51
+ /** 失败冷却:uid → until(ms)。 */
52
+ const cooling = new Map<string, number>()
53
+
54
+ function listKeys(): string[] {
55
+ return env.credentials.list(id)
56
+ }
57
+
58
+ function getKey(uid: string): ApiKeyAccount | undefined {
59
+ return env.credentials.get<ApiKeyAccount>(id, uid)
60
+ }
61
+
62
+ /** 账号顺序:池顺序优先,未配置按凭证原始顺序。 */
63
+ function orderedKeys(): Array<{ uid: string; acct: ApiKeyAccount }> {
64
+ const byUid = new Map(listKeys().map((uid) => [uid, uid] as const))
65
+ const order = env.store.get(id).poolOrder
66
+ const uids = [...order.filter((u) => byUid.has(u)), ...listKeys().filter((u) => !order.includes(u))]
67
+ return uids.map((uid) => ({ uid, acct: getKey(uid)! })).filter((x) => x.acct !== undefined)
68
+ }
69
+
70
+ function isCooling(uid: string, now = Date.now()): boolean {
71
+ return (cooling.get(uid) ?? 0) > now
72
+ }
73
+
74
+ async function refreshModels(): Promise<ModelInfo[]> {
75
+ try {
76
+ const resp = await fetch(MODELS_URL, {
77
+ headers: { 'User-Agent': 'dsh-router', 'HTTP-Referer': 'https://endpoint-proxy.local', 'X-Title': 'dsh-router' },
78
+ signal: AbortSignal.timeout(15000),
79
+ })
80
+ if (!resp.ok) return []
81
+ const json = (await resp.json()) as { data?: Array<{ id: string; name?: string; pricing?: { prompt?: string; completion?: string }; context_length?: number }> }
82
+ const raw = json.data ?? []
83
+ const models: ModelInfo[] = []
84
+ const ids = new Set<string>()
85
+ for (const m of raw) {
86
+ if (m.id === '' || !isFreeModelMeta(m)) continue
87
+ if (ids.has(m.id)) continue
88
+ ids.add(m.id)
89
+ const entry: ModelInfo = { id: m.id }
90
+ if ((m.context_length ?? 0) > 0) entry.context_length = Math.round((m.context_length ?? 0) / 1000)
91
+ models.push(entry)
92
+ }
93
+ if (models.length > 0) {
94
+ modelsCache = models
95
+ freeIds = ids
96
+ }
97
+ return models
98
+ } catch {
99
+ return modelsCache ?? []
100
+ }
101
+ }
102
+
103
+ async function allModels(force: boolean): Promise<ModelInfo[]> {
104
+ return refreshModels()
105
+ }
106
+
107
+ return {
108
+ id,
109
+ name,
110
+ priority,
111
+ icon,
112
+ status: (): SupplierStatus => {
113
+ const now = Date.now()
114
+ const accounts = orderedKeys().map(({ uid, acct }) => ({
115
+ uid,
116
+ nickname: acct.name || 'API Key',
117
+ credits: 0,
118
+ cooling: isCooling(uid, now),
119
+ disabled: false,
120
+ err_count: 0,
121
+ }))
122
+ return { id, name, accounts }
123
+ },
124
+ listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
125
+ getAlias: (): string => 'or',
126
+ async addApiKey(input: { name: string; apiKey: string }): Promise<{ ok: boolean; error?: string; account?: { uid: string; nickname: string } }> {
127
+ const key = input.apiKey.trim()
128
+ if (key === '') return { ok: false, error: 'API key 不能为空' }
129
+ // 验证有效性:GET /api/v1/key 需要鉴权,无效 key 返回 401
130
+ try {
131
+ const probe = await fetch(`${BASE}/key`, {
132
+ headers: { Authorization: `Bearer ${key}`, 'User-Agent': 'dsh-router' },
133
+ signal: AbortSignal.timeout(15000),
134
+ })
135
+ if (!probe.ok) {
136
+ const text = await probe.text().catch(() => '')
137
+ return { ok: false, error: `key 无效: ${probe.status} ${text.slice(0, 200)}` }
138
+ }
139
+ } catch (err) {
140
+ return { ok: false, error: `验证失败: ${(err as Error).message}` }
141
+ }
142
+ // uid:key-<序号>,避免重复
143
+ let n = listKeys().length + 1
144
+ let uid = `key-${n}`
145
+ while (getKey(uid) !== undefined) uid = `key-${++n}`
146
+ env.credentials.save(id, uid, { name: input.name.trim() || `Key ${n}`, apiKey: key })
147
+ env.log(`openrouter add api key ${uid}`)
148
+ return { ok: true, account: { uid, nickname: input.name.trim() || `Key ${n}` } }
149
+ },
150
+ async removeLink(uid: string): Promise<boolean> {
151
+ if (getKey(uid) === undefined) return false
152
+ env.credentials.remove(id, uid)
153
+ cooling.delete(uid)
154
+ return true
155
+ },
156
+ async testModel(mid: string): Promise<{ ok: boolean; error?: string }> {
157
+ const base = stripAlias(mid)
158
+ const keys = orderedKeys()
159
+ if (keys.length === 0) return { ok: false, error: '未添加 API key(点「添加链接」配置)' }
160
+ if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) {
161
+ return { ok: false, error: `unknown free model ${JSON.stringify(mid)}` }
162
+ }
163
+ for (const { acct } of keys) {
164
+ try {
165
+ const resp = await fetch(CHAT_URL, {
166
+ method: 'POST',
167
+ headers: {
168
+ 'Content-Type': 'application/json',
169
+ Authorization: `Bearer ${acct.apiKey}`,
170
+ 'HTTP-Referer': 'https://endpoint-proxy.local',
171
+ 'X-Title': 'dsh-router',
172
+ },
173
+ body: JSON.stringify({ model: base, messages: [{ role: 'user', content: 'ping' }], stream: false, max_tokens: 1 }),
174
+ signal: AbortSignal.timeout(30000),
175
+ })
176
+ if (resp.ok) return { ok: true }
177
+ const body = await resp.text().catch(() => '')
178
+ return { ok: false, error: `upstream ${resp.status}: ${body.slice(0, 200)}` }
179
+ } catch (err) {
180
+ return { ok: false, error: (err as Error).message }
181
+ }
182
+ }
183
+ return { ok: false, error: 'no api key' }
184
+ },
185
+ async chatCompletions(req: ChatRequest, res: ServerResponse): Promise<boolean> {
186
+ const base = stripAlias(req.model)
187
+ if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) return false
188
+ const keys = orderedKeys().filter(({ uid }) => !isCooling(uid))
189
+ if (keys.length === 0) return false // 无健康 key → 路由器 fallback
190
+
191
+ let body = req.rawBody
192
+ try {
193
+ const obj = JSON.parse(body) as Record<string, unknown>
194
+ obj.model = base
195
+ body = JSON.stringify(obj)
196
+ } catch {
197
+ // 保持原样
198
+ }
199
+
200
+ let lastErr = ''
201
+ for (const { uid, acct } of keys) {
202
+ let upstream: Response
203
+ try {
204
+ upstream = await fetch(CHAT_URL, {
205
+ method: 'POST',
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ Authorization: `Bearer ${acct.apiKey}`,
209
+ 'HTTP-Referer': 'https://endpoint-proxy.local',
210
+ 'X-Title': 'dsh-router',
211
+ },
212
+ body,
213
+ signal: AbortSignal.timeout(120000),
214
+ })
215
+ } catch (err) {
216
+ cooling.set(uid, Date.now() + COOL_DOWN_MS)
217
+ lastErr = (err as Error).message
218
+ continue
219
+ }
220
+ // 非 2xx:冷却该 key,试下一个
221
+ if (upstream.status < 200 || upstream.status >= 300) {
222
+ cooling.set(uid, Date.now() + COOL_DOWN_MS)
223
+ const text = await upstream.text().catch(() => '')
224
+ lastErr = `upstream ${upstream.status}: ${text.slice(0, 120)}`
225
+ continue
226
+ }
227
+ // 成功:透传(流式 SSE 或 JSON)
228
+ if (req.stream) {
229
+ res.writeHead(upstream.status, {
230
+ 'Content-Type': 'text/event-stream',
231
+ 'Cache-Control': 'no-cache',
232
+ Connection: 'keep-alive',
233
+ 'X-Accel-Buffering': 'no',
234
+ })
235
+ if (!upstream.body) {
236
+ res.end()
237
+ return true
238
+ }
239
+ const reader = upstream.body.getReader()
240
+ try {
241
+ for (;;) {
242
+ const { done, value } = await reader.read()
243
+ if (done) break
244
+ res.write(Buffer.from(value))
245
+ if (typeof (res as { flushHeaders?: () => void }).flushHeaders === 'function') {
246
+ ;(res as { flushHeaders: () => void }).flushHeaders()
247
+ }
248
+ }
249
+ } finally {
250
+ reader.releaseLock()
251
+ }
252
+ res.end()
253
+ return true
254
+ }
255
+ const text = await upstream.text().catch(() => '')
256
+ let status = upstream.status
257
+ if (status === 200 && text === '') status = 502
258
+ try {
259
+ writeJson(res, status, JSON.parse(text))
260
+ } catch {
261
+ writeJson(res, status, { error: { message: text.slice(0, 500), type: 'api_error', code: 'upstream_error' } })
262
+ }
263
+ return true
264
+ }
265
+ // 所有 key 都失败:记录日志,返回 false 让路由器 fallback
266
+ env.log(`openrouter chat failed: ${lastErr}`)
267
+ return false
268
+ },
269
+ dispose: (): void => {
270
+ modelsCache = undefined
271
+ },
272
+ }
273
+ }
274
+
275
+ function writeJson(res: ServerResponse, status: number, body: unknown): void {
276
+ const payload = typeof body === 'string' ? body : JSON.stringify(body)
277
+ res.writeHead(status, {
278
+ 'Content-Type': 'application/json; charset=utf-8',
279
+ 'Cache-Control': 'no-store',
280
+ })
281
+ res.end(payload)
282
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * 供应商注册表 —— 持有加载的供应商 + 按能力暴露的通用端点。
3
+ *
4
+ * 通用能力(dsh-router 核心,所有供应商自动可用,js 无需实现):
5
+ * GET /suppliers/:id/models models + alias(listModels 合并启用状态)
6
+ * POST /suppliers/:id/models/toggle {id, enabled}
7
+ * POST /suppliers/:id/models/add {id}
8
+ * POST /suppliers/:id/models/remove {id}
9
+ * POST /suppliers/:id/models/bulk {enabled}
10
+ * POST /suppliers/:id/alias {alias}
11
+ * GET /suppliers/:id/pool/order + POST {uids}
12
+ * GET /suppliers/:id/pool/strategy + POST {strategy}
13
+ *
14
+ * 差异化能力(供应商 js 实现,按存在性注册;未实现返回 404):
15
+ * POST /suppliers/:id/models/fetch 拉取上游模型
16
+ * POST /suppliers/:id/models/test {id}
17
+ * POST /suppliers/:id/login 生成登录链接
18
+ * POST /suppliers/:id/login/callback {callbackUrl}
19
+ * POST /suppliers/:id/links/remove {uid}
20
+ * GET /suppliers/:id/checkin/rule + POST {rule}
21
+ * POST /suppliers/:id/checkin 触发签到
22
+ */
23
+ import type { IncomingMessage, ServerResponse } from 'node:http'
24
+ import type { ModelWithEnabled } from '../router/types.ts'
25
+ import type { SupplierConfigStore } from '../supplier-config.ts'
26
+ import type { LoadedSupplier } from './loader.ts'
27
+
28
+ /** webServer 路由形状(与 index.ts 的 WebServerRoute 一致)。 */
29
+ export interface WebServerRoute {
30
+ kind: 'prefix' | 'exact'
31
+ path: string
32
+ handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
33
+ }
34
+
35
+ function writeJson(res: ServerResponse, status: number, body: unknown): void {
36
+ const payload = JSON.stringify(body)
37
+ res.writeHead(status, {
38
+ 'content-type': 'application/json; charset=utf-8',
39
+ 'content-length': Buffer.byteLength(payload),
40
+ })
41
+ res.end(payload)
42
+ }
43
+
44
+ async function readBody(req: IncomingMessage, limit = 64 << 10): Promise<string> {
45
+ const chunks: Buffer[] = []
46
+ let size = 0
47
+ return await new Promise((resolve, reject) => {
48
+ req.on('data', (chunk: Buffer) => {
49
+ size += chunk.length
50
+ if (size > limit) {
51
+ reject(new Error('body_too_large'))
52
+ req.destroy()
53
+ return
54
+ }
55
+ chunks.push(chunk)
56
+ })
57
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
58
+ req.on('error', reject)
59
+ })
60
+ }
61
+
62
+ function cap(set: Set<string>, key: string): boolean {
63
+ return set.has(key)
64
+ }
65
+
66
+ /** 模型列表统一缓存(dsh-router 核心管;插件只管拉取,不缓存)。 */
67
+ const modelsCache = new Map<string, { models: ModelWithEnabled[]; fetchedAt: number }>()
68
+ const MODELS_TTL_MS = 60 * 1000
69
+
70
+ /** 拉取并缓存某供应商模型(force 时强制刷新)。custom 模型并入显示。 */
71
+ async function cachedModels(s: LoadedSupplier['supplier'], cfg: ReturnType<SupplierConfigStore['get']>, force = false): Promise<ModelWithEnabled[]> {
72
+ const hit = modelsCache.get(s.id)
73
+ if (!force && hit && Date.now() - hit.fetchedAt < MODELS_TTL_MS) return hit.models
74
+ const list = await Promise.resolve(s.listModels())
75
+ const custom = new Set(cfg.custom)
76
+ const disabled = new Set(cfg.disabled)
77
+ const models: ModelWithEnabled[] = list.map((mm) => ({
78
+ ...mm,
79
+ enabled: !disabled.has(mm.id),
80
+ custom: custom.has(mm.id) ? true : undefined,
81
+ }))
82
+ // 自定义模型(listModels 之外的)并入显示
83
+ const seen = new Set(models.map((mm) => mm.id))
84
+ for (const id of custom) {
85
+ if (!seen.has(id)) {
86
+ seen.add(id)
87
+ models.push({ id, enabled: !disabled.has(id), custom: true })
88
+ }
89
+ }
90
+ modelsCache.set(s.id, { models, fetchedAt: Date.now() })
91
+ return models
92
+ }
93
+
94
+ function mod(s: LoadedSupplier): Record<string, unknown> {
95
+ return (s.supplier as unknown as { __module?: Record<string, unknown> }).__module ?? {}
96
+ }
97
+
98
+ /** 为一个供应商生成端点(通用 + 差异化,返回 route 列表)。 */
99
+ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: SupplierConfigStore): WebServerRoute[] {
100
+ const s = loaded.supplier
101
+ const m = mod(loaded)
102
+ const c = loaded.capabilities
103
+ const p = `${base}/suppliers/${s.id}`
104
+ const routes: WebServerRoute[] = []
105
+
106
+ // ---- 通用: models(listModels 差异化 + 启用状态通用;核心统一缓存) ----
107
+ routes.push({
108
+ kind: 'exact',
109
+ path: `${p}/models`,
110
+ handler: async (_req, res) => {
111
+ const models = await cachedModels(s, store.get(s.id))
112
+ writeJson(res, 200, { ok: true, alias: s.getAlias(), models })
113
+ },
114
+ })
115
+
116
+ // ---- 通用: alias ----
117
+ routes.push({
118
+ kind: 'exact',
119
+ path: `${p}/alias`,
120
+ handler: async (req, res) => {
121
+ const body = JSON.parse(await readBody(req)) as { alias?: string }
122
+ const alias = body.alias ?? ''
123
+ const clean = alias.trim()
124
+ if (clean === '' || !/^[A-Za-z0-9_-]+$/.test(clean)) {
125
+ writeJson(res, 400, { ok: false, error: '前缀只能包含字母、数字、- 和 _' })
126
+ return
127
+ }
128
+ store.setAlias(s.id, clean)
129
+ writeJson(res, 200, { ok: true })
130
+ },
131
+ })
132
+
133
+ // ---- 通用: models/toggle ----
134
+ routes.push({
135
+ kind: 'exact',
136
+ path: `${p}/models/toggle`,
137
+ handler: async (req, res) => {
138
+ const body = JSON.parse(await readBody(req)) as { id?: string; enabled?: boolean }
139
+ const cfg = store.get(s.id)
140
+ const models = await cachedModels(s, cfg)
141
+ if (!body.id || !models.some((mm) => mm.id === body.id)) {
142
+ writeJson(res, 400, { ok: false, error: '模型不存在' })
143
+ return
144
+ }
145
+ store.setModelEnabled(s.id, body.id, !!body.enabled)
146
+ modelsCache.delete(s.id) // 启用状态变化后失效缓存
147
+ writeJson(res, 200, { ok: true })
148
+ },
149
+ })
150
+
151
+ // ---- 通用: models/add(自定义模型) ----
152
+ routes.push({
153
+ kind: 'exact',
154
+ path: `${p}/models/add`,
155
+ handler: async (req, res) => {
156
+ const body = JSON.parse(await readBody(req)) as { id?: string }
157
+ const id = (body.id ?? '').trim()
158
+ if (id === '') {
159
+ writeJson(res, 400, { ok: false, error: '模型 id 不能为空' })
160
+ return
161
+ }
162
+ const models = await cachedModels(s, store.get(s.id))
163
+ if (models.some((mm) => mm.id === id)) {
164
+ writeJson(res, 400, { ok: false, error: `模型 ${id} 已存在` })
165
+ return
166
+ }
167
+ store.addCustomModel(s.id, id)
168
+ modelsCache.delete(s.id)
169
+ writeJson(res, 200, { ok: true })
170
+ },
171
+ })
172
+
173
+ // ---- 通用: models/remove(自定义模型) ----
174
+ routes.push({
175
+ kind: 'exact',
176
+ path: `${p}/models/remove`,
177
+ handler: async (req, res) => {
178
+ const body = JSON.parse(await readBody(req)) as { id?: string }
179
+ const cfg = store.get(s.id)
180
+ if (!body.id || !cfg.custom.includes(body.id)) {
181
+ writeJson(res, 400, { ok: false, error: `模型 ${body.id} 不是自定义模型` })
182
+ return
183
+ }
184
+ store.removeCustomModel(s.id, body.id)
185
+ modelsCache.delete(s.id)
186
+ writeJson(res, 200, { ok: true })
187
+ },
188
+ })
189
+
190
+ // ---- 通用: models/bulk ----
191
+ routes.push({
192
+ kind: 'exact',
193
+ path: `${p}/models/bulk`,
194
+ handler: async (req, res) => {
195
+ const body = JSON.parse(await readBody(req)) as { enabled?: boolean }
196
+ const models = await cachedModels(s, store.get(s.id))
197
+ store.setAllModelsEnabled(s.id, !!body.enabled, models.map((mm) => mm.id))
198
+ modelsCache.delete(s.id)
199
+ writeJson(res, 200, { ok: true })
200
+ },
201
+ })
202
+
203
+ // ---- 通用: pool/order + pool/strategy ----
204
+ routes.push({
205
+ kind: 'exact',
206
+ path: `${p}/pool/order`,
207
+ handler: async (req, res) => {
208
+ if (req.method === 'GET') {
209
+ writeJson(res, 200, { ok: true, order: store.get(s.id).poolOrder })
210
+ return
211
+ }
212
+ const body = JSON.parse(await readBody(req)) as { uids?: string[] }
213
+ if (!Array.isArray(body.uids) || body.uids.some((u) => typeof u !== 'string')) {
214
+ writeJson(res, 400, { ok: false, error: '顺序必须是 uid 数组' })
215
+ return
216
+ }
217
+ store.setPoolOrder(s.id, body.uids)
218
+ writeJson(res, 200, { ok: true })
219
+ },
220
+ })
221
+
222
+ routes.push({
223
+ kind: 'exact',
224
+ path: `${p}/pool/strategy`,
225
+ handler: async (req, res) => {
226
+ if (req.method === 'GET') {
227
+ writeJson(res, 200, { ok: true, strategy: store.get(s.id).poolStrategy })
228
+ return
229
+ }
230
+ const body = JSON.parse(await readBody(req)) as { strategy?: string }
231
+ if (body.strategy !== 'fallback' && body.strategy !== 'round-robin') {
232
+ writeJson(res, 400, { ok: false, error: '策略无效' })
233
+ return
234
+ }
235
+ store.setPoolStrategy(s.id, body.strategy)
236
+ writeJson(res, 200, { ok: true })
237
+ },
238
+ })
239
+
240
+ // ---- 通用 UI: 获取模型(= 强制刷新 + 核心缓存更新) ----
241
+ routes.push({
242
+ kind: 'exact',
243
+ path: `${p}/models/fetch`,
244
+ handler: async (_req, res) => {
245
+ const models = await cachedModels(s, store.get(s.id), true)
246
+ writeJson(res, 200, { ok: true, models })
247
+ },
248
+ })
249
+
250
+ // ---- 必要差异化: testModel(所有供应商必须实现) ----
251
+ routes.push({
252
+ kind: 'exact',
253
+ path: `${p}/models/test`,
254
+ handler: async (req, res) => {
255
+ const body = JSON.parse(await readBody(req)) as { id?: string }
256
+ const result = await s.testModel(body.id ?? '')
257
+ writeJson(res, result.ok ? 200 : 400, result)
258
+ },
259
+ })
260
+
261
+ // ---- 通用 UI: 添加链接(差异化实现 generateLoginUrl/completeLogin) ----
262
+ routes.push({
263
+ kind: 'exact',
264
+ path: `${p}/login`,
265
+ handler: async (_req, res) => {
266
+ if (!cap(c, 'generateLoginUrl')) {
267
+ writeJson(res, 400, { ok: false, error: '该供应商不支持添加链接' })
268
+ return
269
+ }
270
+ const r = await (m.generateLoginUrl as () => string | { ok: boolean; error?: string; loginUrl?: string } | Promise<string | { ok: boolean; error?: string; loginUrl?: string }>).call(loaded.supplier)
271
+ if (typeof r === 'string') writeJson(res, 200, { ok: true, loginUrl: r })
272
+ else writeJson(res, r.ok === false ? 400 : 200, r)
273
+ },
274
+ })
275
+ routes.push({
276
+ kind: 'exact',
277
+ path: `${p}/login/callback`,
278
+ handler: async (req, res) => {
279
+ if (!cap(c, 'completeLogin')) {
280
+ writeJson(res, 400, { ok: false, error: '该供应商不支持添加链接' })
281
+ return
282
+ }
283
+ const body = JSON.parse(await readBody(req)) as { callbackUrl?: string }
284
+ try {
285
+ const acct = await (m.completeLogin as (url: string) => Promise<{ uid: string; nickname: string }>).call(loaded.supplier, body.callbackUrl ?? '')
286
+ writeJson(res, 200, { ok: true, account: acct })
287
+ } catch (err) {
288
+ writeJson(res, 400, { ok: false, error: (err as Error).message })
289
+ }
290
+ },
291
+ })
292
+
293
+ // ---- 通用 UI: 添加 API key 账号(差异化实现 addApiKey,弹窗填名字+key) ----
294
+ routes.push({
295
+ kind: 'exact',
296
+ path: `${p}/links/add`,
297
+ handler: async (req, res) => {
298
+ if (!cap(c, 'addApiKey')) {
299
+ writeJson(res, 400, { ok: false, error: '该供应商不支持添加 API key' })
300
+ return
301
+ }
302
+ const body = JSON.parse(await readBody(req)) as { name?: string; apiKey?: string }
303
+ try {
304
+ const r = await (m.addApiKey as (input: { name: string; apiKey: string }) => Promise<{ ok: boolean; error?: string; account?: { uid: string; nickname: string } }>).call(loaded.supplier, {
305
+ name: (body.name ?? '').trim(),
306
+ apiKey: (body.apiKey ?? '').trim(),
307
+ })
308
+ writeJson(res, r.ok === false ? 400 : 200, r)
309
+ } catch (err) {
310
+ writeJson(res, 400, { ok: false, error: (err as Error).message })
311
+ }
312
+ },
313
+ })
314
+
315
+ // ---- 通用 UI: 删除链接(数据删除,核心统一;凭证清理由供应商内部 removeLink) ----
316
+ routes.push({
317
+ kind: 'exact',
318
+ path: `${p}/links/remove`,
319
+ handler: async (req, res) => {
320
+ if (typeof s.removeLink !== 'function') {
321
+ writeJson(res, 400, { ok: false, error: '该供应商不支持删除链接' })
322
+ return
323
+ }
324
+ const body = JSON.parse(await readBody(req)) as { uid?: string }
325
+ const ok = await s.removeLink(body.uid ?? '')
326
+ writeJson(res, ok ? 200 : 404, ok ? { ok: true } : { ok: false, error: 'link not found' })
327
+ },
328
+ })
329
+
330
+ // ---- 通用: 签到规则(dsh-router 策略,存 SupplierConfigStore) ----
331
+ routes.push({
332
+ kind: 'exact',
333
+ path: `${p}/checkin/rule`,
334
+ handler: async (req, res) => {
335
+ if (req.method === 'GET') {
336
+ writeJson(res, 200, { ok: true, rule: store.get(s.id).checkinRule })
337
+ return
338
+ }
339
+ const body = JSON.parse(await readBody(req)) as { rule?: string }
340
+ if (body.rule !== 'all' && body.rule !== 'first') {
341
+ writeJson(res, 400, { ok: false, error: 'rule 必须是 all 或 first' })
342
+ return
343
+ }
344
+ store.setCheckinRule(s.id, body.rule)
345
+ writeJson(res, 200, { ok: true })
346
+ },
347
+ })
348
+
349
+ if (cap(c, 'checkinNow')) {
350
+ routes.push({
351
+ kind: 'exact',
352
+ path: `${p}/checkin`,
353
+ handler: async (_req, res) => {
354
+ try {
355
+ const result = await (m.checkinNow as () => Promise<{ ok: boolean; error?: string }>).call(loaded.supplier)
356
+ writeJson(res, result.ok === false ? 400 : 200, result)
357
+ } catch (err) {
358
+ writeJson(res, 500, { ok: false, error: (err as Error).message })
359
+ }
360
+ },
361
+ })
362
+ }
363
+
364
+ return routes
365
+ }