dsh-router-core 0.4.0 → 0.4.2
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 +174 -38
- package/cordis.patch.yml +22 -0
- package/docs/ext.md +29 -6
- package/docs/suppliers.md +28 -5
- package/lib/client-registry.js +939 -132
- package/lib/client.js +939 -132
- package/lib/ext-test/index.js +47 -0
- package/lib/ext-test/locale/en.json +6 -0
- package/lib/ext-test/locale/zh.json +6 -0
- package/lib/ext-test/package.json +5 -0
- package/lib/http-state-C1vb3gW2.js +48 -0
- package/lib/index.js +838 -89
- package/lib/suppliers/{nvidia.js → nvidia/index.js} +14 -6
- package/lib/suppliers/nvidia/locale/en.json +6 -0
- package/lib/suppliers/nvidia/locale/zh.json +6 -0
- package/lib/suppliers/nvidia/package.json +5 -0
- package/lib/suppliers/{opencode.js → opencode/index.js} +14 -6
- package/lib/suppliers/opencode/locale/en.json +6 -0
- package/lib/suppliers/opencode/locale/zh.json +6 -0
- package/lib/suppliers/opencode/package.json +5 -0
- package/lib/suppliers/{openrouter.js → openrouter/index.js} +14 -6
- package/lib/suppliers/openrouter/locale/en.json +6 -0
- package/lib/suppliers/openrouter/locale/zh.json +6 -0
- package/lib/suppliers/openrouter/package.json +5 -0
- package/lib/types/ext/contract.d.ts +9 -0
- package/lib/types/ext-test/index.d.ts +21 -0
- package/lib/types/ext-test/plugin.d.ts +23 -0
- package/lib/types/host-base-url.d.ts +31 -0
- package/lib/types/llm/adapter.d.ts +40 -4
- package/lib/types/router/index.d.ts +61 -8
- package/lib/types/shared.d.ts +12 -0
- package/lib/types/supplier-config.d.ts +15 -0
- package/lib/types/suppliers/contract.d.ts +35 -2
- package/lib/types/suppliers/http-state.d.ts +23 -0
- package/lib/types/suppliers/loader.d.ts +6 -4
- package/lib/types/suppliers/nvidia/index.d.ts +18 -0
- package/lib/types/suppliers/nvidia/plugin.d.ts +13 -0
- package/lib/types/suppliers/opencode/index.d.ts +18 -0
- package/lib/types/suppliers/opencode/plugin.d.ts +7 -0
- package/lib/types/suppliers/openrouter/index.d.ts +18 -0
- package/lib/types/suppliers/openrouter/plugin.d.ts +11 -0
- package/lib/types/suppliers/probe.d.ts +160 -0
- package/lib/types/suppliers/registry.d.ts +3 -1
- package/package.json +39 -4
- package/src/client/ExtDetail.tsx +21 -22
- package/src/client/ExtTab.tsx +25 -42
- package/src/client/ExtTestPanel.tsx +392 -0
- package/src/client/RouterComponentsSection.tsx +241 -0
- package/src/client/RouterView.tsx +12 -5
- package/src/client/comp-button-scope.test.ts +58 -0
- package/src/client/comp-panel-layout.test.ts +58 -0
- package/src/client/env.d.ts +38 -1
- package/src/client/ext-panels.test.ts +44 -0
- package/src/client/ext-panels.ts +49 -0
- package/src/client/index.tsx +41 -0
- package/src/client/model-choice.test.ts +41 -0
- package/src/client/model-choice.ts +37 -0
- package/src/client/router-components.test.ts +114 -0
- package/src/client/router-components.ts +104 -0
- package/src/client/router.css +490 -9
- package/src/client/supplier-visibility.test.ts +57 -0
- package/src/client/supplier-visibility.ts +63 -0
- package/src/ext/contract.ts +9 -0
- package/src/ext-test/ext-test.test.ts +60 -0
- package/src/ext-test/index.ts +35 -0
- package/src/ext-test/locale/en.json +6 -0
- package/src/ext-test/locale/zh.json +6 -0
- package/src/ext-test/plugin.ts +34 -0
- package/src/ext-test/row.json +5 -0
- package/src/host-base-url.test.ts +129 -0
- package/src/host-base-url.ts +43 -0
- package/src/index.ts +46 -11
- package/src/llm/adapter.test.ts +101 -4
- package/src/llm/adapter.ts +89 -20
- package/src/router/disconnect-leak.test.ts +7 -12
- package/src/router/first-byte-budget.test.ts +160 -0
- package/src/router/index.ts +255 -35
- package/src/router/models-cache.test.ts +45 -0
- package/src/router/supplier-switch.test.ts +220 -0
- package/src/shared.ts +12 -0
- package/src/supplier-config.ts +32 -1
- package/src/suppliers/contract.ts +54 -2
- package/src/suppliers/loader.test.ts +36 -2
- package/src/suppliers/loader.ts +33 -18
- package/src/suppliers/nvidia/index.ts +27 -0
- package/src/suppliers/nvidia/locale/en.json +6 -0
- package/src/suppliers/nvidia/locale/zh.json +6 -0
- package/src/suppliers/nvidia/row.json +5 -0
- package/src/suppliers/opencode/index.ts +27 -0
- package/src/suppliers/opencode/locale/en.json +6 -0
- package/src/suppliers/opencode/locale/zh.json +6 -0
- package/src/suppliers/opencode/row.json +5 -0
- package/src/suppliers/openrouter/index.ts +27 -0
- package/src/suppliers/openrouter/locale/en.json +6 -0
- package/src/suppliers/openrouter/locale/zh.json +6 -0
- package/src/suppliers/openrouter/row.json +5 -0
- package/src/suppliers/probe.test.ts +266 -0
- package/src/suppliers/probe.ts +475 -0
- package/src/suppliers/registry.ts +102 -3
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
<a href="#快速安装">快速安装</a> ·
|
|
15
15
|
<a href="#面板设置--路由">面板</a> ·
|
|
16
16
|
<a href="#agent-team-支持">Agent Team</a> ·
|
|
17
|
-
<a href="#api-端点openai-兼容
|
|
17
|
+
<a href="#api-端点openai-兼容v1">API 端点</a> ·
|
|
18
18
|
<a href="docs/suppliers.md">供应商开发</a> ·
|
|
19
19
|
<a href="docs/ext.md">扩展开发</a>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
**插件版的 9router** —— 不是另开一个网关服务,而是直接作为 DSH 插件嵌进 DSH web,
|
|
23
|
-
|
|
23
|
+
在宿主 Web 服务的 `/v1` 上原生暴露 OpenAI 兼容端点(`http://127.0.0.1:<宿主页端口>/v1`;`dsh web` 默认 3080,面板「端点与密钥」页显示的是实际地址),把请求路由到内部供应商。
|
|
24
24
|
管理界面在**设置 → 路由**(官方设置页座位,不是自己开的页面)。装好即用,
|
|
25
25
|
不用多开一个 9router、不用维护第二个端口、不用在网关和 DSH 之间搬配置。
|
|
26
26
|
|
|
@@ -41,6 +41,10 @@ dsh plugin --profile web add dsh-router-core
|
|
|
41
41
|
|
|
42
42
|
然后**重启 `dsh web`**。打开设置面板,左侧导航「模型」下面会出现 **路由**。
|
|
43
43
|
|
|
44
|
+
第二处入口是侧栏的**插件**面板:内置的供应商与扩展在那里各占一行「包含的组件」,
|
|
45
|
+
**供应商与扩展的开关也在那一面**(见 [面板](#面板设置--路由)与
|
|
46
|
+
[供应商开关](#供应商开关))。
|
|
47
|
+
|
|
44
48
|
> 更多供应商:DSH 插件形态的供应商各自发 npm 包,同样
|
|
45
49
|
> `dsh plugin --profile web add <包名>` 即可;供应商接入与开发见
|
|
46
50
|
> [`docs/suppliers.md`](docs/suppliers.md)。
|
|
@@ -53,8 +57,8 @@ dsh plugin --profile web add dsh-router-core
|
|
|
53
57
|
| 能力 | 说明 |
|
|
54
58
|
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
55
59
|
| 零额外进程 | 就是 DSH 插件,随 `dsh web` 启停,天然同源(`/router/api/*` 无 CORS、面板嵌在设置里)。 |
|
|
56
|
-
| 扩展即插即拔 | 扩展插件(如 [`dsh-router-ext-rtk`](https://github.com/CARVIN94/dsh-router-ext-rtk))经 `router.ext` 注册,在 bash 执行前改写命令(如加 `rtk` 前缀压缩输出)
|
|
57
|
-
| 供应商即插即拔 |
|
|
60
|
+
| 扩展即插即拔 | 扩展插件(如 [`dsh-router-ext-rtk`](https://github.com/CARVIN94/dsh-router-ext-rtk))经 `router.ext` 注册,在 bash 执行前改写命令(如加 `rtk` 前缀压缩输出)。开关在官方插件页(设置 → 插件 → dsh-router-core),带自检;面板「扩展」页只列已启用的、只读。 |
|
|
61
|
+
| 供应商即插即拔 | 内置供应商是插件页「包含的组件」里的三行,可单独开关;更多供应商 = 装一个 DSH 插件(`dsh-router-*`)或放一个 js 文件到 `~/.dsh/profiles/web/suppliers/`(同名 js 覆盖内置)。 |
|
|
58
62
|
| 模型不内置 | 供应商只实现差异化能力,模型拉取与缓存由核心统一管,不写死、不过时。 |
|
|
59
63
|
| 策略只写一次 | 组合回退、账号池(选号/冷却/禁用)、响应写入、凭证存储、积分持久化、模型管理都由核心提供。供应商 js 只对**单个账号**调一次上游并报告成败,不自己遍历账号、不维护冷却表、不落盘积分——否则每个插件都会长出一份互相不一致的实现,而核心也就无从判断「该不该换号」。 |
|
|
60
64
|
| 凭证单库 | `auths/credentials.sqlite`,供应商凭证不透明 blob,核心统一生命周期,干净可备份。 |
|
|
@@ -102,6 +106,19 @@ team 测试就是多个会话交错发请求。dsh-router 原生适配这个场
|
|
|
102
106
|
|
|
103
107
|
面板挂在 **设置 → 路由**(官方 `settings.section` 座位,排在「模型」下面):
|
|
104
108
|
|
|
109
|
+
第二处入口是**设置 → 插件 → dsh-router-core 的详情页**,那里分两层:
|
|
110
|
+
|
|
111
|
+
- 宿主的原生**「包含的组件」**列 core 自己 patch 里声明的**行**:core 本体、三个
|
|
112
|
+
内置供应商、插件自检这个内置扩展。每行一个宿主管的开关,关一行 = loader 不
|
|
113
|
+
import 它 = 它压根没注册。
|
|
114
|
+
- 原生列表**下面**是本插件自绘的**「路由组件」**一节,列**外部供应商插件**与
|
|
115
|
+
**扩展插件**,两组都带开关。
|
|
116
|
+
|
|
117
|
+
两层的分工:**已经在原生行里的(内置的那些)不进自绘节** —— 同一份东西列两遍,
|
|
118
|
+
用户就不知道该在哪开关。反过来说外部供应商与扩展进不了原生行:官方那个列表只认
|
|
119
|
+
**本 bundle 自己 patch 里声明的行**(`declaredRows` 只读该 bundle 自己的 patch),
|
|
120
|
+
而它们各自是独立 bundle,自己的行在自己的详情页上。
|
|
121
|
+
|
|
105
122
|
- **概览** — 用量看板(默认页):
|
|
106
123
|
- 周期切换 **今日 / 24 小时 / 7 天 / 30 天**;
|
|
107
124
|
- 汇总卡:总请求(含成功率)、输入 Tokens、输出 Tokens、缓存 Tokens、平均耗时(含首字节);
|
|
@@ -143,11 +160,15 @@ team 测试就是多个会话交错发请求。dsh-router 原生适配这个场
|
|
|
143
160
|
为 DSH 模型目录里的 `router` provider 选项(设置 → 模型直接选组合名即可用),请求
|
|
144
161
|
按组合策略命中其中一个供应商模型;
|
|
145
162
|
- **端点与密钥** — 端点核心(无隧道/Tailscale):
|
|
146
|
-
- API 端点 URL(`
|
|
163
|
+
- API 端点 URL(面板按当前页面 origin 显示,可复制;端口跟着宿主走,`dsh web` 默认 3080);
|
|
147
164
|
- 鉴权设置 `requireApiKey` 开关;
|
|
148
165
|
- API Keys 管理:创建 / 启用切换 / 显示 / 复制 / 删除(持久化到 `data/keys.json`)。
|
|
149
166
|
|
|
150
|
-
## API 端点(OpenAI
|
|
167
|
+
## API 端点(OpenAI 兼容,`/v1`)
|
|
168
|
+
|
|
169
|
+
> 端口 = **宿主 Web 服务实际监听的端口**:`dsh web` 默认 3080,桌面端等通道由宿主
|
|
170
|
+
> 分配(不一定还是 3080)。下面按 3080 写;换通道时把端口换成面板「端点与密钥」
|
|
171
|
+
> 页里显示的那个即可。
|
|
151
172
|
|
|
152
173
|
```bash
|
|
153
174
|
# 模型列表
|
|
@@ -160,7 +181,7 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
160
181
|
```
|
|
161
182
|
|
|
162
183
|
任何支持 OpenAI 兼容 API 的工具(Claude Code、Cline、DSH 设置-模型 等)都可以把
|
|
163
|
-
`baseURL`
|
|
184
|
+
`baseURL` 指向面板「端点与密钥」页里显示的那个端点(`dsh web` 下即 `http://localhost:3080/v1`)。
|
|
164
185
|
|
|
165
186
|
**鉴权**:默认 `requireApiKey=false`,`/v1/*` 不要求鉴权(本地使用,与 9router 一致)。
|
|
166
187
|
在「端点与密钥」页开启「要求 API Key」后,请求必须带
|
|
@@ -170,7 +191,7 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
170
191
|
|
|
171
192
|
| 端点 | 方法 | 说明 |
|
|
172
193
|
| ------------------------------- | --------- | ---------------------------------------------------------------------- |
|
|
173
|
-
| `/health` | GET | 供应商列表(
|
|
194
|
+
| `/health` | GET | 供应商列表(含来源/能力/开关 `enabled`)+ 宿主版本 `hostVersion` / 徽章支持位 `lastHitDock` |
|
|
174
195
|
| `/last-hit` | GET | 最近一次命中 `?session=<id>`(按会话取,缺省全局);供「路由」徽章 |
|
|
175
196
|
| `/status` | GET | 全部账号(含供应商 id) |
|
|
176
197
|
| `/models` | GET | 合并模型列表(已过滤禁用) |
|
|
@@ -185,28 +206,128 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
185
206
|
| `/stats/clear` | POST | 清空全部用量统计 |
|
|
186
207
|
| `/suppliers/:id/login` | POST | 生成登录链接 |
|
|
187
208
|
| `/suppliers/:id/login/callback` | POST | `{callbackUrl}` → 加账号 |
|
|
209
|
+
| `/suppliers/:id/enabled` | PATCH | 供应商开关 `{enabled}` —— 关掉不参与路由(见下) |
|
|
188
210
|
| `/suppliers/:id/models` | GET | 模型 + 启用状态 |
|
|
189
211
|
| `/suppliers/:id/models/toggle` | POST | `{id, enabled}` |
|
|
212
|
+
| `/suppliers/:id/models/test` | POST | `{id, uid?}` 测模型可用性;`uid` = 只测这个连接(不回退),省略则池内任选 |
|
|
213
|
+
|
|
214
|
+
## 供应商开关
|
|
215
|
+
|
|
216
|
+
每个供应商有**一个开关**,决定它是否参与路由(默认开)。落盘在
|
|
217
|
+
`supplier-config.json` 的 `enabled`(与扩展开关的 `ext.json` 同一层:都是用户对
|
|
218
|
+
核心面板的操作,归核心持久化)。
|
|
219
|
+
|
|
220
|
+
关掉之后**真的不参与路由**,不是只把卡片藏起来 —— 核心的活跃集合(`Router` 的
|
|
221
|
+
`active`)里就没有它了:
|
|
222
|
+
|
|
223
|
+
- 请求不会落到它(组合腿与 `alias/model` 直调两个入口都拦);
|
|
224
|
+
- 它的模型前缀不再出现在对外模型全名里,模型列表与组合里也不再出现它
|
|
225
|
+
(避免留下「前缀存在但必然 503」的死前缀);
|
|
226
|
+
- 面板「供应商」页**不再列它的卡片**(与「扩展」页同一形状:面板是在用什么的
|
|
227
|
+
工作面,只列开着的)。全关掉时那句空状态是「供应商都已关闭」并指去开关所在处 ——
|
|
228
|
+
不复用「暂无供应商」,那句说的是一个都没装,用来描述「装了但自己关的」就是说假话。
|
|
229
|
+
|
|
230
|
+
但它**仍留在 `/health` 与插件页那一节**(含关掉的),否则关掉之后就再没有可点的开关能
|
|
231
|
+
把它开回来。开关 UI 在**设置 → 插件 → dsh-router-core 详情页**下面的「路由组件」一节
|
|
232
|
+
(「供应商」组 + 「扩展」组)。
|
|
233
|
+
|
|
234
|
+
## 插件自检(内置扩展 `dsh-router-ext-test`)
|
|
235
|
+
|
|
236
|
+
随核心分发的内置扩展,提供两块自检面板。它**不挂任何监听、不改写任何命令** ——
|
|
237
|
+
与 RTK 那类扩展的区别只在于它只提供面板。
|
|
238
|
+
|
|
239
|
+
1. **连接测试**:选**供应商 / 模型 / 连接**,跑一次真实的访问测试。
|
|
240
|
+
2. **契约体检**:选一个**外部供应商插件**,逐个契约成员报告「实现了吗 / 实跑通了吗 /
|
|
241
|
+
为什么没跑」。
|
|
242
|
+
|
|
243
|
+
它与三个内置供应商走同一套管道:是 `cordis.patch.yml` 里的一个**行**(子路径模块
|
|
244
|
+
`dsh-router-core/ext-test`),因此在插件页原生「包含的组件」里占一行、开关由宿主管,
|
|
245
|
+
且**默认关闭**。行关闭时 loader 根本不 import 这个模块,扩展就不在 `router.ext` 表里 ——
|
|
246
|
+
「打开那一行才出现这张卡片」是天然的,不需要额外的状态位。
|
|
247
|
+
|
|
248
|
+
面板入口**只有一处**:设置 → 路由 → 扩展 → 点开「插件自检」。插件页那一行不挂详情页,
|
|
249
|
+
它只负责开关(理由见 [扩展插件](#扩展插件routerext) 一节)。
|
|
250
|
+
|
|
251
|
+
测试走核心已有的 `POST /suppliers/:id/models/test`,与面板「供应商」详情里的「测试」
|
|
252
|
+
按钮**同一条真实路径**(账号遍历 + chatOnce)。区别只在可以**钉死连接**:留空则由账号池
|
|
253
|
+
任选可用号(与那个按钮同义);指定连接则只测它、**失败不回退到别的号** —— 否则
|
|
254
|
+
「测这个连接」测出来的是另一个号的结果,诊断就成了假话。
|
|
255
|
+
|
|
256
|
+
面板的模型下拉**只列可用模型**:那个端点给的是全部模型(含用户在供应商详情里停用的),
|
|
257
|
+
把停用的也列出来,用户会挑一个自己明明关掉的模型去测。列表为空时还区分「一个模型都
|
|
258
|
+
没有」(该去拉取)与「全被停用」(该去开启)—— 共用一句「没有模型」会把用户往错的方向指。
|
|
259
|
+
|
|
260
|
+
### 契约体检:报告,不是代替你操作
|
|
261
|
+
|
|
262
|
+
`POST /router/api/suppliers/:id/probe` 逐个成员给状态:`ok`(实现了且实跑通)/
|
|
263
|
+
`fail`(必填成员缺失,或实跑抛错)/ `absent`(可选成员没实现)/ `skipped`(实现了但**故意
|
|
264
|
+
不跑**)。
|
|
265
|
+
|
|
266
|
+
**有副作用的一律不自动执行**,只标出它实现了并说明为什么:
|
|
267
|
+
|
|
268
|
+
| 成员 | 为什么不给自动跑 |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| `dispose` | 调用它就是把这个供应商卸载掉 |
|
|
271
|
+
| `addApiKey` / `removeLink` | 写/删凭证 |
|
|
272
|
+
| `generateLoginUrl` | 可能直接触发设备码 / OAuth 登录流 |
|
|
273
|
+
| `completeLogin` | 需要一个真实回调 URL,空调没有意义 |
|
|
274
|
+
| `checkinNow` | 替这个连接真的去签到 |
|
|
275
|
+
| `chatOnce` | 会真发一次请求(消耗额度)——用上面的「跑一次访问测试」单独测 |
|
|
276
|
+
|
|
277
|
+
体检是给眼睛看的,不是给手用的:要触发上面这些,去供应商详情里点对应的按钮。
|
|
278
|
+
|
|
279
|
+
清单与契约**必须一起长**:`src/suppliers/probe.ts` 的成员表有一条判据直接解析
|
|
280
|
+
`contract.ts` 源码里的 `SupplierModule` 成员名并断言全覆盖(编译期抓不到这种「两份
|
|
281
|
+
清单」),所以契约新增成员而体检漏检会当场红。
|
|
190
282
|
|
|
191
283
|
## 扩展插件(`router.ext`)
|
|
192
284
|
|
|
193
285
|
> 完整契约、注册方式、自检与降级约定见 [`docs/ext.md`](docs/ext.md)。
|
|
194
286
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
287
|
+
扩展插件是**独立 npm 包**(如 [`dsh-router-ext-rtk`](https://github.com/CARVIN94/dsh-router-ext-rtk)),
|
|
288
|
+
经 cordis service `router.ext` 把自己追加进核心持有的共享表 —— 与 `router.suppliers`
|
|
289
|
+
同一模式,与加载顺序无关。
|
|
290
|
+
|
|
291
|
+
### 契约
|
|
292
|
+
|
|
293
|
+
注册进表的是一个**声明 + 状态**的对象,核心只认这几个成员:
|
|
294
|
+
|
|
295
|
+
| 成员 | 说明 |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| `id` | 扩展器 id(表的键,也是 `ext.json` 的键) |
|
|
298
|
+
| `name` / `description?` / `icon?` | 面板显示名 / 副标题 / 图标 |
|
|
299
|
+
| `getState()` | **运行时事实**:`{ ready, detail? }`,现算不落盘 |
|
|
300
|
+
| `dispose?` | 表里被移除时核心调用 |
|
|
301
|
+
| `source?: 'builtin'` | 标了就是随核心分发(见下) |
|
|
302
|
+
|
|
303
|
+
**没有 `rewrite`** —— 怎么改命令是插件自己的实现细节,核心不感知、不调用。**开关也不
|
|
304
|
+
在契约里**:开关归核心持久化(`<dataDir>/ext.json`,默认关);插件要存自己的数据走
|
|
305
|
+
`router.extStore` 的 `readData/writeData`,不自己 file IO(落盘位置由核心锚定,
|
|
306
|
+
不跟 cwd 跑)。
|
|
307
|
+
|
|
308
|
+
### 面板与开关
|
|
309
|
+
|
|
310
|
+
面板「扩展」页只列**已启用**的扩展(关闭的连卡片都不出现),整页只读,详情页也没有开关
|
|
311
|
+
—— 启停只有一个入口:插件页 dsh-router-core 详情页的「路由组件」一节
|
|
312
|
+
(`PATCH /router/api/ext`)。开启时会**自检**:`getState().ready === false` 的拒绝开启
|
|
313
|
+
(API 409),那一面的开关禁用并显示原因。
|
|
314
|
+
|
|
315
|
+
### 扩展可以自带详情面板
|
|
316
|
+
|
|
317
|
+
扩展详情页原本是一张写死的只读页(名字、id、状态、一段说明),扩展自己想放的交互
|
|
318
|
+
(要选的模型、要跑的测试、要看的诊断)没地方放。客户端因此有一张按**扩展 id** 索引的
|
|
319
|
+
面板注册表(`src/client/ext-panels.ts` 的 `registerExtPanel`):登记了就用你的组件渲染
|
|
320
|
+
整个内容区,没登记才落回通用只读页。`插件自检` 就是这么用的。
|
|
199
321
|
|
|
200
|
-
|
|
322
|
+
⚠️ 这张注册表目前只在 dsh-router 自己的 client bundle 内,**外部插件包还引不到** ——
|
|
323
|
+
`dsh-router-core/client` 是整个入口闭包、不导出它。要让外部插件也能自带面板需要加一个
|
|
324
|
+
真正的导出路径,详见 [`docs/ext.md`](docs/ext.md)。
|
|
201
325
|
|
|
202
|
-
|
|
203
|
-
调用,把命令委派给表里 **enabled 且 ready** 的扩展器改写;命中则短路。只拦
|
|
204
|
-
`bash`,其他工具(含 `run_code` 体内自起的子进程)不动。
|
|
205
|
-
- **扩展插件**:实现 `rewrite(command)`(同步、不能做 IO)+ 自管开关状态
|
|
206
|
-
(何时 enabled、是否 ready、怎么持久化)。核心不感知具体扩展器实现。
|
|
326
|
+
### 内置扩展不在自绘节里重复列
|
|
207
327
|
|
|
208
|
-
|
|
209
|
-
|
|
328
|
+
随核心分发的扩展(`source: 'builtin'`)已经是插件页原生「包含的组件」里的一行、自带
|
|
329
|
+
宿主管的开关,自绘节因此**只列独立安装的扩展**。与内置供应商同理:已经有原生行的东西,
|
|
330
|
+
再列一遍就是同一个东西显示两处。
|
|
210
331
|
|
|
211
332
|
### 已知坑
|
|
212
333
|
|
|
@@ -218,15 +339,22 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
218
339
|
|
|
219
340
|
```
|
|
220
341
|
浏览器(client 半)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
342
|
+
├─ 设置 → 路由(settings.section 座位, order 10, 排在「模型」下面)
|
|
343
|
+
│ ├─ RouterSettingsSection 注册入口(settings-section.tsx)
|
|
344
|
+
│ ├─ RouterView tab: 概览 / 供应商 / 组合 / 扩展 / 端点与密钥(tab 条:下划线指示器)
|
|
345
|
+
│ │ ├─ StatsTab 概览:用量看板(周期按钮组 + 汇总卡 + 折线趋势 + Top 榜 + 最近请求)
|
|
346
|
+
│ │ ├─ SupplierDetail 供应商详情:链接池 + 加链接 + 可用模型 + 模型测试
|
|
347
|
+
│ │ ├─ CombosTab / EndpointTab
|
|
348
|
+
│ │ └─ ExtTab → ExtDetail 扩展列表(只列已启用)与详情(优先渲染扩展自带的面板)
|
|
349
|
+
│ └─ LastHitDock 「路由」徽章(composer.dock 座位,≥0.1.7;按会话显示最近命中)
|
|
350
|
+
├─ 设置 → 插件(官方 plugins.detail.section 座位)
|
|
351
|
+
│ ├─ (原生「包含的组件」) core patch 里声明的行:core 本体 + 内置供应商 + 内置扩展
|
|
352
|
+
│ │ 宿主管的开关,关一行 = loader 不 import 它
|
|
353
|
+
│ └─ RouterComponentsSection dsh-router-core 详情页自绘的「路由组件」一节
|
|
354
|
+
│ (外部供应商 + 扩展,两组都带开关)
|
|
355
|
+
│ └─ ExtTestPanel 插件自检扩展的详情面板(按 ext id 登记到 ext-panels.ts)
|
|
356
|
+
└─ fetch /router/api/* (同源,无 CORS)
|
|
357
|
+
└─ host 半(src/index.ts)
|
|
230
358
|
├─ /v1/models + /v1/chat/completions (OpenAI 兼容, KeysStore 鉴权)
|
|
231
359
|
│ └─ RouterAdapter(src/llm/adapter.ts) OpenAI SSE → DSH StreamChunk
|
|
232
360
|
│ (usage 经 toTokenUsage 转 DSH 契约,见 docs/suppliers.md)
|
|
@@ -234,22 +362,30 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
234
362
|
├─ host-version(src/host-version.ts) 按宿主版本号适配 0.1.5 / 0.1.7
|
|
235
363
|
├─ KeysStore(src/keys.ts) 密钥库 + requireApiKey
|
|
236
364
|
└─ Router(路由器) → suppliers[]
|
|
237
|
-
├─ OpenCodeSupplier(lib/suppliers/opencode.js) 无账号直连(Zen 免费档,需 CLI 握手)
|
|
238
|
-
├─ OpenRouterSupplier(lib/suppliers/openrouter.js) API key 账号
|
|
239
|
-
|
|
365
|
+
├─ OpenCodeSupplier(lib/suppliers/opencode/index.js) 无账号直连(Zen 免费档,需 CLI 握手)
|
|
366
|
+
├─ OpenRouterSupplier(lib/suppliers/openrouter/index.js) API key 账号
|
|
367
|
+
├─ NvidiaSupplier(lib/suppliers/nvidia/index.js) API key 账号
|
|
240
368
|
└─ 外部插件供应商(经 router.suppliers service 注册)
|
|
369
|
+
↑ 内置三个是 core 自己 patch 里的**行**(子路径模块),与外部插件同一条通道
|
|
370
|
+
└─ 扩展表 router.ext(core provide 空表,谁都能往里 append)
|
|
371
|
+
├─ 内置:插件自检(行 dsh-router-core/ext-test,默认关闭)
|
|
372
|
+
└─ 外部:dsh-router-ext-rtk 等独立 bundle 经同一个 service 注册
|
|
241
373
|
```
|
|
242
374
|
|
|
243
375
|
- **供应商抽象**:可插拔 js 模块只提供**差异化能力**(`status/listModels/getAlias/chatOnce`
|
|
244
376
|
- 可选登录/签到/加 key);**策略与通用能力**(组合回退、账号池选号/冷却/禁用、
|
|
245
377
|
连接池排序、模型启用/自定义、别名、凭证、响应写入)由核心统一管。
|
|
246
378
|
`chatOnce(uid, req)` 一次只服务一个账号,返回成功/失败 + 语义状态,换号由核心决定。
|
|
247
|
-
- **供应商加载**(
|
|
248
|
-
1.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
379
|
+
- **供应商加载**(两来源,见 [`docs/suppliers.md`](docs/suppliers.md)):
|
|
380
|
+
1. **经 cordis service `router.suppliers`**(值为
|
|
381
|
+
`{ [supplierId]: (env) => SupplierModule }`),dsh-router
|
|
382
|
+
`ctx.inject(['router.suppliers'])` 延迟加载。两条子路径都走它:
|
|
383
|
+
- **内置三个 = core 自己 patch 里的行**(`cordis.patch.yml` insert
|
|
384
|
+
`dsh-router-core/suppliers/<x>`),所以在官方插件页 dsh-router-core 详情页的
|
|
385
|
+
**「包含的组件」里各占一行、带着宿主管的开关** —— 关掉一行 = loader 不 import
|
|
386
|
+
它 = 供应商没注册 = 路由不会落到它;
|
|
387
|
+
- 外部 DSH 插件(独立安装的供应商包)经同一个 service 注册。
|
|
388
|
+
2. **用户目录**:`~/.dsh/profiles/<profile>/suppliers/*.js`,**压过**同名内置行。
|
|
253
389
|
- **模型统一策略**:插件不内置、不缓存模型;`listModels` 每次从上游拉取,
|
|
254
390
|
缓存由核心按 60s TTL 统一管(`/suppliers/:id/models`),`/v1/models` 保持实时。
|
|
255
391
|
- **凭证存储**:SQLite 单库 `{authDir}/credentials.sqlite`(表 `credentials(supplier, uid, data)`,
|
package/cordis.patch.yml
CHANGED
|
@@ -10,6 +10,28 @@
|
|
|
10
10
|
# `require.resolve(name + '/package.json')` 定位 client bundle。包名已改
|
|
11
11
|
# dsh-router-core,这里不同步就从 npm 装完两边都加载不到(`id` 只是配置树
|
|
12
12
|
# 里的句柄,可以随便取)。
|
|
13
|
+
#
|
|
14
|
+
# 下面除了核心自己,还 insert 了三个**内置供应商行**(子路径模块)。
|
|
15
|
+
# 为什么是行而不是「核心扫一个目录」:宿主的插件页「包含的组件」只认本 bundle
|
|
16
|
+
# 自己 patch 里声明的行,所以「装完有哪些内置供应商、怎么单独关掉一个」这件事,
|
|
17
|
+
# 做成行才有宿主管的开关(HMR、重载、profile patch 全是现成的)。关掉一行 =
|
|
18
|
+
# loader 不 import 它 = 供应商没注册 = 路由不会落到它,不是假开关。
|
|
19
|
+
#
|
|
20
|
+
# 子路径的 `name` 同样逐字重要:它要能在 package.json 的 `exports` 里解析到。
|
|
21
|
+
# 行的显示名/说明走**各自的** `package.json` + `locale/*.json`(子路径资源,
|
|
22
|
+
# 见 package.json exports)——所以每个供应商带自己的名字,不是三行都叫
|
|
23
|
+
# dsh-router-core。
|
|
13
24
|
- insert:
|
|
14
25
|
- id: dsh-router
|
|
15
26
|
name: 'dsh-router-core'
|
|
27
|
+
- id: dsh-router-opencode
|
|
28
|
+
name: 'dsh-router-core/suppliers/opencode'
|
|
29
|
+
- id: dsh-router-openrouter
|
|
30
|
+
name: 'dsh-router-core/suppliers/openrouter'
|
|
31
|
+
- id: dsh-router-nvidia
|
|
32
|
+
name: 'dsh-router-core/suppliers/nvidia'
|
|
33
|
+
# 连接自检扩展。**默认关闭**:行关闭时 loader 根本不 import 它,扩展就不在
|
|
34
|
+
# `router.ext` 表里 —— 所以「打开才出现这张卡片」是天然的,不需要额外状态位。
|
|
35
|
+
- id: dsh-router-ext-test
|
|
36
|
+
name: 'dsh-router-core/ext-test'
|
|
37
|
+
disabled: true
|
package/docs/ext.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# 扩展插件开发
|
|
2
2
|
|
|
3
3
|
扩展插件是**独立的 DSH 插件(npm 包)**,只提供**差异化能力** —— 目前是改写一条
|
|
4
|
-
bash 命令。装好后出现在「设置 → 路由 →
|
|
4
|
+
bash 命令。装好后出现在「设置 → 路由 → 扩展」,**开关不在那里** —— 启停的唯一入口是
|
|
5
|
+
官方插件页「设置 → 插件 → dsh-router-core」详情页的「路由组件」一节;面板这一页只列
|
|
6
|
+
已启用的扩展,只读。
|
|
5
7
|
|
|
6
8
|
**dsh-router 核心只做管理面,不做拦截。** 扩展插件自己挂监听、自己裁决、自己短路
|
|
7
9
|
(2026-09 重构,见下)。
|
|
@@ -9,6 +11,20 @@ bash 命令。装好后出现在「设置 → 路由 → 扩展」,每个一个
|
|
|
9
11
|
数据目录是 `<profile>/data/`,**不跟进程 cwd 跑**——从任何目录启动 `dsh web`
|
|
10
12
|
读到的都是同一份配置(见 `src/data-dir.ts`)。
|
|
11
13
|
|
|
14
|
+
## 扩展详情页可以自带内容
|
|
15
|
+
|
|
16
|
+
扩展详情页默认是一张**写死的只读页**(名字、id、状态、一段说明)。扩展若想在详情里放
|
|
17
|
+
自己的交互(要选的模型、要跑的测试、要看的诊断),可以按**扩展 id** 登记一个面板组件
|
|
18
|
+
(`src/client/ext-panels.ts` 的 `registerExtPanel`):登记后 `ExtDetail` 用你的组件渲染
|
|
19
|
+
整个内容区,没登记才落回那张通用只读页。组件**不收 props** —— 面板的渲染点只有详情页
|
|
20
|
+
一处。核心自带的 `插件自检` 就是这么做的。
|
|
21
|
+
|
|
22
|
+
⚠️ **目前这张注册表在 dsh-router 自己的 client bundle 内部,外部插件包还引不到它**:
|
|
23
|
+
`dsh-router-core/client` 是整个 client 入口(一个注册座位的 CJS 闭包),并不导出
|
|
24
|
+
`registerExtPanel`。要让外部插件也能自带面板,需要给它加一个真正的导出路径
|
|
25
|
+
(如 `./client/panels`)。这是已知的升级路径,不是现在就能用的写法 —— 照着现有 exports
|
|
26
|
+
去 `import { registerExtPanel } from 'dsh-router-core/client'` 会拿到 undefined。
|
|
27
|
+
|
|
12
28
|
参考实现:[dsh-router-ext-rtk](https://github.com/CARVIN94/dsh-router-ext-rtk)
|
|
13
29
|
(把命令改写成 `rtk <cmd>` 压缩输出)。
|
|
14
30
|
|
|
@@ -19,7 +35,7 @@ bash 命令。装好后出现在「设置 → 路由 → 扩展」,每个一个
|
|
|
19
35
|
| 持有 `router.ext` 注册表(发现) | 核心 |
|
|
20
36
|
| 面板渲染、开关交互、`/router/api/ext` | 核心 |
|
|
21
37
|
| **开关状态 + 插件数据落盘**(`router.extStore` → `<dataDir>/ext.json`) | **核心** |
|
|
22
|
-
| 开启自检(
|
|
38
|
+
| 开启自检(不可用时拒绝开启,插件页那一面的开关禁用) | 核心 |
|
|
23
39
|
| **挂 `tools/execute` 拦截** | **插件** |
|
|
24
40
|
| **按 enabled / ready 裁决、命中则短路执行** | **插件** |
|
|
25
41
|
| **怎么改写一条命令** | **插件**(私有,不在契约里) |
|
|
@@ -49,6 +65,8 @@ interface RouterExt {
|
|
|
49
65
|
readonly id: string // 唯一 id(注册键,如 'rtk')
|
|
50
66
|
readonly name: string // 面板显示名(如 'RTK')
|
|
51
67
|
readonly description?: string // 面板内容区说明
|
|
68
|
+
readonly icon?: string // 卡片/详情图标 URL
|
|
69
|
+
readonly source?: 'builtin' // 标了 = 随核心分发(见下);不标 = 独立插件
|
|
52
70
|
getState(): ExtState // 运行时事实
|
|
53
71
|
dispose?(): void
|
|
54
72
|
}
|
|
@@ -62,6 +80,11 @@ interface ExtState {
|
|
|
62
80
|
|
|
63
81
|
**没有 `rewrite`。** 怎么改命令是插件的实现细节,核心不感知、不调用。
|
|
64
82
|
|
|
83
|
+
**`source: 'builtin'`** 标的是「随核心分发」。这类扩展同时是插件页原生「包含的组件」
|
|
84
|
+
里的**一行**(自带宿主管的开关,关一行 = loader 不 import 它),因此插件页那个自绘的
|
|
85
|
+
「路由组件」节**只列独立安装的扩展**,不把它们重复列一遍。判定只认这一个字段,没有别的
|
|
86
|
+
连带影响。
|
|
87
|
+
|
|
65
88
|
插件经 `router.extStore` 读写(核心 provide):
|
|
66
89
|
|
|
67
90
|
```ts
|
|
@@ -85,9 +108,9 @@ interface ExtStoreService {
|
|
|
85
108
|
### `getState()`:只报运行时事实
|
|
86
109
|
|
|
87
110
|
- `ready: true` 才能生效;`false` 表示运行时不可用(如没装 rtk)。核心据此:
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
111
|
+
- 插件页的开关**禁用**,点不开
|
|
112
|
+
- 即使绕过界面直连 API 开启,核心也**拒 409** 并带上 `detail`
|
|
113
|
+
- 面板「扩展」页用红字显示 `detail`
|
|
91
114
|
- **不报 `enabled`** —— 开关在核心
|
|
92
115
|
- 建议在插件**启动时**就探测一次(而不是等第一次拦截),让面板首屏就有状态
|
|
93
116
|
|
|
@@ -200,7 +223,7 @@ tools.get('bash') // ❌ 只查全局视图,查不到 → 静默走
|
|
|
200
223
|
|
|
201
224
|
```bash
|
|
202
225
|
# 1) 看 API:扩展器 enabled / ready 是否都为 true
|
|
203
|
-
curl -s http://127.0.0.1:3080/router/api/ext
|
|
226
|
+
curl -s http://127.0.0.1:3080/router/api/ext # 端口 = 宿主页端口(面板可见),非 3080 的通道要换
|
|
204
227
|
|
|
205
228
|
# 2) 跑一条有等价改写的命令,看输出是否变成压缩格式
|
|
206
229
|
ls # 被改写时是 rtk 树形格式,不是原生列表
|
package/docs/suppliers.md
CHANGED
|
@@ -152,6 +152,21 @@ type ChatOnceResult =
|
|
|
152
152
|
重启后所有号都会重新被选中一次,所以**上游确实挂了的号会各撞一次**,属预期。
|
|
153
153
|
要跨重启保留,届时给账号池加一个落盘 state 文件即可,接口不用变。
|
|
154
154
|
|
|
155
|
+
### 首字节预算:body 停摆由核心兜,js 不要自己设
|
|
156
|
+
|
|
157
|
+
上游常有一种「连上了、200 响应头也回来了、body 却一个字节都不吐」的停摆
|
|
158
|
+
(额度用完 / 过载时的静默排队)。`chatOnce` 已经把流交出去之后,js 就没有任何
|
|
159
|
+
「换号/换模型」的能力了,所以这个截止只能由**核心**设:
|
|
160
|
+
|
|
161
|
+
- 每条降级腿进核心起 60s(`FIRST_BYTE_BUDGET_MS`),上游还没吐第一个字节就判
|
|
162
|
+
这条腿失败 → 按 `transport` 走换号/换模型(2026-09-24 现场:ttfb 84~255s
|
|
163
|
+
的 8 次请求全挂在一条腿上,用户干等 200 多秒手动暂停)
|
|
164
|
+
- **第一个字节之后不再计时**,长生成不受影响
|
|
165
|
+
- 同一供应商的多个号共享这份预算(停滞是上游级的,按号各等一份会 60s×号数)
|
|
166
|
+
|
|
167
|
+
因此 js 侧的 `chatOnce` 超时**只守「连接 + 响应头」**就够了,不必、也不该给
|
|
168
|
+
body 再套一层空闲超时:响应头一到就把控制权交回核心,body 的事归核心。
|
|
169
|
+
|
|
155
170
|
### 流式一旦写出第一个字节才绑死
|
|
156
171
|
|
|
157
172
|
核心**推迟到第一个字节才写响应头**,所以:
|
|
@@ -313,11 +328,19 @@ export function dispose() {}
|
|
|
313
328
|
|
|
314
329
|
## 加载顺序
|
|
315
330
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
两个来源,同 id 时**用户目录的 js 压过内置行**(`supplierWins`:user > builtin > external):
|
|
332
|
+
|
|
333
|
+
1. **cordis service `router.suppliers`**(值为 `{ [supplierId]: (env) => SupplierModule }`),
|
|
334
|
+
dsh-router `ctx.inject(['router.suppliers'])` 延迟加载(service 可用即注册)。两条子路径都走它:
|
|
335
|
+
- **内置三个 = dsh-router-core 自己 patch 里的行**:`cordis.patch.yml` insert
|
|
336
|
+
`dsh-router-core/suppliers/<x>` 三个子路径模块,每行一个 cordis 壳(`src/suppliers/<x>/index.ts`)
|
|
337
|
+
负责把工厂登记进表。于是它们在官方插件页 dsh-router-core 详情页的「包含的组件」里
|
|
338
|
+
**各占一行、带着宿主管的开关**。行的显示名/说明走**子路径自己的** `row.json` +
|
|
339
|
+
`locale/*.json`(构建期拷成 `lib/suppliers/<x>/{package.json,locale/}`),所以三行各有各的名字。
|
|
340
|
+
内置必须是**行**而不是「扫 `<plugin>/lib/suppliers/` 目录」:扫目录拿不到行的
|
|
341
|
+
身份(显示名/说明/图标),也拿不到宿主的开关 —— 单独关一个内置供应商就没有入口。
|
|
342
|
+
- 外部 DSH 插件(独立安装的供应商包)经同一个 service 注册。
|
|
343
|
+
2. **用户目录** `~/.dsh/profiles/<profile>/suppliers/*.js`(只认 `*.js`;同 id 多个文件按文件名序后者覆盖前者)。
|
|
321
344
|
cordis 的 `provide` 每个 service name 只允许一个插件提供,多个供应商插件采用
|
|
322
345
|
**共享表模式**:先 provide 的插件持有聚合表对象,
|
|
323
346
|
其余插件 **`inject` 后把工厂追加进同一对象**并广播一次
|