dsh-cherry-bridge 0.2.0 → 0.2.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.
@@ -46,6 +46,11 @@
46
46
  **权威来源**;实时 `interaction.updated` 流是**加速器/保鲜器**——它让插件不必为每张卡都
47
47
  回查一次,但**不保证**送达,也不构成资格依据。任何一张卡都必须仅凭 list 快照可应答。
48
48
 
49
+ **上游实证**:2.1.9 之前,桌面的通知发送链会被一次失败**永久毒死**——`RemoteConnection.send()`
50
+ 把可 rejected 的 Promise 直接存回链头,此后该连接上的 `interaction.updated` 等通知静默停发,
51
+ 而请求-应答不受影响(应答不走这条链)。2.1.9 由 PR #22593 修复。这就是「实时流只是加速器、
52
+ list 快照才是权威」的实证来源:一条健康的连接随时可能不再推送任何通知。
53
+
49
54
  **手动核验步骤**(与 `CONTRACT_DIFFERENCES[1].checklistStep` 同源):
50
55
 
51
56
  - [ ] approval-card-not-realtime: trigger a tool call on the real desktop that raises an approval card; confirm `cherry_interactions` finds it through `agent.interactions.list` even when no `interaction.updated` event arrived, and that `cherry_respond` still accepts the listed snapshot.
@@ -132,6 +137,7 @@ schema 已允许 `interrupted`,所以客户端**必须**处理,但当前服
132
137
  - [ ] 长回复(>4096 单元)经 `{ref}` 外置,聚合结果仍包含完整文本。
133
138
  - [ ] `cherry_read` 能按 messageId 读到该轮消息与 parts。
134
139
  - [ ] `wait: false` 立即返回命令回执,随后 `cherry_wait` / `cherry_events` 能续查到同一终态。
140
+ - [ ] 长回答(事件流超过 256 KiB,约 30 小节的一次长输出)时 `wait: true` 仍在终态后立即返回 `completed`;期间每 5 s 采一次 `GET /diagnostics` 的 `sessions[].cursor.seq`,确认它没有停住(修复前会冻结在窗口处,最终报 `timeout`)。
135
141
  - [ ] 同 `commandId` 同参重发 → 返回**同一回执**(幂等);异参重发 → `IDEMPOTENCY_CONFLICT` 透传。
136
142
 
137
143
  ### C. 交互应答
@@ -159,6 +165,14 @@ schema 已允许 `interrupted`,所以客户端**必须**处理,但当前服
159
165
  - [ ] 设置页同时渲染两张设备卡片,状态灯随各自连接独立变化。
160
166
  - [ ] 停用其中一台 → 该设备转 `disabled` 且套接字被断开;再启用 → 从档案重建连接。
161
167
  - [ ] `cherry_unpair` 移除一台 → 另一台连接与订阅不受影响。
168
+ - [ ] 设置页「忘记 / 恢复」:对某台设备的一个候选地址点「**忘记**」→ 该地址从「地址」行消失、
169
+ 出现在「**已忘记地址**」行;`GET /devices` 的 `forgottenHosts` 含它,且**重启桌面或重连后
170
+ 该地址不会被自动学回来**(`cherry_devices` 的 `action: "forget"` 是同一个写入)。
171
+ 点「**恢复**」→ 标记消失,桌面下次广播该地址时重新加入;只剩最后一个候选地址时点「忘记」
172
+ → 页面显示「不能忘记最后一个候选地址」而**不是** `last-address:` 前缀的服务端原文。
173
+ 另外核一条**冻结**规则:打开「地址维护」后,卡片上的「忘记 / 恢复」应变为**不可点**——那是
174
+ 因为该表单整体替换 `hosts`,它的草稿不会被卡片重新播种,编辑器开着时点「忘记」会被下一次
175
+ 「保存地址」连同标记一起覆盖掉;关掉编辑器即恢复可点。
162
176
 
163
177
  ### F. 工具启用范围(`/scope`)
164
178
 
@@ -269,7 +283,7 @@ http://127.0.0.1:23023/cherry-remote/api/scope`(宿主换过端口就换端口
269
283
  - [ ] 该端点同样受信任 fence 约束:非回环 Host、非同源 Origin、`sec-fetch-site: cross-site`
270
284
  一律 403;无 Origin 的非浏览器客户端放行。
271
285
  - [ ] 全流程日志不含 token / invitationSecret / accessToken / 消息文本 / part 文本。
