dsh-router-core 0.4.7 → 0.4.8
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 +9 -69
- package/cordis.patch.yml +2 -7
- package/docs/ext.md +3 -17
- package/lib/client-registry.js +50 -565
- package/lib/client.js +50 -565
- package/lib/ext-prompt/index.js +20 -15
- package/lib/index.js +0 -419
- package/lib/types/ext/contract.d.ts +1 -1
- package/lib/types/ext-prompt/content.d.ts +11 -0
- package/lib/types/ext-prompt/index.d.ts +7 -9
- package/lib/types/ext-prompt/mount.d.ts +2 -2
- package/lib/types/ext-prompt/plugin.d.ts +3 -5
- package/lib/types/suppliers/registry.d.ts +0 -1
- package/package.json +1 -10
- package/src/client/ExtDetail.tsx +1 -13
- package/src/client/group-controls.test.ts +1 -1
- package/src/client/group-controls.ts +1 -1
- package/src/client/index.tsx +0 -17
- package/src/client/router-components.test.ts +1 -1
- package/src/client/router.css +3 -354
- package/src/ext/contract.ts +1 -1
- package/src/ext-prompt/content.ts +17 -12
- package/src/ext-prompt/ext-prompt.test.ts +3 -3
- package/src/ext-prompt/index.ts +7 -9
- package/src/ext-prompt/mount.ts +2 -2
- package/src/ext-prompt/plugin.ts +3 -5
- package/src/router/index.ts +2 -3
- package/src/router/models-cache.test.ts +5 -6
- package/src/router/supplier-switch.test.ts +1 -1
- package/src/suppliers/registry.ts +0 -71
- package/lib/contract-D4BWZ7Xe.js +0 -13
- package/lib/ext-test/index.js +0 -36
- package/lib/ext-test/locale/en.json +0 -6
- package/lib/ext-test/locale/zh.json +0 -6
- package/lib/ext-test/package.json +0 -5
- package/lib/types/ext-test/index.d.ts +0 -21
- package/lib/types/ext-test/plugin.d.ts +0 -23
- package/lib/types/suppliers/probe.d.ts +0 -160
- package/src/client/ExtTestPanel.tsx +0 -392
- package/src/client/comp-button-scope.test.ts +0 -58
- package/src/client/comp-panel-layout.test.ts +0 -58
- package/src/client/ext-panels.test.ts +0 -44
- package/src/client/ext-panels.ts +0 -49
- package/src/client/model-choice.test.ts +0 -41
- package/src/client/model-choice.ts +0 -37
- package/src/ext-test/ext-test.test.ts +0 -60
- package/src/ext-test/index.ts +0 -35
- package/src/ext-test/locale/en.json +0 -6
- package/src/ext-test/locale/zh.json +0 -6
- package/src/ext-test/plugin.ts +0 -34
- package/src/ext-test/row.json +0 -5
- package/src/suppliers/probe.test.ts +0 -266
- package/src/suppliers/probe.ts +0 -475
|
@@ -722,15 +722,14 @@ test('组合窗口:模型都没报 context_length → undefined', async () =>
|
|
|
722
722
|
* 面板的「已禁用」列表于是空了。曾想过在核心按配置把缺的 id 补回来,**这是错的**:
|
|
723
723
|
*
|
|
724
724
|
* 1. 那是替有问题的插件擦屁股,插件永远不会被修;
|
|
725
|
-
* 2.
|
|
726
|
-
*
|
|
727
|
-
* 第三方那个 bug 就再也没人知道了。
|
|
725
|
+
* 2. 更糟的是**它把症状掩盖掉** —— 模型被补全之后页面上一切正常,那个 bug
|
|
726
|
+
* 就再也没人发现了。
|
|
728
727
|
*
|
|
729
|
-
*
|
|
728
|
+
* 所以核心只负责:自己不合并不掩饰(插件少报就是少报),让症状留在页面上。
|
|
730
729
|
* 下面两条判据钉住这个方向,防止以后有人(我)再犯同样的错。
|
|
731
730
|
*/
|
|
732
731
|
|
|
733
|
-
test('
|
|
732
|
+
test('核心不替违约插件补回已禁用的模型(症状要留在页面上)', async () => {
|
|
734
733
|
const router = new Router('')
|
|
735
734
|
const a = supplier('a', [{ id: 'a-1' }, { id: 'a-2' }])
|
|
736
735
|
// 越权实现:listModels 自己过滤掉了已禁用的
|
|
@@ -740,7 +739,7 @@ test('核心不替违约插件补回已禁用的模型(要留着证据给体
|
|
|
740
739
|
|
|
741
740
|
const models = await router.modelsOf('a')
|
|
742
741
|
assert.deepEqual(models.map((m) => m.id), ['a-1'],
|
|
743
|
-
'核心不该把插件藏掉的模型补回来 ——
|
|
742
|
+
'核心不该把插件藏掉的模型补回来 —— 补回来就等于替它兜底,症状被掩盖就再没人修')
|
|
744
743
|
})
|
|
745
744
|
|
|
746
745
|
test('正常供应商(listModels 全量返回):列表内容与启用状态照旧', async () => {
|
|
@@ -164,7 +164,7 @@ test('没记录过的供应商默认开着(升级不把全灭变成全开)',
|
|
|
164
164
|
assert.equal(router.isEnabled('supA'), true)
|
|
165
165
|
})
|
|
166
166
|
|
|
167
|
-
// ----
|
|
167
|
+
// ---- 指定连接自检(诊断单个链接到底通不通) ----
|
|
168
168
|
// 面板「供应商」详情里的「测试」按钮走账号池(任选可用号、失败自动换号);
|
|
169
169
|
// 面板的连接测试可以**钉死一个连接**,用来诊断单个链接到底通不通。关键性质:
|
|
170
170
|
// 指定了连接就**不回退**到别的号,否则「测这个连接」测出来的是另一个号的结果。
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 通用能力(dsh-router 核心,所有供应商自动可用,js 无需实现):
|
|
5
5
|
* GET /suppliers/:id/models models + alias(listModels 合并启用状态)
|
|
6
|
-
* POST /suppliers/:id/probe 契约体检:逐成员报「实现/可用/不执行」
|
|
7
6
|
* PATCH /suppliers/:id/enabled {enabled} —— 供应商开关(关掉不参与路由)
|
|
8
7
|
* POST /suppliers/:id/models/toggle {id, enabled}
|
|
9
8
|
* POST /suppliers/:id/models/add {id}
|
|
@@ -26,7 +25,6 @@ import type { Router } from '../router/index.ts'
|
|
|
26
25
|
import type { ModelWithEnabled, SupplierStatus } from '../router/types.ts'
|
|
27
26
|
import type { SupplierConfigStore } from '../supplier-config.ts'
|
|
28
27
|
import type { LoadedSupplier } from './loader.ts'
|
|
29
|
-
import { probeSupplier } from './probe.ts'
|
|
30
28
|
|
|
31
29
|
/** webServer 路由形状(与 index.ts 的 WebServerRoute 一致)。 */
|
|
32
30
|
export interface WebServerRoute {
|
|
@@ -170,75 +168,6 @@ export function supplierRoutes(base: string, loaded: LoadedSupplier, store: Supp
|
|
|
170
168
|
},
|
|
171
169
|
})
|
|
172
170
|
|
|
173
|
-
// ---- 契约体检(只跑只读成员;有副作用的只报存在性)----
|
|
174
|
-
routes.push({
|
|
175
|
-
kind: 'exact',
|
|
176
|
-
path: `${p}/probe`,
|
|
177
|
-
handler: async (req, res) => {
|
|
178
|
-
if (req.method !== 'POST') {
|
|
179
|
-
writeJson(res, 405, { ok: false, error: 'probe requires POST' })
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
// body 可空;只用来选模型/连接(不选就用第一个可用的)
|
|
184
|
-
let pick: { model?: string; uid?: string } = {}
|
|
185
|
-
const raw = (await readBody(req)).trim()
|
|
186
|
-
if (raw !== '') {
|
|
187
|
-
const body = JSON.parse(raw) as { model?: unknown; uid?: unknown }
|
|
188
|
-
pick = {
|
|
189
|
-
...(typeof body.model === 'string' ? { model: body.model } : {}),
|
|
190
|
-
...(typeof body.uid === 'string' ? { uid: body.uid } : {}),
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// 模型启用状态走 router.modelsOf(核心缓存,不额外打上游)
|
|
194
|
-
const models = await router.modelsOf(s.id).catch(() => undefined)
|
|
195
|
-
const report = await probeSupplier(loaded, {
|
|
196
|
-
models,
|
|
197
|
-
// **实跑「全部禁用 → 看列表 → 全部启用」,验完还原。**
|
|
198
|
-
//
|
|
199
|
-
// 这是体检里唯一能抓出「插件在 listModels 里过滤已禁用模型」越权的路径:
|
|
200
|
-
// 违规插件把已禁用的藏起来,于是全部禁用之后核心一个模型都看不到,
|
|
201
|
-
// 用户点「全部启用」也就再也点不回来(没有 id 可传)。
|
|
202
|
-
// 还原写在 finally —— 体检不该在用户配置上留痕,哪怕中间抛了。
|
|
203
|
-
runBulkToggleRoundTrip: async () => {
|
|
204
|
-
const original = [...store.get(s.id).disabled]
|
|
205
|
-
const before = await router.modelsOf(s.id)
|
|
206
|
-
const ids = before.map((mm) => mm.id)
|
|
207
|
-
if (ids.length === 0) return { ok: true, detail: '该供应商没有模型,跳过' }
|
|
208
|
-
try {
|
|
209
|
-
store.setAllModelsEnabled(s.id, false, ids)
|
|
210
|
-
router.invalidateModels(s.id)
|
|
211
|
-
const after = await router.modelsOf(s.id, true)
|
|
212
|
-
const hidden = after.filter((mm) => !mm.enabled).length
|
|
213
|
-
const missing = ids.length - after.length
|
|
214
|
-
if (hidden !== ids.length || missing !== 0) {
|
|
215
|
-
return {
|
|
216
|
-
ok: false,
|
|
217
|
-
// 一句话说完:现象 + 归因。细说留给源码与提交记录,面板上不占地方。
|
|
218
|
-
detail: `全部禁用后只剩 ${after.length}/${ids.length} 个 —— 插件在 listModels 里藏了已禁用的模型(启用状态由核心合并)`,
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return { ok: true, detail: `全部禁用后 ${after.length} 个都在,再启用可恢复` }
|
|
222
|
-
} finally {
|
|
223
|
-
store.setAllModelsEnabled(s.id, true, ids)
|
|
224
|
-
for (const id of original) store.setAllModelsEnabled(s.id, false, [id])
|
|
225
|
-
router.invalidateModels(s.id)
|
|
226
|
-
await router.modelsOf(s.id, true)
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
// chatOnce 真跑一次:账号遍历 / 冷却 / 首字节预算都走真实路径
|
|
230
|
-
runChatOnce: async (model) => {
|
|
231
|
-
const r = await router.testModel(s.id, model, pick.uid)
|
|
232
|
-
return { ok: r.ok, detail: r.ok ? '通了' : (r.error ?? '上游没给原因') }
|
|
233
|
-
},
|
|
234
|
-
})
|
|
235
|
-
writeJson(res, 200, { ok: true, report })
|
|
236
|
-
} catch (err) {
|
|
237
|
-
writeJson(res, 500, { ok: false, error: (err as Error).message })
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
|
|
242
171
|
// ---- 通用: alias ----
|
|
243
172
|
routes.push({
|
|
244
173
|
kind: 'exact',
|
package/lib/contract-D4BWZ7Xe.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
//#region src/ext/contract.ts
|
|
2
|
-
/** 读取当前 router.ext 表(同一 live 对象,可追加)——扩展插件用它注册自己。 */
|
|
3
|
-
function currentExts(ctx) {
|
|
4
|
-
const c = ctx;
|
|
5
|
-
return c.get?.("router.ext") ?? c.router?.ext;
|
|
6
|
-
}
|
|
7
|
-
/** 读取扩展存储服务——扩展插件用它读开关、读写自己存的数据。 */
|
|
8
|
-
function currentExtStore(ctx) {
|
|
9
|
-
const c = ctx;
|
|
10
|
-
return c.get?.("router.extStore") ?? c.router?.extStore;
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
export { currentExts as n, currentExtStore as t };
|
package/lib/ext-test/index.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { n as currentExts, t as currentExtStore } from "../contract-D4BWZ7Xe.js";
|
|
2
|
-
//#region src/ext-test/plugin.ts
|
|
3
|
-
/** 扩展开关表里的注册键(也是 URL 面板读它的 id)。 */
|
|
4
|
-
const EXT_TEST_ID = "test";
|
|
5
|
-
/**
|
|
6
|
-
* 构造这个扩展器。
|
|
7
|
-
*
|
|
8
|
-
* `getState()` 恒为就绪:自检能力不依赖外部二进制(不像 rtk 需要本机装 rtk),
|
|
9
|
-
* 「能不能测」取决于用户选的供应商/模型/连接,不该由扩展提前否决 —— 真跑一次
|
|
10
|
-
* 才知道,那时错误信息比一个「未就绪」有用得多。
|
|
11
|
-
*/
|
|
12
|
-
function createTestExt() {
|
|
13
|
-
return {
|
|
14
|
-
id: EXT_TEST_ID,
|
|
15
|
-
name: "插件自检",
|
|
16
|
-
description: "选供应商、模型与连接跑一次真实访问测试;或对外部供应商插件做契约体检,逐个成员报告实现与可用情况",
|
|
17
|
-
source: "builtin",
|
|
18
|
-
getState: () => ({ ready: true })
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/ext-test/index.ts
|
|
23
|
-
/** cordis 插件身份(配置树里这一行的技术名;显示名走同目录的 locale)。 */
|
|
24
|
-
const name = "dsh-router-ext-test";
|
|
25
|
-
/** 登记本扩展。幂等:同一个 id 已在表里就直接返回,不顶掉别人的登记。 */
|
|
26
|
-
function apply(ctx) {
|
|
27
|
-
ctx.inject(["router.ext"], (sctx) => {
|
|
28
|
-
const table = currentExts(sctx);
|
|
29
|
-
if (table === void 0) return;
|
|
30
|
-
if (table["test"] !== void 0) return;
|
|
31
|
-
table[EXT_TEST_ID] = createTestExt();
|
|
32
|
-
currentExtStore(sctx)?.setEnabled(EXT_TEST_ID, true);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
//#endregion
|
|
36
|
-
export { apply, name };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 插件自检扩展的 **cordis 行** —— 官方插件页(设置 → 插件 → dsh-router-core)详情页
|
|
3
|
-
* 原生「包含的组件」里的那一行。
|
|
4
|
-
*
|
|
5
|
-
* 壳存在的理由与内置供应商那三层一样:`plugin.ts` 已经把 `name` 用作**面板显示名**
|
|
6
|
-
* (扩展契约的字段,所有扩展都这么写),而 cordis 的插件身份也要一个 `name`,两者抢
|
|
7
|
-
* 同一个名字。所以行 = 这一层壳(cordis 身份 + 登记),实现 = `plugin.ts`。
|
|
8
|
-
*
|
|
9
|
-
* **默认关闭**(patch 里 `disabled: true`):行关闭时 loader 根本不 import 这个模块,
|
|
10
|
-
* 扩展也就没进 `router.ext` 表 —— 所以「打开才出现这张卡片」是天然的,不需要额外的
|
|
11
|
-
* 状态位。用户在这一行打开后,卡片出现在插件页的「路由组件 → 扩展」组与面板的
|
|
12
|
-
* 「扩展」页。
|
|
13
|
-
*
|
|
14
|
-
* 登记时把扩展开关置为开:本扩展**没有独立的运行期开关**,它的开关就是这一行
|
|
15
|
-
* (宿主管的那个)。不置上的话,用户开了行还要再点一次,两级开关说同一件事。
|
|
16
|
-
*/
|
|
17
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
18
|
-
/** cordis 插件身份(配置树里这一行的技术名;显示名走同目录的 locale)。 */
|
|
19
|
-
export declare const name = "dsh-router-ext-test";
|
|
20
|
-
/** 登记本扩展。幂等:同一个 id 已在表里就直接返回,不顶掉别人的登记。 */
|
|
21
|
-
export declare function apply(ctx: Context): void;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 插件自检扩展(`router.ext` 侧的实现)—— 声明与状态都在这里,**不做任何拦截**。
|
|
3
|
-
*
|
|
4
|
-
* 它和 rtk 那类扩展的区别要写清楚:本扩展**不挂任何监听、不改写任何命令**,
|
|
5
|
-
* 唯一作用是让用户在官方插件页里打开一块自检面板(选供应商 / 模型 / 连接,跑一次
|
|
6
|
-
* 真实的访问测试并看到底报了什么)。所以它复用 `router.ext` 这条通道只是为了拿到
|
|
7
|
-
* **发现 + 开关**两件事,而不是为了拦截。
|
|
8
|
-
*
|
|
9
|
-
* 面板本身在浏览器侧(`src/client/ExtTestPanel.tsx`),通过原生行的
|
|
10
|
-
* `plugins.row.config` 座位挂在「包含的组件」里那一行的详情页上;这里只负责让它在
|
|
11
|
-
* 列表里有个身份、以及报出运行时就绪状态。
|
|
12
|
-
*/
|
|
13
|
-
import type { RouterExt } from '../ext/contract.ts';
|
|
14
|
-
/** 扩展开关表里的注册键(也是 URL 面板读它的 id)。 */
|
|
15
|
-
export declare const EXT_TEST_ID = "test";
|
|
16
|
-
/**
|
|
17
|
-
* 构造这个扩展器。
|
|
18
|
-
*
|
|
19
|
-
* `getState()` 恒为就绪:自检能力不依赖外部二进制(不像 rtk 需要本机装 rtk),
|
|
20
|
-
* 「能不能测」取决于用户选的供应商/模型/连接,不该由扩展提前否决 —— 真跑一次
|
|
21
|
-
* 才知道,那时错误信息比一个「未就绪」有用得多。
|
|
22
|
-
*/
|
|
23
|
-
export declare function createTestExt(): RouterExt;
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import type { LoadedSupplier } from './loader.ts';
|
|
2
|
-
/**
|
|
3
|
-
* 体检只有**一档,全自动**。
|
|
4
|
-
*
|
|
5
|
-
* 前提由使用者保证:连接池里已经有一个可用的 token。所以「每个功能到底通不通」这个问题
|
|
6
|
-
* 是可以被回答的 —— 于是能跑的就都跑掉:
|
|
7
|
-
* - 没有副作用的成员直接实跑;
|
|
8
|
-
* - 有副作用但可以用**无害探针输入**跑通代码路径的也实跑(`removeLink` 传不存在的
|
|
9
|
-
* uid、`completeLogin` 传无效回调 URL、`generateLoginUrl` 直接调);
|
|
10
|
-
* - 需要真实账号/真实额度的也实跑(`chatOnce` 走 `router.testModel` 真发一次、
|
|
11
|
-
* `checkinNow` 对连接池里的真实连接签到);
|
|
12
|
-
* - 会破坏体检前提的(`dispose` 卸载自己、`addApiKey` 写凭证…)**不进清单** ——
|
|
13
|
-
* 见 `PROBE_EXCLUDED_MEMBERS`,逐个写了不测的理由。
|
|
14
|
-
*
|
|
15
|
-
* 报告的每个成员都标 `executed`:体检的价值全在「哪些是验过的、哪些只是看了一眼」。
|
|
16
|
-
*/
|
|
17
|
-
export type ProbeExecuted = 'ran' | 'no';
|
|
18
|
-
/** 这个成员是不是有副作用的(`skip` = 需要特别处理:要么探针实跑,要么只报存在性)。 */
|
|
19
|
-
export type ProbeKind = 'safe' | 'skip';
|
|
20
|
-
/** 一个契约成员的体检条目。 */
|
|
21
|
-
export interface ProbeMember {
|
|
22
|
-
/** 契约里的成员名(与 `SupplierModule` 逐字一致)。 */
|
|
23
|
-
key: string;
|
|
24
|
-
/** 面板显示名。 */
|
|
25
|
-
label: string;
|
|
26
|
-
/** 必填成员 / 可选(差异化)成员。 */
|
|
27
|
-
required: boolean;
|
|
28
|
-
probe: ProbeKind;
|
|
29
|
-
/** 不自动执行的原因(`probe: 'skip'` 时必填,面板要显示给用户看)。 */
|
|
30
|
-
skipReason?: string;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* 结果里一条成员的最终状态。
|
|
34
|
-
*
|
|
35
|
-
* `unverified` 与 `fail` 必须分开:**没条件验**(没有 token、没有已启用模型、连接池
|
|
36
|
-
* 为空)不是插件的毛病。压成 fail 会让「用户还没放 token」被出厂结论判成不合格 ——
|
|
37
|
-
* 体检最忌讳把「没测到」说成「测出问题」。
|
|
38
|
-
*/
|
|
39
|
-
export type ProbeState = 'ok' | 'fail' | 'absent' | 'unverified';
|
|
40
|
-
export interface ProbeResult {
|
|
41
|
-
key: string;
|
|
42
|
-
label: string;
|
|
43
|
-
required: boolean;
|
|
44
|
-
present: boolean;
|
|
45
|
-
state: ProbeState;
|
|
46
|
-
/** 到底跑没跑。`no` 时 `detail` 写清为什么。 */
|
|
47
|
-
executed: ProbeExecuted;
|
|
48
|
-
/** 摘要/失败原因;`executed: 'no'` 时这里是「为什么没跑」。 */
|
|
49
|
-
detail?: string;
|
|
50
|
-
}
|
|
51
|
-
/** 体检的外部输入:模型启用状态 + 跑一次真实 `chatOnce` 的入口。 */
|
|
52
|
-
export interface ProbeInput {
|
|
53
|
-
/** 该供应商当前的模型启用状态(调用方用 `router.modelsOf` 取,走核心缓存)。 */
|
|
54
|
-
models?: ReadonlyArray<{
|
|
55
|
-
id: string;
|
|
56
|
-
enabled: boolean;
|
|
57
|
-
}>;
|
|
58
|
-
/**
|
|
59
|
-
* 跑一次真实调用(`router.testModel`:账号遍历 + 冷却 + 首字节预算都生效)。
|
|
60
|
-
* 调用方决定用哪个模型 / 哪个连接。取不到模型时可以不提供 —— 报告里会写明
|
|
61
|
-
* 「无法验证」而不是假装通过。
|
|
62
|
-
*/
|
|
63
|
-
runChatOnce?: (model: string) => Promise<{
|
|
64
|
-
ok: boolean;
|
|
65
|
-
detail: string;
|
|
66
|
-
}>;
|
|
67
|
-
/**
|
|
68
|
-
* **真跑一遍「全部禁用 → 全部启用」并还原**。
|
|
69
|
-
*
|
|
70
|
-
* 这不是「只报可用性」—— 它就是体检里能测出「插件在 listModels 里过滤已禁用模型」
|
|
71
|
-
* 越权的那条路径:核心把全部模型标成停用,再拉一次模型列表,**被藏起来的就露馅**
|
|
72
|
-
* (插件以为该藏,核心却必须还能看见它们,否则面板的「已禁用」列表会空、用户再也
|
|
73
|
-
* 看不到自己禁用过什么)。
|
|
74
|
-
*
|
|
75
|
-
* 由调用方实现,**必须自己还原**(体检不该在用户配置上留痕)。
|
|
76
|
-
*/
|
|
77
|
-
runBulkToggleRoundTrip?: () => Promise<{
|
|
78
|
-
ok: boolean;
|
|
79
|
-
detail: string;
|
|
80
|
-
}>;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* **核心侧**能力:不属于插件契约(插件不实现它们,是核心代劳),所以不在上面的
|
|
84
|
-
* 成员表里 —— 但体检要看它们的状态。模型启用/禁用就是这一类。
|
|
85
|
-
*/
|
|
86
|
-
export interface ProbeCoreReport {
|
|
87
|
-
models: {
|
|
88
|
-
/** 模型总数;`listModels` 失败时为 null(= 拿不到,不代表 0 个)。 */
|
|
89
|
-
total: number | null;
|
|
90
|
-
enabled: number;
|
|
91
|
-
disabled: number;
|
|
92
|
-
/** 拿不到总数时的原因。 */
|
|
93
|
-
note?: string;
|
|
94
|
-
};
|
|
95
|
-
/** 核心代劳的操作:**只报可用性,一律不自动执行**(改了就是替用户改配置)。 */
|
|
96
|
-
/**
|
|
97
|
-
* 核心代劳的能力区。**「全部启用 / 全部禁用」是实跑的**(并还原):它既是面板上
|
|
98
|
-
* 真实存在的操作,也是体检唯一能验出「插件私自过滤已禁用模型」的办法。
|
|
99
|
-
*/
|
|
100
|
-
operations: Array<{
|
|
101
|
-
key: string;
|
|
102
|
-
label: string;
|
|
103
|
-
ran: boolean;
|
|
104
|
-
ok?: boolean;
|
|
105
|
-
detail?: string;
|
|
106
|
-
}>;
|
|
107
|
-
}
|
|
108
|
-
export interface ProbeReport {
|
|
109
|
-
supplier: string;
|
|
110
|
-
name: string;
|
|
111
|
-
/** 出厂结论:`fail` = 必填成员缺失/实跑失败;`warn` = 可选能力缺或没实跑;`pass` = 全绿。 */
|
|
112
|
-
members: ProbeResult[];
|
|
113
|
-
core: ProbeCoreReport;
|
|
114
|
-
summary: {
|
|
115
|
-
total: number;
|
|
116
|
-
implemented: number;
|
|
117
|
-
ok: number;
|
|
118
|
-
fail: number;
|
|
119
|
-
absent: number;
|
|
120
|
-
/** 实现了但没验(任何输入都会毁掉体检前提)。 */
|
|
121
|
-
unverified: number;
|
|
122
|
-
/** 实际实跑过的成员数(`executed !== 'no'`)。 */
|
|
123
|
-
ran: number;
|
|
124
|
-
/** 核心区「全部启用/全部禁用」实跑的结果(抓插件越权的那条路)。 */
|
|
125
|
-
bulk: 'ok' | 'fail' | 'unverified';
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* 契约成员清单(体检的唯一事实来源)。
|
|
130
|
-
*
|
|
131
|
-
* `required: false` = 差异化能力,按存在性暴露端点/UI;`probe: 'skip'` 的理由都写在这里,
|
|
132
|
-
* 面板会把 `skipReason` 原样显示 —— 用户能知道「有这个能力,只是体检不替你调」。
|
|
133
|
-
*/
|
|
134
|
-
export declare const SUPPLIER_CONTRACT_MEMBERS: readonly ProbeMember[];
|
|
135
|
-
/**
|
|
136
|
-
* **故意不列进体检清单的成员**(不是「不跑」,是**不在报告里出现**):
|
|
137
|
-
*
|
|
138
|
-
* - `completeLogin`:需要一个真实回调 URL 才跑得通,体检拿不到 —— 跑了也只是验
|
|
139
|
-
* 「无效 URL 会不会抛错」,那不是功能是否可用。
|
|
140
|
-
* - `addApiKey`:需要一个真实 API key,自动造一个只会往凭证库写垃圾。
|
|
141
|
-
* - `removeLink`:用不存在的 uid 去试,它当然返回 false —— 那是**正常响应**,
|
|
142
|
-
* 判成失败是体检自己错判功能有问题(踩过)。真要验它只能删一个真连接。
|
|
143
|
-
* - `pollLogin`:只读一个布尔标记,验它通不通没有意义。
|
|
144
|
-
* - `generateLoginUrl`:可能直接触发设备码 / OAuth 登录流,会在上游留下待处理的会话。
|
|
145
|
-
*
|
|
146
|
-
* 保留在清单里、逐条占行、只为说「没验」,是在用篇幅淹掉真问题。
|
|
147
|
-
* 但**契约里加了新成员,体检要能报出来** —— `probe.test.ts` 有一条判据解析
|
|
148
|
-
* `contract.ts` 断言「除了这份有意排除的名单之外必须全覆盖」。
|
|
149
|
-
*/
|
|
150
|
-
export declare const PROBE_EXCLUDED_MEMBERS: Record<string, string>;
|
|
151
|
-
/**
|
|
152
|
-
* 体检一个已装载的供应商。
|
|
153
|
-
*
|
|
154
|
-
* 只跑 `probe: 'safe'` 的成员;`skip` 的只报存在性。绝不代替用户调有副作用的成员。
|
|
155
|
-
*
|
|
156
|
-
* @param models - 该供应商当前的模型启用状态(由调用方用 `router.modelsOf` 取,
|
|
157
|
-
* 走核心缓存不额外打上游)。取不到就传 undefined —— 报告里会如实写「拿不到」,
|
|
158
|
-
* 不能拿 0 冒充「一个模型都没有」。
|
|
159
|
-
*/
|
|
160
|
-
export declare function probeSupplier(loaded: LoadedSupplier, input?: ProbeInput): Promise<ProbeReport>;
|