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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自检面板的「模型」下拉该列哪些 —— 纯函数,不碰 React。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不能把端点返回的模型全列出来:那个端点给的是**全部**模型(含用户在
|
|
5
|
+
* 供应商详情里停用的)。实测某供应商 30 个模型里只有 3 个是启用的 —— 把 30 个都
|
|
6
|
+
* 塞进下拉,用户会挑到一个自己明明关掉的模型去测,测通了也不代表配置是对的。
|
|
7
|
+
* 所以只列**可用**的(用户没停用的),与路由实际会用的那批保持一致。
|
|
8
|
+
*
|
|
9
|
+
* 顺带把「没有可选模型」拆成两种,因为**用户该做的事不一样**:
|
|
10
|
+
* - `none`(一个模型都没有)→ 该去供应商详情拉取模型;
|
|
11
|
+
* - `all-disabled`(有模型但全被停用)→ 该去供应商详情把它们打开,
|
|
12
|
+
* 去拉取是白跑一趟。
|
|
13
|
+
* 两种共用一句「没有模型」就是把用户往错的方向指。
|
|
14
|
+
*
|
|
15
|
+
* `enabled` 缺省按**未停用**读(`!== false`):这个字段是用户的配置,缺失时不该
|
|
16
|
+
* 凭空把模型藏起来。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** `/suppliers/:id/models` 里的一条模型。 */
|
|
20
|
+
export interface ModelRow {
|
|
21
|
+
id: string
|
|
22
|
+
enabled?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModelChoice {
|
|
26
|
+
/** 可用模型的 id,按端点返回的顺序(不重排 —— 端点已经按优先级排过了)。 */
|
|
27
|
+
ids: string[]
|
|
28
|
+
/** 没有可选模型时的原因;`undefined` = 有得选。 */
|
|
29
|
+
empty?: 'none' | 'all-disabled'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 从端点答复里挑出可用的模型,并说明「空的」到底是哪一种空。 */
|
|
33
|
+
export function modelChoice(models: readonly ModelRow[]): ModelChoice {
|
|
34
|
+
const ids = models.filter((m) => m.enabled !== false).map((m) => m.id)
|
|
35
|
+
if (ids.length > 0) return { ids }
|
|
36
|
+
return { ids, empty: models.length === 0 ? 'none' : 'all-disabled' }
|
|
37
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「路由组件」分组判据测试 —— 锁住三条会静默出错的规则。
|
|
3
|
+
*
|
|
4
|
+
* 1. `source` 判据取反:只有 external 是独立安装的 bundle,其余归本地组。
|
|
5
|
+
* 2. 缺 `source` 的供应商不许被静默丢掉,也不许被误报成独立插件。
|
|
6
|
+
* 3. 只有 ext 那组 `togglable` —— 供应商在这一页没有可写的开关。
|
|
7
|
+
*
|
|
8
|
+
* 这三条错了都不会抛异常,只会显示成「少了一组 / 多了个开关」,所以必须有闸门。
|
|
9
|
+
*/
|
|
10
|
+
import test from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
import { groupRouterComponents, isEmptyComponents, EMPTY_COMPONENTS } from './router-components.ts'
|
|
13
|
+
import type { RouterExtResponse, RouterHealthResponse } from '../shared.ts'
|
|
14
|
+
|
|
15
|
+
const health = (suppliers: NonNullable<RouterHealthResponse['suppliers']>): RouterHealthResponse => ({ ok: true, suppliers })
|
|
16
|
+
const noExt: RouterExtResponse = { ok: true, enhancers: [] }
|
|
17
|
+
|
|
18
|
+
test('内置供应商一个都不进这一节(它们是原生「包含的组件」里的行)', () => {
|
|
19
|
+
const out = groupRouterComponents(health([
|
|
20
|
+
{ id: 'opencode', name: 'OpenCode', source: 'builtin' },
|
|
21
|
+
{ id: 'openrouter', name: 'OpenRouter', source: 'builtin' },
|
|
22
|
+
{ id: 'nvidia', name: 'NVIDIA NIM', source: 'builtin' },
|
|
23
|
+
]), noExt)
|
|
24
|
+
// 重复显示两处 = 用户看到同一个东西两次,且有一处没有开关。
|
|
25
|
+
assert.deepEqual(out.external, [])
|
|
26
|
+
assert.deepEqual(out.ext, [])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('外部供应商插件进「供应商」组(各是独立 bundle,这一页有开关)', () => {
|
|
30
|
+
const out = groupRouterComponents(health([
|
|
31
|
+
{ id: 'codebuddy', name: 'CodeBuddy', source: 'external' },
|
|
32
|
+
{ id: 'traework', name: 'TRAE SOLO', source: 'external' },
|
|
33
|
+
]), noExt)
|
|
34
|
+
assert.deepEqual(out.external.map((r) => r.key), ['supplier:codebuddy', 'supplier:traework'])
|
|
35
|
+
assert.equal(out.external.every((r) => r.togglable), true, '供应商在这一页有开关(写 PATCH /suppliers/:id/enabled)')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('缺 source 的供应商不显示,也不被误报成独立插件', () => {
|
|
39
|
+
const out = groupRouterComponents(health([{ id: 'mystery', name: 'Mystery' }]), noExt)
|
|
40
|
+
assert.deepEqual(out.external, [])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('user 目录投放的供应商也不进这一节(它的对应物是行的覆盖或外部插件)', () => {
|
|
44
|
+
const out = groupRouterComponents(health([{ id: 'mine', name: 'My Supplier', source: 'user' }]), noExt)
|
|
45
|
+
assert.deepEqual(out.external, [])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('扩展插件进 ext 组且唯一可开关;状态按「宁可关着」读', () => {
|
|
49
|
+
const out = groupRouterComponents(health([]), {
|
|
50
|
+
ok: true,
|
|
51
|
+
enhancers: [
|
|
52
|
+
{ id: 'rtk', name: 'RTK', enabled: true, ready: true },
|
|
53
|
+
{ id: 'jev', name: 'JEV' },
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
assert.equal(out.ext.length, 2)
|
|
57
|
+
assert.equal(out.ext.every((r) => r.togglable), true)
|
|
58
|
+
assert.deepEqual(out.ext.map((r) => r.enabled), [true, false])
|
|
59
|
+
assert.deepEqual(out.ext.map((r) => r.ready), [true, false])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('图标与说明只在该有的时候带上(不塞 undefined 键)', () => {
|
|
63
|
+
const out = groupRouterComponents(health([{ id: 'loomy', name: 'Loomy', icon: 'data:image/png;base64,x', source: 'external' }]), {
|
|
64
|
+
ok: true,
|
|
65
|
+
enhancers: [{ id: 'rtk', name: 'RTK', detail: '本机未装 rtk' }],
|
|
66
|
+
})
|
|
67
|
+
const supplier = out.external.at(0)
|
|
68
|
+
const extRow = out.ext.at(0)
|
|
69
|
+
assert.ok(supplier !== undefined && extRow !== undefined)
|
|
70
|
+
assert.equal('icon' in supplier, true)
|
|
71
|
+
assert.equal('detail' in extRow, true)
|
|
72
|
+
assert.equal('icon' in extRow, false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('两个端点都没有内容时不渲染这一节(不留空标题)', () => {
|
|
76
|
+
assert.equal(isEmptyComponents(EMPTY_COMPONENTS), true)
|
|
77
|
+
assert.equal(isEmptyComponents(groupRouterComponents({ ok: true }, { ok: true })), true)
|
|
78
|
+
assert.equal(isEmptyComponents(groupRouterComponents({ ok: true }, { ok: true, enhancers: [{ id: 'rtk', name: 'RTK' }] })), false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('端点报错/缺字段时分组仍是空组,不抛(形状恒定)', () => {
|
|
82
|
+
const out = groupRouterComponents({ ok: false, error: 'boom' }, { ok: false, error: 'boom' })
|
|
83
|
+
assert.deepEqual(out, EMPTY_COMPONENTS)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('供应商的开关状态:缺 enabled 按开着读,关掉的仍列出来(关掉后要能再开)', () => {
|
|
87
|
+
const out = groupRouterComponents(health([
|
|
88
|
+
{ id: 'codebuddy', name: 'CodeBuddy', source: 'external' },
|
|
89
|
+
{ id: 'traework', name: 'TRAE SOLO', source: 'external', enabled: false },
|
|
90
|
+
]), noExt)
|
|
91
|
+
assert.deepEqual(out.external.map((r) => r.key), ['supplier:codebuddy', 'supplier:traework'],
|
|
92
|
+
'关掉的供应商不能从列表消失 —— 消失了就再没有可点的开关把它开回来')
|
|
93
|
+
assert.deepEqual(out.external.map((r) => r.enabled), [true, false])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('供应商行没有 ready(那是扩展的运行时事实)—— 缺失不得让它变成点不开', () => {
|
|
97
|
+
const out = groupRouterComponents(health([{ id: 'codebuddy', name: 'CodeBuddy', source: 'external' }]), noExt)
|
|
98
|
+
const row = out.external.at(0)
|
|
99
|
+
assert.ok(row !== undefined)
|
|
100
|
+
assert.equal('ready' in row, false, '供应商不报就绪,别塞一个 undefined 键进去')
|
|
101
|
+
assert.equal(row.togglable, true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('内置扩展不进这一节(它已经是原生「包含的组件」里的一行)', () => {
|
|
105
|
+
const out = groupRouterComponents({ ok: true }, {
|
|
106
|
+
ok: true,
|
|
107
|
+
enhancers: [
|
|
108
|
+
{ id: 'test', name: '插件自检', source: 'builtin' },
|
|
109
|
+
{ id: 'rtk', name: 'RTK' },
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
// 重复显示两处 = 用户看到同一个东西两次,且有一处没有开关。
|
|
113
|
+
assert.deepEqual(out.ext.map((r) => r.key), ['ext:rtk'])
|
|
114
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 官方插件页「路由组件」一节的分组 —— 纯函数,不碰 React。
|
|
3
|
+
*
|
|
4
|
+
* 为什么抽出来:这一节唯一有分支的地方就是「一条供应商/扩展归哪组、能不能
|
|
5
|
+
* 就地开关」(见下两条判据)。留在组件里就只能靠真页面点出来验,抽出来单测,
|
|
6
|
+
* 判据改错当场变红。
|
|
7
|
+
*
|
|
8
|
+
* 数据源是核心**已经在提供**的两个端点,本模块不新增任何 API:
|
|
9
|
+
* - `/health` 的 `suppliers[]` 带 `source`(builtin / user / external)
|
|
10
|
+
* - `/ext` 的 `enhancers[]` 是核心合并完开关与运行时事实的扩展表
|
|
11
|
+
*/
|
|
12
|
+
import type { RouterExtResponse, RouterHealthResponse } from '../shared.ts'
|
|
13
|
+
|
|
14
|
+
/** `/health` 里的一条供应商摘要(就地取型,不另抄一份形状)。 */
|
|
15
|
+
export type RouterSupplierSummary = NonNullable<RouterHealthResponse['suppliers']>[number]
|
|
16
|
+
|
|
17
|
+
/** 一行组件(两组共用同一种形状,渲染时才分叉)。 */
|
|
18
|
+
export interface RouterComponentRow {
|
|
19
|
+
/** 组内唯一键:`supplier:<id>` / `ext:<id>`。 */
|
|
20
|
+
key: string
|
|
21
|
+
name: string
|
|
22
|
+
icon?: string
|
|
23
|
+
/** ext 报的就绪状态说明(如「本机未装 rtk」)。 */
|
|
24
|
+
detail?: string
|
|
25
|
+
enabled?: boolean
|
|
26
|
+
ready?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* 能否就地开关。**两组都能**:扩展走 `PATCH /router/api/ext`(核心持久化到
|
|
29
|
+
* `<dataDir>/ext.json`),供应商走 `PATCH /router/api/suppliers/:id/enabled`
|
|
30
|
+
* (落到 `supplier-config.json`)。两组都是核心持久化的用户开关,核心都据此
|
|
31
|
+
* 真的停止路由 —— 不是一个只把卡片藏起来的假开关。
|
|
32
|
+
*/
|
|
33
|
+
togglable: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 这一节要列的两组。
|
|
38
|
+
*
|
|
39
|
+
* **为什么没有「内置与本地供应商」组**:内置供应商是 dsh-router-core 自己 patch
|
|
40
|
+
* 里的**行**(`cordis.patch.yml` insert 了三个子路径模块),已经在宿主的原生
|
|
41
|
+
* 「包含的组件」里,带着宿主管的开关。在这里再列一遍就是同一个东西显示两处。
|
|
42
|
+
*/
|
|
43
|
+
export interface RouterComponents {
|
|
44
|
+
/** 独立安装的供应商插件(各是独立 bundle,启停在它们自己的插件页)。 */
|
|
45
|
+
external: RouterComponentRow[]
|
|
46
|
+
/** 扩展插件(`router.ext`,开关归核心)。 */
|
|
47
|
+
ext: RouterComponentRow[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 空组:加载前与加载失败都用它,保证形状恒定。 */
|
|
51
|
+
export const EMPTY_COMPONENTS: RouterComponents = { external: [], ext: [] }
|
|
52
|
+
|
|
53
|
+
/** 一条外部供应商摘要 → 只读行。 */
|
|
54
|
+
function supplierRow(s: RouterSupplierSummary): RouterComponentRow {
|
|
55
|
+
return {
|
|
56
|
+
key: `supplier:${s.id}`,
|
|
57
|
+
name: s.name,
|
|
58
|
+
// 缺 `enabled` 按开着读(老核心没这个字段)。关掉的供应商**仍列出来** ——
|
|
59
|
+
// 面板「扩展」页的形状是「关掉就不出现」,但供应商关掉后如果也消失,这里就
|
|
60
|
+
// 少了一个可点的开关、再也开不回来。两组刻意不同。
|
|
61
|
+
enabled: s.enabled !== false,
|
|
62
|
+
togglable: true,
|
|
63
|
+
...(s.icon === undefined ? {} : { icon: s.icon }),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 把两个端点的答复分成两组。
|
|
69
|
+
*
|
|
70
|
+
* 供应商这边只收 `source === 'external'`(独立安装的供应商插件)。`builtin`
|
|
71
|
+
* 的那三个已经作为**原生行**在宿主的「包含的组件」里显示了(core 自己 patch
|
|
72
|
+
* insert 的子路径模块,开关是宿主管的那个行开关),`user` 的是用户目录里投放的
|
|
73
|
+
* js —— 两者都不该在这里出现第二次。`source` 缺失按「不是 external」处理:
|
|
74
|
+
* 不显示总比把内置的错标成独立插件强。
|
|
75
|
+
*
|
|
76
|
+
* 两组都在这一页开关,指向核心的同两个端点(`PATCH /suppliers/:id/enabled` 与
|
|
77
|
+
* `PATCH /ext`),所以「在哪开关」是一致的:**设置 → 插件 → dsh-router-core**。
|
|
78
|
+
* 供应商的开关关掉是真的不参与路由(见 Router 的活跃集合),不是只隐藏卡片。
|
|
79
|
+
*/
|
|
80
|
+
export function groupRouterComponents(health: RouterHealthResponse, ext: RouterExtResponse): RouterComponents {
|
|
81
|
+
const external = (health.suppliers ?? [])
|
|
82
|
+
.filter((s) => s.source === 'external')
|
|
83
|
+
.map(supplierRow)
|
|
84
|
+
// 只列**独立安装**的扩展(`source !== 'builtin'`)。随核心分发的内置扩展已经在
|
|
85
|
+
// 宿主原生的「包含的组件」里占一行、自带宿主管的开关,在这里再列一遍就是同一个
|
|
86
|
+
// 东西显示两处 —— 与内置供应商的处理完全一样。
|
|
87
|
+
const extRows = (ext.enhancers ?? []).filter((e) => e.source !== 'builtin').map((e): RouterComponentRow => ({
|
|
88
|
+
key: `ext:${e.id}`,
|
|
89
|
+
name: e.name,
|
|
90
|
+
// 缺字段一律按「关 / 未就绪」读:没拿到的事实不当真,宁可显示成关着
|
|
91
|
+
// 让用户自己去开,也不要谎报在跑(`ready` 是运行时事实,猜不得)。
|
|
92
|
+
enabled: e.enabled === true,
|
|
93
|
+
ready: e.ready === true,
|
|
94
|
+
togglable: true,
|
|
95
|
+
...(e.icon === undefined ? {} : { icon: e.icon }),
|
|
96
|
+
...(e.detail === undefined ? {} : { detail: e.detail }),
|
|
97
|
+
}))
|
|
98
|
+
return { external, ext: extRows }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 两组加起来一个都没有 → 这一节不渲染(不留空标题)。 */
|
|
102
|
+
export function isEmptyComponents(components: RouterComponents): boolean {
|
|
103
|
+
return components.external.length === 0 && components.ext.length === 0
|
|
104
|
+
}
|