272
- - [ ] **上游版本核验**(桌面端为 2.1.4–2.1.8 或已比对过的更新版本;依据见
286
+ - [ ] **上游版本核验**(桌面端为 2.1.4–2.1.10 或已比对过的更新版本;依据见
273
287
  src/AGENTS.md「Protocol basis」):
274
288
  - [ ] 桌面端**升级后无需重新配对**:设备档案直接连上,`desktopIdentity` 未变。
275
289
  - [ ] 桌面端为 2.1.5+ 且会话里有**来自手机上传的附件**时:该 part 为 `kind:"file"`
@@ -284,8 +298,65 @@ http://127.0.0.1:23023/cherry-remote/api/scope`(宿主换过端口就换端口
284
298
  `data` part(任务事件等)不是附件,原样透传。
285
299
  - [ ] `cherry_send` 传空白文本(如 `" "`)→ 本地返回 `text-empty`,对端未收到
286
300
  `agent.messages.send`。
301
+ - [ ] 桌面端为 2.1.8+ 且 hello 广播了 `agentMessageSummariesVersion: 1` 时:
302
+ `GET /cherry-remote/api/diagnostics` 顶层出现 `messageSummaries: 1`,且一次
303
+ `cherry_read` 正常返回消息与 parts(消息行走分页摘要,请求带 `partsMode:"paged"`,
304
+ part 经 `agent.parts.list` 按 messageId + revision 枚举)。桌面端未广播时该字段
305
+ **不出现**,读路径回落到字节安全的分页尺寸。
287
306
  - [ ] 桌面端广播了本插件不认识的支持字段时,`cherry_devices` 的 limits 投影仍只有
288
307
  4 个已知键(heartbeatMs/subscriptions/pageItems/inFlightRequests),连接不被拒。
289
308
  - [ ] `package.json` 中 `@cherrystudio/remote-protocol`、`@cherrystudio/remote-transport`
290
309
  为精确版本(无 `^`)。
291
310
  - [ ] `npm test` 全绿(含 `tests/contract/contract.test.ts`)。
311
+
312
+ ---
313
+
314
+ ## 第四部分:机器可验证项实测记录
315
+
316
+ A–G 各节要真人 + Electron 端批准,无法被 CI 化;下表只记录其中**不需要人工点击、agent
317
+ 在本机就能断言**的那部分,用于跨版本比对「哪些已经被证过」。带勾选框的条目仍是唯一判据。
318
+
319
+ ### 0.2.0(2026-10-27 · DSH profile `web` · 桌面端 ISV-ZYX 2.1.8)
320
+
321
+ | 项 | 结果 | 观测 |
322
+ | --- | --- | --- |
323
+ | 装机生效 | 通过 | 包 mtime `18:49:52` 早于宿主 PID 1 启动 `18:50:00`;装机 `dist/index.mjs`、`dist/index.d.mts`、`lib/client.js`、`locale/zh.json` 与发行 tarball sha256 全同 |
324
+ | 设备档案迁移 `schemaVersion` 2→4 | 通过 | `devices.json` 为 `schemaVersion: 4`,逐设备 `security` / `ports` 两个映射就位;设备**未重新配对**,重启后自行连上 |
325
+ | `GET /cherry-remote/api/devices` 新字段 | 通过 | 返回 `security` 与 `ports`(0.1.11 没有这两个键) |
326
+ | 分页摘要能力 | 通过 | `GET …/diagnostics` 顶层 `messageSummaries: 1`;`cherry_read` 正常返回历史会话,parts 由 `agent.parts.list` 回填 |
327
+ | 坏 pin 解析期拒绝(#153) | 通过 | `desktopIdentity: "not-a-peer-id"` → `invalid-payload`/`payload field "desktopIdentity" is not a peer id`;`connections`/`sockets`/`connectionsBuilt` 保持 1/1/1,设备数不变,**零 socket** |
328
+ | 失败分型 + 裁决有界(#142 / #148) | 通过 | 3 个 193 字符主机 → `unreachable`,每段形如 `…:9400: websocket: getaddrinfo ENOTFOUND … (+147 chars)`:分型在、端口保留、段 ≤240 |
329
+ | 进度行有界(#152) | 通过 | 宿主日志 `pairing stage validate (hosts: h0.… (+2 more))` 整行 251 字符 |
330
+
331
+ **本轮未覆盖**(环境不可达,**不是通过**):
332
+
333
+ - **#141 拨号总预算**:需要黑洞地址让每次拨号烧满 `connectTimeoutMs`,实测要挂满 30 s;
334
+ 由集成测试的 `dialBudgetMs: 0` 用例覆盖(断言 `connectionCount === 0` 且每个候选各出现一次)。
335
+ - **#133 `wsTls`**:被测环境是内网 `ws://`、没有 TLS 前置,配置该键只会被忽略;由
336
+ `tests/helpers/tls/dualPeer.ts` 的自签 TLS 握手用例覆盖。
337
+ - **#140 设置页 per-host 端口行**:该行**仅在**各主机端口与记录级 `port` 不一致时渲染,
338
+ 被测设备是单主机同端口;需要造一条混合端口记录才能走。
339
+ - **#161 `failureText`**:要制造一个 peer 可达的 caught failure(如 ack 失败)才会出现,
340
+ 本轮 `recentErrors` 为空。
341
+
342
+ ### 0.2.1(2026-11-01 · DSH profile `web` · 桌面端 ISV-ZYX 2.1.10)
343
+
344
+ | 项 | 结果 | 观测 |
345
+ | --- | --- | --- |
346
+ | 装机生效 | 通过 | 版本 `0.2.1`;包 mtime `23:19:54` 早于宿主 PID 1 启动 `23:20:24`;装机 `dist/index.mjs`、`dist/index.d.mts`、`lib/client.js`、`locale/zh.json` 与 npm `dsh-cherry-bridge@0.2.1` tarball(shasum `ae7ed02b…`)sha256 全同 |
347
+ | 档案无迁移 | 通过 | `devices.json` 仍是 `schemaVersion: 4`(本轮没有触发任何迁移);`forgottenHosts` 是**可选键、不升版本**(代码依据:档案校验器与视图),首次 `forget` 后出现在文件里。注:捕获里**没有 forget 之前的档案快照**,所以「之前不在文件里」不算本轮观测 |
348
+ | `GET /cherry-remote/api/devices` 新键 | 通过 | 每台设备多出 `forgottenHosts`(无标记时是空数组 `[]`,不是缺键);本机设备 `hosts` 两条、`security` / `ports` 各两条 |
349
+ | 忘记一条非末位地址(#185 / #204) | 通过 | `POST …/devices/<id>/forget {"hosts":["172.17.14.36"]}` → 200;同一响应里 `hosts` 少一条、`forgottenHosts` 多一条、`state` 变 `connecting`(驱逐),`security` / `ports` 同步删掉该键;档案已落盘。注:forget 之前的「两条地址」由收尾/live 的原状锚定,捕获的第一条事件就是 forget |
350
+ | **重连后不会学回来** | 通过 | 21 s 后 `state: connected`,`hosts` **仍是单条**(观测)。**推断**:桌面一直在广播两条、学习合并跳过了被标记者 —— 依据是下一行「恢复只摘标记」之后 18 s 地址就自己回来了,中间没有别的写入 |
351
+ | 恢复只摘标记 | 通过 | `POST …/remember` → 200,`forgottenHosts: []` 而 `hosts` **未变**;再 18 s 后 `hosts` 回到两条,`security` / `ports` 里重新出现该键(`ws` / 23333,这两个值是否对端所报见下方「未覆盖」) |
352
+ | 拒绝忘记最后一条 | 通过 | 只剩一条时 `forget` → `400` `{"error":"last-address","message":"… refusing to forget the last dial candidate — the record would have nothing to dial. Keep at least one address."}`;档案**未变**(`hosts` 与标记都保持原样;代码在持久化**之前**抛出) |
353
+ | 驱逐的代价可观测 | 通过 | 四次改动 = 四次重建:`stats.connectionsBuilt` 收尾 **5**,两次改动后读到 **3**(故基线推为 1,`1` 本身没有直接读到);`connections` / `sockets` 两次读到 1/1(23:25:00 那次没取到 `stats`);`recentErrors` 恰好 **4** 条 `CONNECTION_OFFLINE`(`device transitioned to offline`,每次驱逐一条,无其他;捕获里第 4 条被截断,`/diagnostics` 现读为 4);收尾回到原状(两条地址、无标记、`connected`) |
354
+
355
+ **本轮未覆盖**(**不是通过**):
356
+
357
+ - **恢复后的端口 / 协议是否真来自对端**:本环境桌面广播的就是记录级默认(`ws` / 23333),
358
+ 观测无法区分「对端说的」与「默认值」;由 `DeviceStore.learnEndpoints` 与
359
+ `advertisedEndpoints` 的单测覆盖(每个端点自带 port/scheme)。
360
+ - **设置页那对按钮的点击**:需要能打开宿主的浏览器;本机渲染由 `tests/unit/ui.test.ts`
361
+ 覆盖,地址编辑器打开时两个控件冻结由 `#204m` 覆盖。本轮走的是页面调用的同两条 HTTP 路由。
362
+ - **驱逐时 in-flight 命令丢失**:需要一个正在执行的 command 才会出现,本轮没有。