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
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
* file IO —— 落盘位置由核心锚定(不跟 cwd 跑),读写经核心,插件无状态。
|
|
26
26
|
*/
|
|
27
27
|
import type { Context } from '@deepseek-ai/cordis';
|
|
28
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
29
28
|
/** 扩展器唯一 id(注册键)。 */
|
|
30
29
|
export type ExtId = string;
|
|
31
30
|
/**
|
|
@@ -55,12 +54,6 @@ export interface RouterExt {
|
|
|
55
54
|
readonly icon?: string;
|
|
56
55
|
/** 当前运行时状态(ready + 不就绪时的说明)。 */
|
|
57
56
|
getState(): ExtState;
|
|
58
|
-
/**
|
|
59
|
-
* 通用扩展 API 点(可选):核心把 `/router/api/ext/{id}/{subpath}` 兜底路由派发进来,
|
|
60
|
-
* 由扩展器自己裁决/处理(如 watch 的 `processes`/`kill`)。返回 true 表示已处理;
|
|
61
|
-
* false / 未实现 → 核心回 404。这样扩展的执行面逻辑进插件,核心只加一条通用转发。
|
|
62
|
-
*/
|
|
63
|
-
api?(subpath: string, req: IncomingMessage, res: ServerResponse): boolean | Promise<boolean>;
|
|
64
57
|
/** 卸载清理(表删除时由 dsh-router 调用,可选)。 */
|
|
65
58
|
dispose?(): void;
|
|
66
59
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 探测 profile 里安装的 DSH 宿主版本。
|
|
3
|
+
* @param baseUrl 宿主注入的 profile 锚点(`ctx.baseUrl`)
|
|
4
|
+
* @returns 形如 `0.1.7-alpha.2`;读不到返回 undefined(调用方按「不支持」处理)
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectHostVersion(baseUrl?: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* 宿主是否 >= 0.1.7。三处兼容分叉共用这一个判据:
|
|
9
|
+
* - 「最近命中」徽章(composer.dock 位置正确)
|
|
10
|
+
* - 设置命名空间(Config 驱动而非 installSection)
|
|
11
|
+
* - adapter 的独立 role:'tool' 消息形态
|
|
12
|
+
*/
|
|
13
|
+
export declare function isHost017Plus(baseUrl?: string): boolean;
|
|
14
|
+
/** 「最近命中」徽章是否可用(= 宿主 >= 0.1.7)。 */
|
|
15
|
+
export declare function supportsLastHitDock(baseUrl?: string): boolean;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,5 +5,11 @@
|
|
|
5
5
|
export declare const name = "dsh-router-core";
|
|
6
6
|
/** Services required before mounting: the webserver (routes) + llm (设置-模型). */
|
|
7
7
|
export declare const inject: string[];
|
|
8
|
+
/**
|
|
9
|
+
* 0.1.7 的设置镜像只认「插件导出的 Config + 配置树行 id」这条收录链,且空 object
|
|
10
|
+
* 会被 volatileForm 过滤掉 —— 顶层 `.volatile()` 才能让 Router 卡片出现在设置-模型。
|
|
11
|
+
* Router 的真配置在自己的 state.json(路由系统面板),这里故意不暴露任何字段。
|
|
12
|
+
*/
|
|
13
|
+
export declare const Config: import("@deepseek-ai/schemastery").Schema<unknown>;
|
|
8
14
|
/** Plugin body. */
|
|
9
15
|
export declare function apply(rawContext: unknown): void;
|
|
@@ -34,14 +34,27 @@ export declare class AccountPool {
|
|
|
34
34
|
private rrCursor;
|
|
35
35
|
/** (supplier, model) → 当前驻留的号(只有**无亲和**的回退路径用它)。 */
|
|
36
36
|
private blocks;
|
|
37
|
-
/** (supplier, model, uid) →
|
|
37
|
+
/** (supplier, model, uid[, session]) → 该号在某会话下的缓存质量样本。 */
|
|
38
38
|
private samples;
|
|
39
|
-
/**
|
|
39
|
+
/** 同上键 → 缓存无效降权。 */
|
|
40
40
|
private demoted;
|
|
41
41
|
/** (supplier, model) → Map<前缀指纹, uid>:前缀亲和的选号记忆(LRU 封顶)。 */
|
|
42
42
|
private affinity;
|
|
43
43
|
constructor(supplierId?: string);
|
|
44
44
|
private key;
|
|
45
|
+
/**
|
|
46
|
+
* 缓存质量样本/降权的键。
|
|
47
|
+
*
|
|
48
|
+
* **按会话隔离**(team 场景的关键):同一个号被多个会话共用时,各会话的
|
|
49
|
+
* 前缀会互相驱逐,把它们混在一起算命中率,会把「多会话互踩」误判成
|
|
50
|
+
* 「这个号缓存坏了」→ 错误降权 → 只剩一个号 → 那个号又被打低 → 降权串。
|
|
51
|
+
* 所以有会话身份时按 (model, uid, session) 记,降权只反映**该会话自己**
|
|
52
|
+
* 在这个号上的真实缓存表现。
|
|
53
|
+
*
|
|
54
|
+
* 无会话身份(外部 OpenAI 客户端,走无指纹回退路径)→ 保持 (model, uid),
|
|
55
|
+
* 与块轮询的原语义一致(那时本就是单会话视角)。
|
|
56
|
+
*/
|
|
57
|
+
private sampleKey;
|
|
45
58
|
private entry;
|
|
46
59
|
/** 某连接整体状态(连接级冷却/手动暂停)。 */
|
|
47
60
|
private uidState;
|
|
@@ -54,33 +67,53 @@ export declare class AccountPool {
|
|
|
54
67
|
*/
|
|
55
68
|
private healthy;
|
|
56
69
|
/**
|
|
57
|
-
*
|
|
70
|
+
* 为「某个模型」选一个健康账号 —— **会话亲和选号,无策略旋钮**。
|
|
58
71
|
*
|
|
59
|
-
* round-robin
|
|
72
|
+
* 旧的两策略(fallback 取第一个 / round-robin 轮转)都删了:前者让所有
|
|
73
|
+
* 会话挤同一个号(team/多会话必互踩),后者的"轮转"与亲和"同会话永不换号"
|
|
74
|
+
* 直接冲突。正确模型只有一条:
|
|
60
75
|
*
|
|
61
76
|
* 1. 算请求的前缀指纹(messages 前几条),查绑定表 → 命中且该号健康 → 用它。
|
|
62
|
-
*
|
|
63
|
-
* 缓存里只写一份。块轮询治不了跨会话驱逐(多会话大前缀在同一号互踩,
|
|
64
|
-
* 实测 3 会话 3.8% vs 单会话 82.5%),亲和才是根治。
|
|
77
|
+
* 同一个会话的连续请求**永远落回同一个号**,前缀在该号缓存里只写一份。
|
|
65
78
|
* 2. 新指纹(或绑定的号不健康)→ 按游标挑一个号并**记下绑定**,之后就固定。
|
|
66
79
|
* 新会话用游标分发,所以不同会话天然散到不同号上(均衡)。
|
|
67
|
-
* 3.
|
|
68
|
-
*
|
|
80
|
+
* 3. 指纹算不出(拿不到 messages)→ **块轮询兜底**(§2 旧行为)。这不是
|
|
81
|
+
* "策略",是无会话身份时的替身。
|
|
82
|
+
* 4. 块统计不驱动轮转(亲和已保证驻留),只保留 `BLOCK_MAX` 次不达标 →
|
|
83
|
+
* 降权该号(缓存无效,可恢复)。
|
|
69
84
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
85
|
+
* 天花板:号数 < 并发会话数时必然有会话共用号 → 互踩,无法用选号手法消除
|
|
86
|
+
* (缓存空间是账号侧物理限制)。见 docs/pool-sticky-block.md §10。
|
|
72
87
|
*
|
|
73
88
|
* @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)
|
|
74
89
|
* @param poolOrder 用户在面板拖出来的顺序(核心管)
|
|
75
|
-
* @param strategy fallback 取第一个健康 / round-robin 前缀亲和
|
|
76
90
|
* @param modelId 当前要路由的模型(决定查哪个 (model, uid) 冷却单元)
|
|
77
|
-
* @param messages 请求体的 messages
|
|
91
|
+
* @param messages 请求体的 messages(算亲和指纹的内容兜底)
|
|
92
|
+
* @param session 宿主会话身份;给了就以它为主键(精确、零撞车)
|
|
78
93
|
* @returns 选中的 uid;无健康账号返回 undefined
|
|
79
94
|
*/
|
|
80
|
-
pick(accounts: SupplierAccountNow[], poolOrder: string[],
|
|
95
|
+
pick(accounts: SupplierAccountNow[], poolOrder: string[], modelId: string, messages?: unknown, session?: string): string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* 新会话分配:优先返回**未被其它会话占用**的号(按游标推进),
|
|
98
|
+
* 空闲号用尽才回绕到已占用的号(此时按游标复用,语义等同旧行为)。
|
|
99
|
+
*
|
|
100
|
+
* 「占用」= 该 model 的绑定表里,某个 **其它** 指纹当前指向该 uid。
|
|
101
|
+
* 同一会话自己的旧绑定不算占用(它已经在上面的亲和分支处理过,走到这里
|
|
102
|
+
* 说明原绑定不可用,允许它改绑到自己的其它号或空闲号)。
|
|
103
|
+
*/
|
|
104
|
+
private allocSession;
|
|
105
|
+
/** 当前 model 下、本指纹之外没有任何其它会话绑定的号(按 pool 原顺序)。 */
|
|
106
|
+
private freeUids;
|
|
107
|
+
/** 上一次 pick 的选号理由(诊断用:解释"为什么没粘住原号")。 */
|
|
108
|
+
lastWhy: string;
|
|
81
109
|
/** 记下「前缀指纹 → 号」的绑定(LRU:超上限丢最久未用的)。 */
|
|
82
110
|
private bind;
|
|
83
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* 该号在**本会话**(有身份时)本模型上是否被判定「缓存无效」(降权中)。
|
|
113
|
+
*
|
|
114
|
+
* 判据按会话隔离(见 sampleKey):降权只惩罚「这个会话在这个号上真的
|
|
115
|
+
* 反复不命中」,不会因别的会话共用同号互踩而被牵连。
|
|
116
|
+
*/
|
|
84
117
|
private isDemoted;
|
|
85
118
|
/**
|
|
86
119
|
* 驻留是否还应留在当前号上(**只用于无亲和的回退路径**)。
|
|
@@ -99,13 +132,15 @@ export declare class AccountPool {
|
|
|
99
132
|
* 记录一次成功请求的缓存命中情况(降权判据的反馈信号)。
|
|
100
133
|
*
|
|
101
134
|
* 只有**成功**请求才计入:失败没产生缓存,计入会污染命中率并导致错降权。
|
|
102
|
-
* 统计按 (model, uid)
|
|
103
|
-
*
|
|
135
|
+
* 统计按 (model, uid[, session]) 记,**不管这个号是不是「当前驻留」**——
|
|
136
|
+
* 亲和开启后多个会话各绑各的号交错服务,按驻留记会漏掉大部分真实反馈。
|
|
137
|
+
* 按会话隔离(见 sampleKey)则保证「多会话共用同号互踩」不会误伤降权。
|
|
104
138
|
* @param uid 实际服务的号
|
|
105
139
|
* @param modelId 模型(块粒度)
|
|
106
140
|
* @param cachedTokens 上游报的命中 token 数;0 = 全量重算
|
|
141
|
+
* @param session 宿主会话身份;有则按会话隔离统计
|
|
107
142
|
*/
|
|
108
|
-
noteCache(uid: string, modelId: string, cachedTokens: number): void;
|
|
143
|
+
noteCache(uid: string, modelId: string, cachedTokens: number, session?: string): void;
|
|
109
144
|
/** 故障切走时作废当前驻留:换号后前缀归属变了,驻留计数不能沿用。 */
|
|
110
145
|
dropBlock(modelId: string): void;
|
|
111
146
|
/** 块状态键:与冷却同粒度 (supplier, model),不含 uid。 */
|
|
@@ -7,9 +7,7 @@ export declare class Router {
|
|
|
7
7
|
private suppliers;
|
|
8
8
|
private combosFp;
|
|
9
9
|
private customCombos;
|
|
10
|
-
/**
|
|
11
|
-
private rrCursors;
|
|
12
|
-
/** 通用供应商配置(连接池顺序/策略、模型启用、别名)。 */
|
|
10
|
+
/** 通用供应商配置(连接池顺序、模型启用、别名)。 */
|
|
13
11
|
private store;
|
|
14
12
|
/** 模型列表缓存(supplierId → 模型 + 拉取时间)。插件只管拉,不缓存。 */
|
|
15
13
|
private modelsCache;
|
|
@@ -104,7 +102,6 @@ export declare class Router {
|
|
|
104
102
|
models: ModelWithEnabled[];
|
|
105
103
|
}>>;
|
|
106
104
|
private validModels;
|
|
107
|
-
private validStrategy;
|
|
108
105
|
/** 组合模型统一存裸 id:剥掉 alias/ 前缀(前缀随供应商动态变)。
|
|
109
106
|
* 只剥「已知 alias + /」开头的前缀——模型 id 本身可以含斜杠
|
|
110
107
|
* (如 nvidia 的 `deepseek-ai/deepseek-v4-flash-0731`),用 lastIndexOf 会把
|
|
@@ -115,14 +112,14 @@ export declare class Router {
|
|
|
115
112
|
* 这里只做清理与校验,不再剥掉前缀——精准调用靠的就是供应商 id。
|
|
116
113
|
*/
|
|
117
114
|
private normalizeModelIds;
|
|
118
|
-
/** 创建组合(name 唯一,非 default
|
|
119
|
-
createCombo(name: string,
|
|
115
|
+
/** 创建组合(name 唯一,非 default)。选模型是降级链(按序尝试),无策略。 */
|
|
116
|
+
createCombo(name: string, models: string[]): {
|
|
120
117
|
ok: boolean;
|
|
121
118
|
error?: string;
|
|
122
119
|
combo?: Combo;
|
|
123
120
|
};
|
|
124
121
|
/** 更新组合(按 id)。 */
|
|
125
|
-
updateCombo(id: string, name: string,
|
|
122
|
+
updateCombo(id: string, name: string, models: string[]): {
|
|
126
123
|
ok: boolean;
|
|
127
124
|
error?: string;
|
|
128
125
|
};
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 算请求的亲和指纹(选号主键)。
|
|
3
|
+
*
|
|
4
|
+
* **身份优先,内容兜底**:
|
|
5
|
+
* 1. 宿主给了会话身份(`session`,来自 dsh-llm 的 `sessionId`)→ 直接用它。
|
|
6
|
+
* 精确、零撞车 —— 同一会话永不漂移,不同会话(含 team 各成员)天然隔离。
|
|
7
|
+
* 2. 没有身份(外部 OpenAI 客户端)→ 回落 {@link prefixFingerprint}。
|
|
8
|
+
*
|
|
9
|
+
* 为什么优先身份而不是内容指纹:DSH 的 system prompt 是同一个 agent preset
|
|
10
|
+
* 生成的几 KB 文本,内容指纹只取前 512 字符 → 各会话高度雷同 → 撞车导致
|
|
11
|
+
* 绑定互相覆盖、同会话漂移(team 多成员时实测过)。
|
|
12
|
+
*
|
|
13
|
+
* @param session 宿主会话身份;无则 undefined
|
|
14
|
+
* @param messages OpenAI 请求体的 `messages`(无身份时才用)
|
|
15
|
+
* @returns 指纹字符串;**空串 = 算不出来(无亲和,回退到块轮询)**
|
|
16
|
+
*/
|
|
17
|
+
export declare function sessionFingerprint(session: string | undefined, messages: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* 算请求的前缀指纹(内容兜底)。
|
|
3
20
|
* @param messages OpenAI 请求体的 `messages` 字段(未校验,来自外部输入)
|
|
4
21
|
* @returns 指纹字符串;**空串 = 算不出来(无亲和,回退到块轮询)**
|
|
5
22
|
*/
|
|
@@ -32,13 +32,10 @@ export interface ModelWithEnabled extends ModelInfo {
|
|
|
32
32
|
/** 是否为自定义模型(可删除)。 */
|
|
33
33
|
custom?: boolean;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
export type ComboStrategy = 'fallback' | 'round-robin';
|
|
37
|
-
/** 组合:命名的一组模型,路由时按策略(回退/轮询)命中其中一个。 */
|
|
35
|
+
/** 组合:命名的一组模型,路由时作为**降级链**按序尝试(无策略旋钮)。 */
|
|
38
36
|
export interface Combo {
|
|
39
37
|
id: string;
|
|
40
38
|
name: string;
|
|
41
|
-
strategy: ComboStrategy;
|
|
42
39
|
models: string[];
|
|
43
40
|
}
|
|
44
41
|
/** 一次 /v1/chat/completions 请求。 */
|
|
@@ -49,6 +46,12 @@ export interface ChatRequest {
|
|
|
49
46
|
model: string;
|
|
50
47
|
/** 推理等级(reasoning effort),供应商各自决定怎么映射到上游。缺省 = 'auto'。 */
|
|
51
48
|
lv?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 宿主盖的会话身份(dsh-llm `GenerateOptions.sessionId`),由 adapter 转成
|
|
51
|
+
* 内部头带进来。用于把用量记录**按会话归因**(「最近命中」徽章要按当前会话
|
|
52
|
+
* 显示,而不是显示别的会话的命中)。缺省 = 非宿主调用(外部 OpenAI 客户端)。
|
|
53
|
+
*/
|
|
54
|
+
session?: string;
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* 供应商接口。核心负责账号池(选号/冷却/禁用/错误累计/遍历回退),
|
|
@@ -25,6 +25,11 @@ export interface UsageRecord {
|
|
|
25
25
|
uid?: string;
|
|
26
26
|
/** 失败原因(ok=false 时)。 */
|
|
27
27
|
error?: string;
|
|
28
|
+
/**
|
|
29
|
+
* 宿主会话身份(有则记)。用于「最近命中」按当前会话归因 —— 徽章要显示
|
|
30
|
+
* 当前会话的命中,而不是全局最近(team 多会话下后者会张冠李戴)。
|
|
31
|
+
*/
|
|
32
|
+
session?: string;
|
|
28
33
|
}
|
|
29
34
|
export type Period = 'today' | '24h' | '7d' | '30d';
|
|
30
35
|
/** 本地日期键 `YYYY-MM-DD`(照 9router getLocalDateKey,不按 UTC 切天)。 */
|
|
@@ -74,6 +79,13 @@ export declare class UsageStore {
|
|
|
74
79
|
chart(period: Period, now?: number): ChartBucket[];
|
|
75
80
|
/** 最近请求明细。 */
|
|
76
81
|
recentList(limit?: number): UsageRecord[];
|
|
82
|
+
/**
|
|
83
|
+
* 最近一次命中。给了 `session` 就只在该会话的记录里找(「最近命中」徽章
|
|
84
|
+
* 要显示**当前会话**的命中,team 多会话下全局最近会张冠李戴);
|
|
85
|
+
* 不给则退回全局最近一条。
|
|
86
|
+
* @param session 宿主会话身份;undefined = 全局
|
|
87
|
+
*/
|
|
88
|
+
lastHit(session?: string): UsageRecord | undefined;
|
|
77
89
|
clear(): void;
|
|
78
90
|
/** 进程退出前把防抖中的写入落盘。 */
|
|
79
91
|
flush(): void;
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ export interface RouterCheckinResponse {
|
|
|
62
62
|
/** `/router/api/health` response. */
|
|
63
63
|
export interface RouterHealthResponse {
|
|
64
64
|
ok: boolean;
|
|
65
|
+
/** 宿主 DSH 版本(读不到为空串)。 */
|
|
66
|
+
hostVersion?: string;
|
|
67
|
+
/** 「最近命中」徽章是否受支持(宿主 >= 0.1.7 才有 composer.dock 正确位置)。 */
|
|
68
|
+
lastHitDock?: boolean;
|
|
65
69
|
suppliers?: Array<{
|
|
66
70
|
id: string;
|
|
67
71
|
name: string;
|
|
@@ -76,13 +80,11 @@ export interface RouterHealthResponse {
|
|
|
76
80
|
}>;
|
|
77
81
|
error?: string;
|
|
78
82
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
/** 组合。models = 该组合命中的模型列表(裸模型 id,展示时动态拼前缀)。 */
|
|
83
|
+
/** 组合。models = 该组合命中的模型列表(裸模型 id,展示时动态拼前缀)。
|
|
84
|
+
* 路由时作为降级链按序尝试,无策略字段。 */
|
|
82
85
|
export interface RouterCombo {
|
|
83
86
|
id: string;
|
|
84
87
|
name: string;
|
|
85
|
-
strategy: RouterComboStrategy;
|
|
86
88
|
models: string[];
|
|
87
89
|
}
|
|
88
90
|
/** `/router/api/combos` response. */
|
|
@@ -151,23 +153,6 @@ export interface RouterExtResponse {
|
|
|
151
153
|
error?: string;
|
|
152
154
|
enhancers?: RouterExtItem[];
|
|
153
155
|
}
|
|
154
|
-
/** 一行 watch 进程(`/router/api/ext/watch/processes`)。 */
|
|
155
|
-
export interface RouterWatchProcess {
|
|
156
|
-
pid: number;
|
|
157
|
-
ppid: number;
|
|
158
|
-
/** %cpu 快照 */ cpu: number;
|
|
159
|
-
/** %mem 快照 */ mem: number;
|
|
160
|
-
command: string;
|
|
161
|
-
/** 命中的已记录 agent 命令。 */
|
|
162
|
-
match: string;
|
|
163
|
-
recordedAt: number;
|
|
164
|
-
}
|
|
165
|
-
/** `/router/api/ext/{id}/processes` response。 */
|
|
166
|
-
export interface RouterWatchResponse {
|
|
167
|
-
ok: boolean;
|
|
168
|
-
error?: string;
|
|
169
|
-
processes?: RouterWatchProcess[];
|
|
170
|
-
}
|
|
171
156
|
/** 供应商模型(含启用状态)。 */
|
|
172
157
|
export interface RouterSupplierModel {
|
|
173
158
|
id: string;
|
|
@@ -210,6 +195,13 @@ export interface RouterUsageRecord {
|
|
|
210
195
|
durationMs: number;
|
|
211
196
|
/** 首字节延迟(ms);非流式为 0。 */
|
|
212
197
|
ttfbMs: number;
|
|
198
|
+
/**
|
|
199
|
+
* 连接名(uid → 显示别名,后端解析好)。空串 = 无连接(失败/未服务)。
|
|
200
|
+
* 「最近请求」的「连接」列用它,而不是 supplier。
|
|
201
|
+
*/
|
|
202
|
+
connection?: string;
|
|
203
|
+
/** 实际服务的连接 uid(成功且有账号时)。 */
|
|
204
|
+
uid?: string;
|
|
213
205
|
error?: string;
|
|
214
206
|
}
|
|
215
207
|
/** Top 榜一行。 */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export type PoolStrategy = 'fallback' | 'round-robin';
|
|
2
1
|
/**
|
|
3
2
|
* 积分哨兵:插件还**没拿到过**真实额度(刚重启、积分拉取失败、或压根不支持
|
|
4
3
|
* 积分)时报 `-1`,核心据此保留上一次的缓存值,而不是把 0 写进去。
|
|
@@ -13,7 +12,6 @@ export interface SupplierConfig {
|
|
|
13
12
|
disabled: string[];
|
|
14
13
|
custom: string[];
|
|
15
14
|
poolOrder: string[];
|
|
16
|
-
poolStrategy: PoolStrategy;
|
|
17
15
|
/** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
|
|
18
16
|
credits: Record<string, number>;
|
|
19
17
|
/** 每连接显示别名:uid → 显示名(空/缺省用供应商原始昵称,纯展示不改路由键)。 */
|
|
@@ -46,7 +44,6 @@ export declare class SupplierConfigStore {
|
|
|
46
44
|
addCustomModel(supplierId: string, modelId: string): void;
|
|
47
45
|
removeCustomModel(supplierId: string, modelId: string): void;
|
|
48
46
|
setPoolOrder(supplierId: string, uids: string[]): void;
|
|
49
|
-
setPoolStrategy(supplierId: string, strategy: string): void;
|
|
50
47
|
/** 读某连接的显示别名;没设或为空返回 `undefined`(用供应商原始昵称)。 */
|
|
51
48
|
getAccountName(supplierId: string, uid: string): string | undefined;
|
|
52
49
|
/** 设置连接显示名。空名 = 删除别名(回到供应商原始昵称)。 */
|
|
@@ -31,8 +31,8 @@ export interface SupplierEnv {
|
|
|
31
31
|
*
|
|
32
32
|
* 核心实现 = `pool.noteFailure(uid, model, state, message)`,按该状态的规则
|
|
33
33
|
* 冷却。真实案例:TRAE 免费通道对某账号拒绝某模型时发 HTTP 200 +
|
|
34
|
-
* 流内 `code=4008
|
|
35
|
-
* (2026-09-08
|
|
34
|
+
* 流内 `code=4008`,三个号里两个如此,选号一旦落到坏号就 2/3 请求直接失败
|
|
35
|
+
* (2026-09-08 实测)——有了这条通道,坏号被冷一次就退出选号。
|
|
36
36
|
*
|
|
37
37
|
* 插件拿不到也不用兜底:不调用就退化成今天的行为,不影响正确性。
|
|
38
38
|
*/
|
|
@@ -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 拉取上游模型
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|
|
@@ -65,13 +65,13 @@
|
|
|
65
65
|
},
|
|
66
66
|
"license": "MIT",
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
68
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
72
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.
|
|
74
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.
|
|
72
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
75
75
|
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
76
76
|
"cordis": "^4.0.0-rc.7",
|
|
77
77
|
"react": "^18.2.0",
|
|
@@ -89,16 +89,15 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
|
-
"@
|
|
93
|
-
"@types/
|
|
94
|
-
"@types/react": "~19.
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"react": "
|
|
98
|
-
"react-dom": "19.2.8",
|
|
92
|
+
"@types/node": "^26.6.2",
|
|
93
|
+
"@types/react": "~19.3.0",
|
|
94
|
+
"@types/react-dom": "~19.3.0",
|
|
95
|
+
"cordis": "4.0.0-rc.10",
|
|
96
|
+
"react": "^19.3.0",
|
|
97
|
+
"react-dom": "19.3.0",
|
|
99
98
|
"tsdown": "^0.23.0",
|
|
100
99
|
"typescript": "^7.0.2",
|
|
101
|
-
"vite": "8.
|
|
100
|
+
"vite": "8.3.0",
|
|
102
101
|
"vite-plugin-css-injected-by-js": "^5.0.2"
|
|
103
102
|
},
|
|
104
103
|
"publishConfig": {
|
package/src/client/CombosTab.tsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 组合 — 布局交互贴近 9router combos 页。
|
|
3
3
|
* 组合 = 命名的一组模型(完整名 alias/id),请求 model 传组合名时,
|
|
4
|
-
*
|
|
4
|
+
* 作为降级链按序尝试命中其中一个。可创建/编辑/删除。
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useRef, useState } from 'react'
|
|
7
7
|
import {
|
|
8
8
|
ROUTER_API_BASE,
|
|
9
9
|
type RouterCombo,
|
|
10
10
|
type RouterComboAlias,
|
|
11
|
-
type RouterComboStrategy,
|
|
12
11
|
type RouterComboSupplierGroup,
|
|
13
12
|
} from '../shared.ts'
|
|
14
13
|
import { Modal } from './Modal.tsx'
|
|
@@ -44,7 +43,6 @@ interface CombosTabProps {
|
|
|
44
43
|
|
|
45
44
|
interface ComboForm {
|
|
46
45
|
name: string
|
|
47
|
-
strategy: RouterComboStrategy
|
|
48
46
|
models: string[]
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -71,7 +69,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
71
69
|
}
|
|
72
70
|
const [showForm, setShowForm] = useState(false)
|
|
73
71
|
const [editing, setEditing] = useState<RouterCombo | null>(null)
|
|
74
|
-
const [form, setForm] = useState<ComboForm>({ name: '',
|
|
72
|
+
const [form, setForm] = useState<ComboForm>({ name: '', models: [] })
|
|
75
73
|
const [formError, setFormError] = useState('')
|
|
76
74
|
const [saving, setSaving] = useState(false)
|
|
77
75
|
const [deleteTarget, setDeleteTarget] = useState<RouterCombo | null>(null)
|
|
@@ -115,7 +113,6 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
115
113
|
const openCreate = (): void => {
|
|
116
114
|
setForm({
|
|
117
115
|
name: '',
|
|
118
|
-
strategy: 'fallback',
|
|
119
116
|
models: [],
|
|
120
117
|
})
|
|
121
118
|
setModelQuery('')
|
|
@@ -128,7 +125,6 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
128
125
|
const openEdit = (combo: RouterCombo): void => {
|
|
129
126
|
setForm({
|
|
130
127
|
name: combo.name,
|
|
131
|
-
strategy: combo.strategy,
|
|
132
128
|
models: [...combo.models],
|
|
133
129
|
})
|
|
134
130
|
setModelQuery('')
|
|
@@ -191,8 +187,8 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
191
187
|
method: 'POST',
|
|
192
188
|
headers: { 'Content-Type': 'application/json' },
|
|
193
189
|
body: JSON.stringify(isEdit
|
|
194
|
-
? { id: editing!.id, name: form.name.trim(),
|
|
195
|
-
: { name: form.name.trim(),
|
|
190
|
+
? { id: editing!.id, name: form.name.trim(), models: form.models }
|
|
191
|
+
: { name: form.name.trim(), models: form.models }),
|
|
196
192
|
cache: 'no-store',
|
|
197
193
|
})
|
|
198
194
|
const data = await response.json() as { ok: boolean; error?: string }
|
|
@@ -237,24 +233,6 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
237
233
|
}
|
|
238
234
|
}
|
|
239
235
|
|
|
240
|
-
const updateStrategy = async (id: string, strategy: RouterComboStrategy): Promise<void> => {
|
|
241
|
-
const combo = combos.find(c => c.id === id)
|
|
242
|
-
if (!combo) return
|
|
243
|
-
const response = await fetch(`${ROUTER_API_BASE}/combos/update`, {
|
|
244
|
-
method: 'POST',
|
|
245
|
-
headers: { 'Content-Type': 'application/json' },
|
|
246
|
-
body: JSON.stringify({ id, name: combo.name, strategy, models: combo.models }),
|
|
247
|
-
cache: 'no-store',
|
|
248
|
-
})
|
|
249
|
-
const data = await response.json() as { ok: boolean; error?: string }
|
|
250
|
-
if (data.ok) {
|
|
251
|
-
onRefresh()
|
|
252
|
-
showToast(`策略已切换为${strategy === 'round-robin' ? '轮询' : '回退'}`)
|
|
253
|
-
} else {
|
|
254
|
-
showToast(data.error ?? '更新失败')
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
236
|
return (
|
|
259
237
|
<div className="dshr-tabBody">
|
|
260
238
|
{/* Header */}
|
|
@@ -262,15 +240,14 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
262
240
|
<div className="dshr-cardHead">
|
|
263
241
|
<span className="dshr-cardIcon"><Icon d={I.layers} size={16} /></span>
|
|
264
242
|
<div className="dshr-cardTitle">组合</div>
|
|
265
|
-
<span className="dshr-muted dshr-comboIntroDesc"
|
|
243
|
+
<span className="dshr-muted dshr-comboIntroDesc">请求传组合名,在一组模型中按序降级命中</span>
|
|
266
244
|
<button type="button" className="dshr-primaryButton dshr-cardAction" onClick={openCreate}>
|
|
267
245
|
<Icon d={I.add} size={14} />
|
|
268
246
|
创建组合
|
|
269
247
|
</button>
|
|
270
248
|
</div>
|
|
271
249
|
<div className="dshr-comboIntroBody">
|
|
272
|
-
<span className="dshr-comboHeaderKey"
|
|
273
|
-
<span className="dshr-comboHeaderKey dshr-comboHeaderKey-2">轮询</span> 在模型间轮转分配请求
|
|
250
|
+
<span className="dshr-comboHeaderKey">降级链</span> 按顺序尝试,前面失败时用下一个
|
|
274
251
|
</div>
|
|
275
252
|
</section>
|
|
276
253
|
|
|
@@ -294,15 +271,6 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
294
271
|
<code className="dshr-comboName">{combo.name}</code>
|
|
295
272
|
<span className="dshr-muted dshr-cardMeta">{combo.models.length} 模型</span>
|
|
296
273
|
<div className="dshr-comboOps">
|
|
297
|
-
<select
|
|
298
|
-
className="dshr-input dshr-comboStrategySelect"
|
|
299
|
-
value={combo.strategy}
|
|
300
|
-
title="组合策略"
|
|
301
|
-
onChange={(e) => void updateStrategy(combo.id, e.target.value as RouterComboStrategy)}
|
|
302
|
-
>
|
|
303
|
-
<option value="fallback">回退</option>
|
|
304
|
-
<option value="round-robin">轮询</option>
|
|
305
|
-
</select>
|
|
306
274
|
<button type="button" className="dshr-iconBtn dshr-iconBtn-sm" title="复制组合名" onClick={() => void copyName(combo.name, combo.id)}>
|
|
307
275
|
<Icon d={copied === combo.id ? I.check : I.copy} size={15} />
|
|
308
276
|
</button>
|
|
@@ -333,9 +301,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
333
301
|
)}
|
|
334
302
|
</div>
|
|
335
303
|
<div className="dshr-muted dshr-reason">
|
|
336
|
-
|
|
337
|
-
? '按此顺序尝试模型;当前模型不可用时换下一个。'
|
|
338
|
-
: '请求在模型间轮转分配。'}
|
|
304
|
+
按此顺序尝试模型;当前模型不可用时换下一个。
|
|
339
305
|
</div>
|
|
340
306
|
</div>
|
|
341
307
|
</section>
|
|
@@ -349,7 +315,7 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
349
315
|
<div className="dshr-modalForm">
|
|
350
316
|
<div className="dshr-comboFormSplit">
|
|
351
317
|
<div className="dshr-comboFormDivider" aria-hidden="true" />
|
|
352
|
-
{/* 左:组合名 +
|
|
318
|
+
{/* 左:组合名 + 已选模型 */}
|
|
353
319
|
<div className="dshr-comboFormLeft">
|
|
354
320
|
<div>
|
|
355
321
|
<p className="dshr-formLabel">组合名</p>
|
|
@@ -362,17 +328,6 @@ export function CombosTab({ combos, aliases, onRefresh }: CombosTabProps): JSX.E
|
|
|
362
328
|
/>
|
|
363
329
|
<p className="dshr-formHint">只能含字母、数字、-、_ 和 .</p>
|
|
364
330
|
</div>
|
|
365
|
-
<div>
|
|
366
|
-
<p className="dshr-formLabel">策略</p>
|
|
367
|
-
<select
|
|
368
|
-
className="dshr-input"
|
|
369
|
-
value={form.strategy}
|
|
370
|
-
onChange={(e) => patchForm({ strategy: e.target.value as RouterComboStrategy })}
|
|
371
|
-
>
|
|
372
|
-
<option value="fallback">回退 — 按顺序尝试,失败换下一个</option>
|
|
373
|
-
<option value="round-robin">轮询 — 在模型间轮转分配</option>
|
|
374
|
-
</select>
|
|
375
|
-
</div>
|
|
376
331
|
<div>
|
|
377
332
|
<p className="dshr-formLabel">模型(已选 {form.models.length})</p>
|
|
378
333
|
{form.models.length === 0 ? (
|