dsh-sidecard-ask 1.2.1 → 1.3.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/CHANGELOG.md +19 -0
- package/README.md +38 -2
- package/client.js +66 -1
- package/index.js +81 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
**主题:把"客户端半的状态"变成宿主可读——补上导致前两次缺陷排查困难的观测盲点。**
|
|
6
|
+
|
|
7
|
+
- 新增 **`POST /sidecard-ask/api/diagnose`**:客户端半在启动、槽位落点变化、承载面回退时把一份受限摘要
|
|
8
|
+
(客户端版本、当前承载面、区域锚点是否可用、三个槽位的真实落点、两个适配器的可用性与失败原因、
|
|
9
|
+
侧边卡片插件的版本与能力列表)上报给宿主;`GET /state` 的 **`value.client`** 即可读到。
|
|
10
|
+
宿主对上报做**白名单 + 截断**(未知键丢弃、字符串 ≤200 字符、能力列表 ≤40 项、槽位 ≤8 项)。
|
|
11
|
+
- 客户端版本改为单一常量 `CLIENT_VERSION`:自检上报与 `api.version` 同源,报告不会声称一个页面其实没在跑的版本。
|
|
12
|
+
- 此前两次客户端静默失效(原生 tab 类型因 kind 冲突从未注册;客户端产物是上一代)都只能靠人工翻阅
|
|
13
|
+
槽位清单才发现,这条通道把它们变成一条命令就能查到的事实。
|
|
14
|
+
|
|
15
|
+
**同时复核 1.2.1 的修复在运行实例上生效**:`sidebar.right.pane.tab` 的占用者现在**同时**包含
|
|
16
|
+
`sidecard-ask:card`(本插件原生右侧栏)与 `dsh-better-sidebar:sidecard-ask:card:workbench`(better-sidebar 镜像),
|
|
17
|
+
两者均 active——kind 冲突已消除。
|
|
18
|
+
|
|
19
|
+
自测 295 → **322 项**(verify 53 / contract 135 / smoke 134),新增用例覆盖上报的接收、白名单裁剪、
|
|
20
|
+
截断、非法体拒绝,以及客户端上报体的构造。
|
|
21
|
+
|
|
3
22
|
## 1.2.1
|
|
4
23
|
|
|
5
24
|
**主题:核对 DSH 0.2.0-rc.1 的适配性,并修掉由此暴露的一个真实缺陷。**
|
package/README.md
CHANGED
|
@@ -212,6 +212,15 @@ better-sidebar 会把收到的每个 tab 描述符**镜像注册进同一个原
|
|
|
212
212
|
2. `contract-test` **模拟该注册表的"重复 kind 抛错"规则**,此类冲突今后会直接让测试失败;
|
|
213
213
|
3. 设置页自检行改为**同时报告两个适配器**的状态(此前原生适配器的错误压根看不到)。
|
|
214
214
|
|
|
215
|
+
**④ 修复后的运行实例复核(2026-09-29,1.3.0)**:`sidebar.right.pane.tab` 的占用者现在**同时**包含
|
|
216
|
+
|
|
217
|
+
| key | 来源 |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `sidecard-ask:card` | 本插件的**原生右侧栏**承载面(1.2.1 之前完全缺失) |
|
|
220
|
+
| `dsh-better-sidebar:sidecard-ask:card:workbench` | 经 dsh-better-sidebar 镜像注册的承载面(独立 kind) |
|
|
221
|
+
|
|
222
|
+
两者共存、均 `active: true` —— 即"同 kind 冲突"已被消除,`auto` 默认走原生右侧栏,强制 `sideSurface: better-sidebar` 仍可用。
|
|
223
|
+
|
|
215
224
|
---
|
|
216
225
|
|
|
217
226
|
## 六、未知 DSH API:占位接口与替换方式
|
|
@@ -237,8 +246,13 @@ ctx.inject(['sidebarRightTabs', 'sidebarRight'], (injected) => { ... })
|
|
|
237
246
|
- **占位 1(草稿降级)**:`ctx.get('conversation').input.for(scope)` → `{ state.getSnapshot().draft, setDraft(text) }`。
|
|
238
247
|
这是**未出现在服务目录里**的接口(属 harness 内部形态),所以只作为第二顺位降级;搜索 `PLACEHOLDER: composer-draft`。
|
|
239
248
|
- **占位 2(最后兜底)**:前两者都不可用时,插件抛出可读错误并提示用户复制文本,搜索 `PLACEHOLDER: clipboard-fallback`。
|
|
240
|
-
- **线协议**:客户端与宿主半之间是插件**自有的** `/sidecard-ask/api`(`GET /state`、`POST /ask|cancel|config|reset`),
|
|
249
|
+
- **线协议**:客户端与宿主半之间是插件**自有的** `/sidecard-ask/api`(`GET /state`、`POST /ask|cancel|config|reset|diagnose`),
|
|
241
250
|
不依赖任何 harness 内部 RPC;若未来 harness 提供正式的同进程 RPC,替换点就是 `client.js` §3 的 `postJson/streamAsk`。
|
|
251
|
+
- **`POST /diagnose`(客户端自检上报)**:宿主**看不到浏览器里的客户端半**,所以客户端在启动、槽位落点变化、
|
|
252
|
+
承载面回退时把自己的一份受限摘要(版本、承载面、区域锚点、槽位落点、两个适配器的可用性与原因、侧边卡片插件版本与能力)
|
|
253
|
+
POST 给宿主;随后 `GET /state` 的 `value.client` 就能读到它。宿主侧对上报做**白名单 + 长度截断**(未知键丢弃、
|
|
254
|
+
字符串截断到 200 字符、能力列表最多 40 项),所以页面即使被注入垃圾也不会污染状态面。
|
|
255
|
+
这条通道是在两次"客户端静默失效只能靠人工翻槽位清单才发现"之后补上的。
|
|
242
256
|
|
|
243
257
|
> 约定:所有占位点都写成 `PLACEHOLDER: <名字>` 注释 + 可运行的降级路径,替换时只需改该函数的实现,调用方(卡片、设置页)无需改动。
|
|
244
258
|
|
|
@@ -382,7 +396,7 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
|
|
|
382
396
|
```
|
|
383
397
|
|
|
384
398
|
三个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
|
|
385
|
-
当前规模:verify 53 项 + contract
|
|
399
|
+
当前规模:verify 53 项 + contract 135 项 + smoke 134 项 = **322 项全部通过**。
|
|
386
400
|
|
|
387
401
|
### 10.2 版本能力探测(§7 矩阵的来源)
|
|
388
402
|
|
|
@@ -409,8 +423,30 @@ $tmp = Join-Path $env:TEMP 'probe.json'
|
|
|
409
423
|
[System.IO.File]::WriteAllText($tmp, $body, (New-Object System.Text.UTF8Encoding($false)))
|
|
410
424
|
(Invoke-WebRequest http://127.0.0.1:8080/sidecard-ask/api/ask -Method POST `
|
|
411
425
|
-ContentType 'application/json; charset=utf-8' -InFile $tmp -TimeoutSec 240 -UseBasicParsing).Content
|
|
426
|
+
|
|
427
|
+
# 3) 客户端半的真实状态(浏览器跑的那份代码/槽位落点/两个适配器)
|
|
428
|
+
# 页面加载后 GET /state,看 value.client —— 这是唯一能从页面外读到客户端状态的通道
|
|
429
|
+
((Invoke-WebRequest http://127.0.0.1:8080/sidecard-ask/api/state -UseBasicParsing).Content | ConvertFrom-Json).value.client
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**客户端状态样例**(1.3.0 起):
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
{
|
|
436
|
+
"at": 1790555771338,
|
|
437
|
+
"version": "1.3.0",
|
|
438
|
+
"surface": "native-rightbar",
|
|
439
|
+
"zoneAnchors": true,
|
|
440
|
+
"sessionKnown": true,
|
|
441
|
+
"slots": { "overlay": "shell.overlay", "settings": "settings.section", "sessionProbe": "conversation.input.right" },
|
|
442
|
+
"native": { "available": true },
|
|
443
|
+
"better": { "available": true, "version": "0.22.1", "features": ["badge", "…", "tabMeta"] }
|
|
444
|
+
}
|
|
412
445
|
```
|
|
413
446
|
|
|
447
|
+
`native.reason` / `better.reason` 会写明**为什么**某个承载面不可用(例如 `no-tab-meta` 或注册表的报错原文)——
|
|
448
|
+
1.2.1 修掉的那个 tab kind 冲突正是靠这类信息才定位到的。
|
|
449
|
+
|
|
414
450
|
**2026-09-28 在 DSH 0.1.7-rc.2 上的实测结果**(`provider=spawn`、`sideTools=readonly`):
|
|
415
451
|
`start x1 → reasoning x181 → delta x90 → status x1 → done x1`,耗时 2.6 s,`done.text` 为真实模型答案。
|
|
416
452
|
这条记录同时说明:子代理启动、只读工具白名单(与真实工具表求交后)、流式帧桥接、SSE 线协议、运行结束后的 `dispose()` 都是**在真实宿主上跑通的**,
|
package/client.js
CHANGED
|
@@ -42,6 +42,13 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
settings: 'sidecard-ask-settings',
|
|
43
43
|
sessionProbe: 'sidecard-ask-session-probe',
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Version of this Client half. Single source of truth: the self-report to
|
|
47
|
+
* the Host and the module's `api.version` both read it, so a report can
|
|
48
|
+
* never claim a generation the browser is not actually running.
|
|
49
|
+
*/
|
|
50
|
+
const CLIENT_VERSION = '1.3.0'
|
|
51
|
+
|
|
45
52
|
/**
|
|
46
53
|
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
47
54
|
*/
|
|
@@ -941,6 +948,41 @@ window.__ModuleLoader__.load({
|
|
|
941
948
|
*/
|
|
942
949
|
let capturedComposerActions = null
|
|
943
950
|
|
|
951
|
+
/**
|
|
952
|
+
* What this Client half wants the Host to know about its own state.
|
|
953
|
+
*
|
|
954
|
+
* The Host cannot inspect a browser-side plugin, so this report is what
|
|
955
|
+
* makes a silent CLIENT-side failure visible from outside the page
|
|
956
|
+
* (`GET /sidecard-ask/api/state` → `value.client`). It was added after two
|
|
957
|
+
* such failures were only diagnosable by hand-inspecting the live slot
|
|
958
|
+
* inventory: a native tab type that never registered, and a client build
|
|
959
|
+
* that was still the previous generation.
|
|
960
|
+
*
|
|
961
|
+
* @param {object} state - `store.state`.
|
|
962
|
+
* @returns {object} the report body (allow-listed again on the Host).
|
|
963
|
+
*/
|
|
964
|
+
function buildClientReport(state) {
|
|
965
|
+
return {
|
|
966
|
+
// Read from the module's own version constant so the report can never
|
|
967
|
+
// claim a generation the browser is not actually running.
|
|
968
|
+
version: CLIENT_VERSION,
|
|
969
|
+
surface: state.surface,
|
|
970
|
+
zoneAnchors: state.zoneAnchors,
|
|
971
|
+
sessionKnown: typeof state.sessionId === 'string' && state.sessionId !== '',
|
|
972
|
+
slots: { ...state.slots },
|
|
973
|
+
native: {
|
|
974
|
+
available: surfaces.native.available(),
|
|
975
|
+
...(surfaces.native.error === null ? {} : { reason: String(surfaces.native.error) }),
|
|
976
|
+
},
|
|
977
|
+
better: {
|
|
978
|
+
available: surfaces.better.available(),
|
|
979
|
+
...(surfaces.better.version === null ? {} : { version: surfaces.better.version }),
|
|
980
|
+
...(surfaces.better.reason === null ? {} : { reason: String(surfaces.better.reason) }),
|
|
981
|
+
features: surfaces.better.features,
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
944
986
|
/**
|
|
945
987
|
* The card id an external host is CURRENTLY rendering. `CardHost` sets it
|
|
946
988
|
* on mount and clears it on unmount, which is what turns "the adapter
|
|
@@ -1743,6 +1785,7 @@ window.__ModuleLoader__.load({
|
|
|
1743
1785
|
if (!opened) {
|
|
1744
1786
|
store.set({ surface: 'flow' })
|
|
1745
1787
|
patchCard(card.id, { surface: 'flow' })
|
|
1788
|
+
scheduleReport(0)
|
|
1746
1789
|
} else {
|
|
1747
1790
|
// Render proof: an adapter that accepts the open but never
|
|
1748
1791
|
// mounts our body would leave an empty tab. Unless the card is
|
|
@@ -1757,6 +1800,7 @@ window.__ModuleLoader__.load({
|
|
|
1757
1800
|
store.set({ surface: 'flow', surfaceNote: t('surfaceUnproven') })
|
|
1758
1801
|
patchCard(card.id, { surface: 'flow' })
|
|
1759
1802
|
toast(t('surfaceUnproven'))
|
|
1803
|
+
scheduleReport(0)
|
|
1760
1804
|
}, 600)
|
|
1761
1805
|
pendingProofs.add(proof)
|
|
1762
1806
|
}
|
|
@@ -2118,10 +2162,26 @@ window.__ModuleLoader__.load({
|
|
|
2118
2162
|
}
|
|
2119
2163
|
}
|
|
2120
2164
|
|
|
2165
|
+
/**
|
|
2166
|
+
* Debounced self-report to the Host. Diagnostics are best-effort: a
|
|
2167
|
+
* failure is logged and never surfaces as a user-visible error.
|
|
2168
|
+
*/
|
|
2169
|
+
let reportTimer = null
|
|
2170
|
+
const scheduleReport = (delay = 600) => {
|
|
2171
|
+
if (reportTimer !== null) clearTimeout(reportTimer)
|
|
2172
|
+
reportTimer = setTimeout(() => {
|
|
2173
|
+
reportTimer = null
|
|
2174
|
+
void postJson('diagnose', buildClientReport(store.state)).catch((error) => {
|
|
2175
|
+
console.warn(`[${PLUGIN_ID}] 自检上报失败(不影响功能):`, error?.message ?? error)
|
|
2176
|
+
})
|
|
2177
|
+
}, delay)
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2121
2180
|
/** Record which rung a registration actually landed on. */
|
|
2122
2181
|
const noteSlot = (name, key) => {
|
|
2123
2182
|
const slots = { ...(store.state.slots ?? {}), [name]: key }
|
|
2124
2183
|
store.set({ slots })
|
|
2184
|
+
scheduleReport()
|
|
2125
2185
|
}
|
|
2126
2186
|
|
|
2127
2187
|
disposers.push(firstLiveSlot(
|
|
@@ -2340,12 +2400,16 @@ window.__ModuleLoader__.load({
|
|
|
2340
2400
|
|
|
2341
2401
|
// ── boot ─────────────────────────────────────────────────────────
|
|
2342
2402
|
void refreshState().catch(() => { /* the settings page shows the failure */ })
|
|
2403
|
+
// One report after the slot ladders have had time to settle, so the
|
|
2404
|
+
// Host ends up holding the CLIENT's real registration state.
|
|
2405
|
+
scheduleReport(2500)
|
|
2343
2406
|
disposers.push(() => {
|
|
2344
2407
|
for (const controller of controllers.values()) controller.abort()
|
|
2345
2408
|
controllers.clear()
|
|
2346
2409
|
for (const proof of pendingProofs) clearTimeout(proof)
|
|
2347
2410
|
pendingProofs.clear()
|
|
2348
2411
|
if (toastTimer !== null) clearTimeout(toastTimer)
|
|
2412
|
+
if (reportTimer !== null) clearTimeout(reportTimer)
|
|
2349
2413
|
store.set({
|
|
2350
2414
|
trigger: null,
|
|
2351
2415
|
popover: null,
|
|
@@ -2373,7 +2437,7 @@ window.__ModuleLoader__.load({
|
|
|
2373
2437
|
* `window` and exercises these without a browser.
|
|
2374
2438
|
*/
|
|
2375
2439
|
api: Object.freeze({
|
|
2376
|
-
version:
|
|
2440
|
+
version: CLIENT_VERSION,
|
|
2377
2441
|
/** Read-only state accessor for diagnostics and the test harness. */
|
|
2378
2442
|
snapshot: () => store.state,
|
|
2379
2443
|
pure: Object.freeze({
|
|
@@ -2391,6 +2455,7 @@ window.__ModuleLoader__.load({
|
|
|
2391
2455
|
selectionSignature,
|
|
2392
2456
|
composeMainPrompt,
|
|
2393
2457
|
askInMainConversation,
|
|
2458
|
+
buildClientReport,
|
|
2394
2459
|
renderRichText,
|
|
2395
2460
|
pickSurface,
|
|
2396
2461
|
dict: DICT,
|
package/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export const name = 'dsh-sidecard-ask'
|
|
|
42
42
|
export const inject = ['webServer']
|
|
43
43
|
|
|
44
44
|
/** Version of this plugin (kept in step with package.json by test/verify.mjs). */
|
|
45
|
-
export const PLUGIN_VERSION = '1.
|
|
45
|
+
export const PLUGIN_VERSION = '1.3.0'
|
|
46
46
|
|
|
47
47
|
/** Route prefix of the plugin's own API. */
|
|
48
48
|
export const ROUTE_PREFIX = '/sidecard-ask/api'
|
|
@@ -1047,6 +1047,58 @@ export function apply(ctx, patchConfig) {
|
|
|
1047
1047
|
const engine = createSideEngine(ctx, configOf)
|
|
1048
1048
|
ctx.effect(() => () => { engine.dispose() }, 'sidecard-ask: side engine')
|
|
1049
1049
|
|
|
1050
|
+
/**
|
|
1051
|
+
* The Client half's own status report, or null before the first one.
|
|
1052
|
+
*
|
|
1053
|
+
* This exists because the Client runs in a browser this plugin cannot
|
|
1054
|
+
* inspect: without it, a broken CLIENT-side registration (for example two
|
|
1055
|
+
* adapters colliding on one tab kind, or a slot that never went live) is
|
|
1056
|
+
* invisible from the Host — it only shows up as a missing entry deep in a
|
|
1057
|
+
* slot inventory. The Client posts a bounded, allow-listed summary after its
|
|
1058
|
+
* registration ladders settle; `/state` hands it back.
|
|
1059
|
+
*/
|
|
1060
|
+
let clientReport = null
|
|
1061
|
+
|
|
1062
|
+
/** Allow-listed, size-bounded view of one Client report. */
|
|
1063
|
+
function sanitizeClientReport(raw) {
|
|
1064
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
1065
|
+
const short = (value, limit = 200) => (typeof value === 'string' ? value.slice(0, limit) : undefined)
|
|
1066
|
+
const flag = value => (typeof value === 'boolean' ? value : undefined)
|
|
1067
|
+
const out = {
|
|
1068
|
+
at: Date.now(),
|
|
1069
|
+
version: short(raw.version, 40),
|
|
1070
|
+
surface: short(raw.surface, 40),
|
|
1071
|
+
zoneAnchors: flag(raw.zoneAnchors),
|
|
1072
|
+
sessionKnown: flag(raw.sessionKnown),
|
|
1073
|
+
slots: {},
|
|
1074
|
+
native: {},
|
|
1075
|
+
better: {},
|
|
1076
|
+
}
|
|
1077
|
+
if (raw.slots !== null && typeof raw.slots === 'object' && !Array.isArray(raw.slots)) {
|
|
1078
|
+
for (const [slot, key] of Object.entries(raw.slots).slice(0, 8)) {
|
|
1079
|
+
const name = short(slot, 40)
|
|
1080
|
+
const value = short(key, 80)
|
|
1081
|
+
if (name !== undefined && value !== undefined) out.slots[name] = value
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
for (const [target, source] of [['native', raw.native], ['better', raw.better]]) {
|
|
1085
|
+
if (source === null || typeof source !== 'object' || Array.isArray(source)) continue
|
|
1086
|
+
const available = flag(source.available)
|
|
1087
|
+
if (available !== undefined) out[target].available = available
|
|
1088
|
+
const version = short(source.version, 40)
|
|
1089
|
+
if (version !== undefined) out[target].version = version
|
|
1090
|
+
const reason = short(source.reason ?? source.error, 200)
|
|
1091
|
+
if (reason !== undefined && reason !== '') out[target].reason = reason
|
|
1092
|
+
if (Array.isArray(source.features)) {
|
|
1093
|
+
out[target].features = source.features
|
|
1094
|
+
.filter(feature => typeof feature === 'string')
|
|
1095
|
+
.slice(0, 40)
|
|
1096
|
+
.map(feature => feature.slice(0, 40))
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return out
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1050
1102
|
/** Whether the request may reach the plugin routes. */
|
|
1051
1103
|
const trustedHostsOf = () => {
|
|
1052
1104
|
const runtime = ctx.get('webRuntime')
|
|
@@ -1074,9 +1126,35 @@ export function apply(ctx, patchConfig) {
|
|
|
1074
1126
|
// Informational: which process-local stream the host bridges.
|
|
1075
1127
|
streamSource: 'agent/assistant-stream',
|
|
1076
1128
|
},
|
|
1129
|
+
// The Client half's last self-report (null until it posts one).
|
|
1130
|
+
client: clientReport,
|
|
1077
1131
|
})
|
|
1078
1132
|
}
|
|
1079
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* `/diagnose` — the Client half reports its own registration state.
|
|
1136
|
+
*
|
|
1137
|
+
* A browser-side plugin cannot be inspected from the Host, so this is the
|
|
1138
|
+
* only channel that turns "the card silently fell back" or "the native tab
|
|
1139
|
+
* never registered" into something readable from outside the page.
|
|
1140
|
+
*/
|
|
1141
|
+
const handleDiagnose = async (req, res) => {
|
|
1142
|
+
let body
|
|
1143
|
+
try {
|
|
1144
|
+
body = await readJsonBody(req)
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
writeError(res, 400, toWireError(error, 'bad-request').code, toWireError(error).message)
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
const report = sanitizeClientReport(body)
|
|
1150
|
+
if (report === null) {
|
|
1151
|
+
writeError(res, 400, 'bad-request', '诊断上报必须是对象')
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
clientReport = report
|
|
1155
|
+
writeOk(res, { accepted: true, at: report.at })
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1080
1158
|
/** `/ask` — one SSE stream per side answer. */
|
|
1081
1159
|
const handleAsk = async (req, res) => {
|
|
1082
1160
|
let body
|
|
@@ -1164,7 +1242,7 @@ export function apply(ctx, patchConfig) {
|
|
|
1164
1242
|
* The HTTP methods the plugin serves; kept in one place because the route
|
|
1165
1243
|
* registration has two shapes (see below).
|
|
1166
1244
|
*/
|
|
1167
|
-
const API_METHODS = ['state', 'ask', 'cancel', 'config', 'reset']
|
|
1245
|
+
const API_METHODS = ['state', 'ask', 'cancel', 'config', 'reset', 'diagnose']
|
|
1168
1246
|
|
|
1169
1247
|
/** One request → one response; the dispatcher owns path parsing and the fence. */
|
|
1170
1248
|
const routeHandler = async (req, res) => {
|
|
@@ -1197,6 +1275,7 @@ export function apply(ctx, patchConfig) {
|
|
|
1197
1275
|
else if (method === 'cancel') await handleCancel(req, res)
|
|
1198
1276
|
else if (method === 'config') await handleConfig(req, res)
|
|
1199
1277
|
else if (method === 'reset') handleReset(req, res)
|
|
1278
|
+
else if (method === 'diagnose') await handleDiagnose(req, res)
|
|
1200
1279
|
else writeError(res, 404, 'not-found', `未知接口 "${method}"`)
|
|
1201
1280
|
} catch (error) {
|
|
1202
1281
|
const wire = toWireError(error)
|