dsh-sidecard-ask 1.2.0 → 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 +42 -0
- package/README.md +68 -3
- package/client.js +113 -11
- package/index.js +81 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
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
|
+
|
|
22
|
+
## 1.2.1
|
|
23
|
+
|
|
24
|
+
**主题:核对 DSH 0.2.0-rc.1 的适配性,并修掉由此暴露的一个真实缺陷。**
|
|
25
|
+
|
|
26
|
+
核对(本机运行中的就是 0.2.0-rc.1,`@deepseek-ai/dsh` 的 `next` 通道):
|
|
27
|
+
|
|
28
|
+
- 包内容层面:`node tools/compat-probe.mjs 0.1.7-rc.2 0.2.0-rc.1` → 16 项能力**逐项一致**,没有任何依赖被移除或改名;
|
|
29
|
+
- 运行实例上:宿主半 `v1.2.0` 正常(`sideEngine`、provider `spawn,fork`、`capabilities.parent` 均可用);
|
|
30
|
+
客户端半在 `shell.overlay` / `conversation.input.right` 都已注册;`standardProps` 仍含 `sessionId` 与 `inputActions`;
|
|
31
|
+
真机端到端作答 `start→delta×31→status→done`(1.36 s)通过。新增的 `sessions.fork`/`uiWorkspace.forkSession`
|
|
32
|
+
可选参数 `onCreated` 是纯增量,不影响既有调用。
|
|
33
|
+
|
|
34
|
+
**修掉的缺陷**:`sidebar.right.pane.tab` 的运行清单里只有 `dsh-better-sidebar:sidecard-ask:card`,
|
|
35
|
+
没有本插件原生右侧栏的 `sidecard-ask:card`——两个适配器用了同一个 tab kind,而 better-sidebar 会把每个 tab 描述符
|
|
36
|
+
镜像注册进**同一个**原生注册表(band `extension`),该注册表对同 band 同 kind 的二次注册抛错,后注册的一方静默失败。
|
|
37
|
+
|
|
38
|
+
- better-sidebar 承载面改用独立类型 `sidecard-ask:card:workbench`(`CARD_KIND_BETTER`),两端不再重叠;
|
|
39
|
+
- `contract-test` 新增"surface kinds must not collide"一节:**在测试里模拟该注册表的重复 kind 抛错规则**
|
|
40
|
+
与 better-sidebar 的镜像注册行为,若两者再共用 kind,测试立即失败;
|
|
41
|
+
- 设置页自检行改为同时报告**两个适配器**(原先原生适配器的错误在界面上不可见,只能靠翻槽位清单发现)。
|
|
42
|
+
|
|
43
|
+
自测 289 → **295 项**(verify 53 / contract 124 / smoke 118)。
|
|
44
|
+
|
|
3
45
|
## 1.2.0
|
|
4
46
|
|
|
5
47
|
**主题:核对并加固与 dsh-better-sidebar 0.22.1 的适配。**
|
package/README.md
CHANGED
|
@@ -183,6 +183,44 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
|
|
|
183
183
|
|
|
184
184
|
> 注意:`version` 是加载到浏览器里的那份模块报告的版本。页面刷新后即可用它确认"跑的是不是 0.22.1"。
|
|
185
185
|
|
|
186
|
+
### 5.2 与 DSH 0.2.0-rc.1 的适配核对(2026-09-29)
|
|
187
|
+
|
|
188
|
+
本机实际运行的就是 **0.2.0-rc.1**(`@deepseek-ai/dsh` 的 `next` 通道;`latest` 仍是 0.1.7-rc.2)。核对分两层:
|
|
189
|
+
|
|
190
|
+
**① 包内容层面**(`node tools/compat-probe.mjs 0.1.7-rc.2 0.2.0-rc.1`):
|
|
191
|
+
16 项能力**逐项与 0.1.7-rc.2 完全一致**——`shell.overlay`、`conversation.input.right`、`settings.section`、
|
|
192
|
+
`data-slot` 锚点、原生右侧栏、`sessions.using/retain`、归档门、`agent/assistant-stream`、子代理、`tools.schemas`、
|
|
193
|
+
`webServer` 路由……全部 ✅。
|
|
194
|
+
|
|
195
|
+
**② 在运行中的实例上实测**:
|
|
196
|
+
|
|
197
|
+
| 检查 | 结果 |
|
|
198
|
+
|---|---|
|
|
199
|
+
| 宿主半是否加载 | ✅ `/sidecard-ask/api/state` 返回 `dsh-sidecard-ask v1.2.0`,`sideEngine=true`(provider `spawn,fork`)、`capabilities.parent` 正常 |
|
|
200
|
+
| 客户端半是否注册 | ✅ `shell.overlay` 有 `sidecard-ask`(order 45、active)、`conversation.input.right` 有 `sidecard-ask-session-probe`、`settings.section` 有设置页 |
|
|
201
|
+
| 依赖的槽位与标准 prop | ✅ `conversation.input.right` 仍是 list/session,`standardProps` 仍含 **`sessionId`** 与 **`inputActions`** |
|
|
202
|
+
| 依赖的客户端服务 | ✅ `sessions`(`retain`/`using`/`scope`)、`slots`、`locale`、`layout`、`theme` 全在;`sessions.fork`/`uiWorkspace.forkSession` 只是**新增**了可选 `onCreated`,不影响既有调用 |
|
|
203
|
+
| 端到端作答 | ✅ 真机 `start→delta×31→status→done`(1.36 s,`streamSource=frames`),只读工具白名单照常生效 |
|
|
204
|
+
|
|
205
|
+
**③ 由此发现并修掉的真实缺陷(1.2.1)**:运行中的 `sidebar.right.pane.tab` 清单里只有
|
|
206
|
+
`dsh-better-sidebar:sidecard-ask:card`,**没有我的原生 `sidecard-ask:card`**。原因是两个适配器用了**同一个 tab kind**:
|
|
207
|
+
better-sidebar 会把收到的每个 tab 描述符**镜像注册进同一个原生注册表**(`sidebarRightTabs.register`,band `extension`),
|
|
208
|
+
而该注册表对*同 band 同 kind 的二次注册直接抛错*(源码注释:*"Everything else colliding on a kind throws"*)——
|
|
209
|
+
后注册的那一方就此失败,而且**在界面上不可见**。修法:
|
|
210
|
+
|
|
211
|
+
1. better-sidebar 承载面改用独立类型 **`sidecard-ask:card:workbench`**,两端 kind 不再重叠;
|
|
212
|
+
2. `contract-test` **模拟该注册表的"重复 kind 抛错"规则**,此类冲突今后会直接让测试失败;
|
|
213
|
+
3. 设置页自检行改为**同时报告两个适配器**的状态(此前原生适配器的错误压根看不到)。
|
|
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
|
+
|
|
186
224
|
---
|
|
187
225
|
|
|
188
226
|
## 六、未知 DSH API:占位接口与替换方式
|
|
@@ -208,8 +246,13 @@ ctx.inject(['sidebarRightTabs', 'sidebarRight'], (injected) => { ... })
|
|
|
208
246
|
- **占位 1(草稿降级)**:`ctx.get('conversation').input.for(scope)` → `{ state.getSnapshot().draft, setDraft(text) }`。
|
|
209
247
|
这是**未出现在服务目录里**的接口(属 harness 内部形态),所以只作为第二顺位降级;搜索 `PLACEHOLDER: composer-draft`。
|
|
210
248
|
- **占位 2(最后兜底)**:前两者都不可用时,插件抛出可读错误并提示用户复制文本,搜索 `PLACEHOLDER: clipboard-fallback`。
|
|
211
|
-
- **线协议**:客户端与宿主半之间是插件**自有的** `/sidecard-ask/api`(`GET /state`、`POST /ask|cancel|config|reset`),
|
|
249
|
+
- **线协议**:客户端与宿主半之间是插件**自有的** `/sidecard-ask/api`(`GET /state`、`POST /ask|cancel|config|reset|diagnose`),
|
|
212
250
|
不依赖任何 harness 内部 RPC;若未来 harness 提供正式的同进程 RPC,替换点就是 `client.js` §3 的 `postJson/streamAsk`。
|
|
251
|
+
- **`POST /diagnose`(客户端自检上报)**:宿主**看不到浏览器里的客户端半**,所以客户端在启动、槽位落点变化、
|
|
252
|
+
承载面回退时把自己的一份受限摘要(版本、承载面、区域锚点、槽位落点、两个适配器的可用性与原因、侧边卡片插件版本与能力)
|
|
253
|
+
POST 给宿主;随后 `GET /state` 的 `value.client` 就能读到它。宿主侧对上报做**白名单 + 长度截断**(未知键丢弃、
|
|
254
|
+
字符串截断到 200 字符、能力列表最多 40 项),所以页面即使被注入垃圾也不会污染状态面。
|
|
255
|
+
这条通道是在两次"客户端静默失效只能靠人工翻槽位清单才发现"之后补上的。
|
|
213
256
|
|
|
214
257
|
> 约定:所有占位点都写成 `PLACEHOLDER: <名字>` 注释 + 可运行的降级路径,替换时只需改该函数的实现,调用方(卡片、设置页)无需改动。
|
|
215
258
|
|
|
@@ -353,7 +396,7 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
|
|
|
353
396
|
```
|
|
354
397
|
|
|
355
398
|
三个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
|
|
356
|
-
当前规模:verify 53 项 + contract
|
|
399
|
+
当前规模:verify 53 项 + contract 135 项 + smoke 134 项 = **322 项全部通过**。
|
|
357
400
|
|
|
358
401
|
### 10.2 版本能力探测(§7 矩阵的来源)
|
|
359
402
|
|
|
@@ -380,8 +423,30 @@ $tmp = Join-Path $env:TEMP 'probe.json'
|
|
|
380
423
|
[System.IO.File]::WriteAllText($tmp, $body, (New-Object System.Text.UTF8Encoding($false)))
|
|
381
424
|
(Invoke-WebRequest http://127.0.0.1:8080/sidecard-ask/api/ask -Method POST `
|
|
382
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
|
+
}
|
|
383
445
|
```
|
|
384
446
|
|
|
447
|
+
`native.reason` / `better.reason` 会写明**为什么**某个承载面不可用(例如 `no-tab-meta` 或注册表的报错原文)——
|
|
448
|
+
1.2.1 修掉的那个 tab kind 冲突正是靠这类信息才定位到的。
|
|
449
|
+
|
|
385
450
|
**2026-09-28 在 DSH 0.1.7-rc.2 上的实测结果**(`provider=spawn`、`sideTools=readonly`):
|
|
386
451
|
`start x1 → reasoning x181 → delta x90 → status x1 → done x1`,耗时 2.6 s,`done.text` 为真实模型答案。
|
|
387
452
|
这条记录同时说明:子代理启动、只读工具白名单(与真实工具表求交后)、流式帧桥接、SSE 线协议、运行结束后的 `dispose()` 都是**在真实宿主上跑通的**,
|
|
@@ -471,5 +536,5 @@ npm deprecate "<旧包名>@*" "Renamed to <新包名> - <原因>" # 旧名
|
|
|
471
536
|
本仓库当前文件比运行中的宿主模块新(1.0.1 的「归档父会话不再一刀切 / 持久化 chunk 流式源 / 能力门控 / 路由降级」),
|
|
472
537
|
**重启后**这些改动才生效;`/state` 的 `version` 与 `capabilities.parent` 字段可以直接确认是否已加载新版。
|
|
473
538
|
6. **除 0.1.7-rc.2 外,其余版本只做了产物层验证**:见 §7.3——插件在 0.1.2-rc.1 … 0.1.6-alpha.2 上安装启动过**没有**得到验证,
|
|
474
|
-
验证到的是"这些版本缺哪些 API + 插件对每个缺失都有分支",分支本身由
|
|
539
|
+
验证到的是"这些版本缺哪些 API + 插件对每个缺失都有分支",分支本身由 289 项自测覆盖。
|
|
475
540
|
|
package/client.js
CHANGED
|
@@ -42,9 +42,34 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
settings: 'sidecard-ask-settings',
|
|
43
43
|
sessionProbe: 'sidecard-ask-session-probe',
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
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
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tab kind served by the DSH native right rail (also its implementation id).
|
|
54
|
+
*/
|
|
46
55
|
const CARD_KIND = 'sidecard-ask:card'
|
|
47
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Tab type served by the dsh-better-sidebar plugin.
|
|
59
|
+
*
|
|
60
|
+
* It MUST differ from {@link CARD_KIND}: better-sidebar mirrors every tab
|
|
61
|
+
* descriptor it is given into the SAME native registry
|
|
62
|
+
* (`sidebarRightTabs.register`, id `dsh-better-sidebar:<id>`, band
|
|
63
|
+
* `extension`), and that registry THROWS when a kind already has a
|
|
64
|
+
* registration in the same band ("Everything else colliding on a kind
|
|
65
|
+
* throws"). Sharing one kind made the two adapters collide: whichever
|
|
66
|
+
* registered second failed — observed live on 0.2.0-rc.1, where the pane
|
|
67
|
+
* listed `dsh-better-sidebar:sidecard-ask:card` and no native
|
|
68
|
+
* `sidecard-ask:card` at all. `test/contract-test.mjs` now emulates that
|
|
69
|
+
* registry rule so the collision cannot come back.
|
|
70
|
+
*/
|
|
71
|
+
const CARD_KIND_BETTER = 'sidecard-ask:card:workbench'
|
|
72
|
+
|
|
48
73
|
/**
|
|
49
74
|
* Client-side mirror of the Host `DEFAULT_CONFIG`. The client needs values
|
|
50
75
|
* before `/state` answers (first paint) and when the Host is unreachable;
|
|
@@ -170,6 +195,7 @@ window.__ModuleLoader__.load({
|
|
|
170
195
|
diagSidecard: '侧边卡片插件(dsh-better-sidebar)',
|
|
171
196
|
diagFeatures: '项能力',
|
|
172
197
|
diagNoTabMeta: '版本过旧:没有 tabMeta 能力,已跳过它改用其它承载面',
|
|
198
|
+
diagNativeKind: 'kind',
|
|
173
199
|
toolGuardUnavailable: '该 provider 不支持工具白名单,本次作答继承了当前会话的工具',
|
|
174
200
|
toolGuardInherited: '按配置继承当前会话的工具',
|
|
175
201
|
diagUnreachable: '宿主接口不可达:{msg}',
|
|
@@ -272,6 +298,7 @@ window.__ModuleLoader__.load({
|
|
|
272
298
|
diagSidecard: 'Side-card plugin (dsh-better-sidebar)',
|
|
273
299
|
diagFeatures: 'capabilities',
|
|
274
300
|
diagNoTabMeta: 'too old: no tabMeta capability, skipped in favour of another surface',
|
|
301
|
+
diagNativeKind: 'kind',
|
|
275
302
|
toolGuardUnavailable: 'This provider supports no tool allow-list, so the answer inherited the session tools',
|
|
276
303
|
toolGuardInherited: 'Inherits the session tools, as configured',
|
|
277
304
|
diagUnreachable: 'Host API unreachable: {msg}',
|
|
@@ -889,20 +916,20 @@ window.__ModuleLoader__.load({
|
|
|
889
916
|
},
|
|
890
917
|
open(card) {
|
|
891
918
|
if (!this.available() || typeof this.service.openTab !== 'function') return false
|
|
892
|
-
const tabId = `${
|
|
919
|
+
const tabId = `${CARD_KIND_BETTER}:${card.id}`
|
|
893
920
|
// `meta` is the transport for the card id (feature `tabMeta`), and
|
|
894
921
|
// `dedupeKey` on our descriptor is what collapses repeat opens onto
|
|
895
922
|
// the same tab; both are part of the stable consumer contract
|
|
896
923
|
// (`lib/types/client/service.d.ts`, unchanged across 0.22.0 → 0.22.1).
|
|
897
924
|
this.service.openTab(
|
|
898
|
-
{ type:
|
|
925
|
+
{ type: CARD_KIND_BETTER, id: tabId, title: card.question.slice(0, 32), meta: { cardId: card.id } },
|
|
899
926
|
store.state.sessionId === null ? undefined : { sessionId: store.state.sessionId },
|
|
900
927
|
)
|
|
901
928
|
return true
|
|
902
929
|
},
|
|
903
930
|
close(card) {
|
|
904
931
|
if (!this.available() || typeof this.service.closeTab !== 'function') return false
|
|
905
|
-
this.service.closeTab(`${
|
|
932
|
+
this.service.closeTab(`${CARD_KIND_BETTER}:${card.id}`)
|
|
906
933
|
return true
|
|
907
934
|
},
|
|
908
935
|
},
|
|
@@ -921,6 +948,41 @@ window.__ModuleLoader__.load({
|
|
|
921
948
|
*/
|
|
922
949
|
let capturedComposerActions = null
|
|
923
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
|
+
|
|
924
986
|
/**
|
|
925
987
|
* The card id an external host is CURRENTLY rendering. `CardHost` sets it
|
|
926
988
|
* on mount and clears it on unmount, which is what turns "the adapter
|
|
@@ -1554,7 +1616,7 @@ window.__ModuleLoader__.load({
|
|
|
1554
1616
|
h('div', { key: 'slots' }, `${t('diagSlots')}: ${Object.entries(state.slots).map(([k, v]) => `${k}=${v}`).join(' · ')}`),
|
|
1555
1617
|
// The side-card plugin's own report: which version is loaded and
|
|
1556
1618
|
// whether it advertises the capability this adapter needs.
|
|
1557
|
-
h('div', { key: 'sidecard' }, `${t('diagSidecard')}: ${
|
|
1619
|
+
h('div', { key: 'sidecard' }, `${t('diagSidecard')}: ${describeSurfaces()}`),
|
|
1558
1620
|
h('div', { key: 'path' }, `${t('settingsTitle')} → ${diag.provenance?.persistedPath ?? ''}`),
|
|
1559
1621
|
])
|
|
1560
1622
|
: null)
|
|
@@ -1582,10 +1644,22 @@ window.__ModuleLoader__.load({
|
|
|
1582
1644
|
}
|
|
1583
1645
|
|
|
1584
1646
|
/**
|
|
1585
|
-
* One line describing how
|
|
1586
|
-
* settings page's capability check.
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1647
|
+
* One line describing how each optional side-card surface resolved, for the
|
|
1648
|
+
* settings page's capability check. Both adapters are reported, because a
|
|
1649
|
+
* failing NATIVE registration was previously invisible here (it only showed
|
|
1650
|
+
* up as a missing entry in the pane's own slot inventory).
|
|
1651
|
+
* @returns {string} human-readable state.
|
|
1652
|
+
*/
|
|
1653
|
+
function describeSurfaces() {
|
|
1654
|
+
const native = surfaces.native.available()
|
|
1655
|
+
? `${t('yes')} · ${t('diagNativeKind')}=${CARD_KIND}`
|
|
1656
|
+
: `${t('no')}${surfaces.native.error === null ? '' : `(${surfaces.native.error})`}`
|
|
1657
|
+
return `${t('surfaceNative')}: ${native} / ${t('surfaceBetter')}: ${describeSidecardAdapter()}`
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* The optional side-card plugin's own report: which version is loaded and
|
|
1662
|
+
* whether it advertises the capability this adapter needs.
|
|
1589
1663
|
* @returns {string} human-readable state.
|
|
1590
1664
|
*/
|
|
1591
1665
|
function describeSidecardAdapter() {
|
|
@@ -1711,6 +1785,7 @@ window.__ModuleLoader__.load({
|
|
|
1711
1785
|
if (!opened) {
|
|
1712
1786
|
store.set({ surface: 'flow' })
|
|
1713
1787
|
patchCard(card.id, { surface: 'flow' })
|
|
1788
|
+
scheduleReport(0)
|
|
1714
1789
|
} else {
|
|
1715
1790
|
// Render proof: an adapter that accepts the open but never
|
|
1716
1791
|
// mounts our body would leave an empty tab. Unless the card is
|
|
@@ -1725,6 +1800,7 @@ window.__ModuleLoader__.load({
|
|
|
1725
1800
|
store.set({ surface: 'flow', surfaceNote: t('surfaceUnproven') })
|
|
1726
1801
|
patchCard(card.id, { surface: 'flow' })
|
|
1727
1802
|
toast(t('surfaceUnproven'))
|
|
1803
|
+
scheduleReport(0)
|
|
1728
1804
|
}, 600)
|
|
1729
1805
|
pendingProofs.add(proof)
|
|
1730
1806
|
}
|
|
@@ -2086,10 +2162,26 @@ window.__ModuleLoader__.load({
|
|
|
2086
2162
|
}
|
|
2087
2163
|
}
|
|
2088
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
|
+
|
|
2089
2180
|
/** Record which rung a registration actually landed on. */
|
|
2090
2181
|
const noteSlot = (name, key) => {
|
|
2091
2182
|
const slots = { ...(store.state.slots ?? {}), [name]: key }
|
|
2092
2183
|
store.set({ slots })
|
|
2184
|
+
scheduleReport()
|
|
2093
2185
|
}
|
|
2094
2186
|
|
|
2095
2187
|
disposers.push(firstLiveSlot(
|
|
@@ -2245,7 +2337,11 @@ window.__ModuleLoader__.load({
|
|
|
2245
2337
|
return
|
|
2246
2338
|
}
|
|
2247
2339
|
const off = service.registerTab({
|
|
2248
|
-
|
|
2340
|
+
// A DIFFERENT type from the native rail's: better-sidebar mirrors
|
|
2341
|
+
// this descriptor into `sidebarRightTabs` as an `extension`-band
|
|
2342
|
+
// registration, and a second registration of the same kind in
|
|
2343
|
+
// that band throws (see CARD_KIND_BETTER).
|
|
2344
|
+
id: CARD_KIND_BETTER,
|
|
2249
2345
|
title: () => t('answerTitle'),
|
|
2250
2346
|
description: () => t('settingsDesc'),
|
|
2251
2347
|
order: 120,
|
|
@@ -2304,12 +2400,16 @@ window.__ModuleLoader__.load({
|
|
|
2304
2400
|
|
|
2305
2401
|
// ── boot ─────────────────────────────────────────────────────────
|
|
2306
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)
|
|
2307
2406
|
disposers.push(() => {
|
|
2308
2407
|
for (const controller of controllers.values()) controller.abort()
|
|
2309
2408
|
controllers.clear()
|
|
2310
2409
|
for (const proof of pendingProofs) clearTimeout(proof)
|
|
2311
2410
|
pendingProofs.clear()
|
|
2312
2411
|
if (toastTimer !== null) clearTimeout(toastTimer)
|
|
2412
|
+
if (reportTimer !== null) clearTimeout(reportTimer)
|
|
2313
2413
|
store.set({
|
|
2314
2414
|
trigger: null,
|
|
2315
2415
|
popover: null,
|
|
@@ -2337,13 +2437,14 @@ window.__ModuleLoader__.load({
|
|
|
2337
2437
|
* `window` and exercises these without a browser.
|
|
2338
2438
|
*/
|
|
2339
2439
|
api: Object.freeze({
|
|
2340
|
-
version:
|
|
2440
|
+
version: CLIENT_VERSION,
|
|
2341
2441
|
/** Read-only state accessor for diagnostics and the test harness. */
|
|
2342
2442
|
snapshot: () => store.state,
|
|
2343
2443
|
pure: Object.freeze({
|
|
2344
2444
|
CLIENT_DEFAULTS,
|
|
2345
2445
|
IDS,
|
|
2346
2446
|
CARD_KIND,
|
|
2447
|
+
CARD_KIND_BETTER,
|
|
2347
2448
|
parseSseBlock,
|
|
2348
2449
|
truncateSelection,
|
|
2349
2450
|
parseShortcut,
|
|
@@ -2354,6 +2455,7 @@ window.__ModuleLoader__.load({
|
|
|
2354
2455
|
selectionSignature,
|
|
2355
2456
|
composeMainPrompt,
|
|
2356
2457
|
askInMainConversation,
|
|
2458
|
+
buildClientReport,
|
|
2357
2459
|
renderRichText,
|
|
2358
2460
|
pickSurface,
|
|
2359
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)
|