docs: 自主推进一批的登记(假开关 / 静默丢正文 / 分享自打断 全修完 + 一条口径冲突要主人拍板)

主人睡前说「按主线继续推,或者先按模块检查」,我把两件都做了:先做了一次只读自审
(假开关 / 死代码 / UI 规范 / 文档漂移,四张带「文件:行号」的清单),然后按清单修了一批。
本批全部是「界面上或文档上声称能做到、代码里其实没接线」这一类问题。

devdocs/05:新增「锚点补记三」,写清——
- 本批 5 条落地(W25 打断模式真接线 / W26 stream_chunk 不再静默丢正文、错误横幅可关闭、
  分享不再自相打断、两处死代码 / W27 删只被测试养的 API、公式去重、03 四处更正 /
  WsEvent KDoc 更正);
- 三个「假开关 / 静默」的真相与各自的修法;
- 两条查完发现不是 bug 的(「抽屉最后消息预览不刷新」与「session_interrupted 丢弃 N 条」),
  附证据,免得下次返工;
- 装机验证:错误行的关闭叉验通(x2/x3 截图),以及一条我刻意没验的(登录页 IME ——
  要清空 App 数据才能回到登录页,万一登不回去今晚所有装机验证能力一起没了,风险不对称);
- 本批没做的:连接所有权(共享 socket)只写设计要点与七条验收清单,不动手;
  DocumentDelivery 那组「正文转文件」API 不删也不接(口径冲突);
- W26 留下的一条边界(回退路径没有 message.start,freshTurnIfFinished 不触发)。

devdocs/02:追加客户端登记三条——
1. 请后端确认 stream_chunk 今天还会不会发、delta 是增量还是整段、会不会与结构化帧并存
   (我们按契约的「非流式回退路径」实现,但那条路可能已经名存实亡);
2. 告知我们开始发 interrupt{scope:"current"} 了(窄触发条件、默认档零变化、分享不打断、
   以及「两条道无强序」这个我们坦承的边界);
3. 登记「超限走文件」的口径冲突(05 的定案说做的是「用户打的字自动变附件」不做,
   主人后来点名要做 —— 两者多半不是一件事),不替主人合并,所以那组 API 原样留着。

本次提交只动文档,无代码改动。
This commit is contained in:
AskaEth committed 2026-10-04 22:40:43 +08:00
1 parent 85a922c01b
commit 219c50b7c2
2 files changed
+140 -1

No files matched your search

+82 -1
View File
@@ -690,7 +690,7 @@ PDF · 563 B · 24 字 · 已读入
### 状态
- **`android` = `origin/android` = `1794e53`,工作区干净**,**884 测试 / 0 失败 / 0 错误**(`--rerun-tasks` 全量重跑后自己读 JUnit XML 求和)。
- **`android` = `origin/android` = `85a922c`,工作区干净**,**891 测试 / 0 失败 / 0 错误**(`--rerun-tasks` 全量重跑后自己读 JUnit XML 求和)。
- 可安装包:`app/build/outputs/apk/debug/app-debug.apk`。
- 那轮「装机验收」的**四项全部有设备证据**(含 ① 的更正:本来就是好的,是我测量假象):见上一节「装机验收轮」。
- **联调凭据(压缩后最容易丢,务必先看这条)**:后端调试用的 `X-API-Key` **已由主人授权落地到本机 `dev-secrets.local.md`**(仓库根目录,**已 gitignore、不在仓库里**——已用 `git check-ignore` / `git ls-files` 双重核对)。用法:`curl -H "X-API-Key: <该文件里的值>" $BASE/api/v1/…`,**owner 级**(`/sessions`、`/admin/observability/{metrics,logs}`、`/admin/system/storage`、`/admin/personas` 全 `200`;不带 key 回 `401`)。**上一个 key(`rG14nMNX…`)已失效(实测 `401`),不要再试。** 轮换只改那个文件,代码/文档里没有任何硬编码。
@@ -839,3 +839,84 @@ PDF · 563 B · 24 字 · 已读入
> ⚠️ **顺手查到但本轮没动**:`data/ws/WsEvent.kt:272` 的 `SessionInterrupted` KDoc 也写着
> 「当前任务取消、pending 清空,`dropped` 为丢弃条数」——与上面第 2 条同一个过时前提。
> 它不在本轮允许改动的文件清单里,留给下一个动那个文件的会话顺手更正。
> **★ 锚点补记三(2026-10-04 深夜续二 · 自主推进一批)**:主人去睡了,我按主线往下推 + 做了一次只读审计。
> 这一批**全部是「界面上/文档上声称能做到、代码里其实没接线」类的问题**(主人最恨的一类)。
>
> ### 本批落地(HEAD `85a922c`,891 测试 / 0 失败)
>
> | # | 内容 | 提交 |
> |---|---|---|
> | 审计 | **只读审计**(假开关 / 死代码 / UI 规范 / 文档漂移四张清单,每条带 `文件:行号`) | 无提交(产出清单) |
> | W25 | **设置页的「打断模式」真接线** —— 它此前是个**假开关** | `e4c6c7c`(`cfdc094`) |
> | W26 | **`stream_chunk` 不再静默丢正文** + **错误横幅可关闭** + **分享不再自相打断** + 两处死代码 | `b7ac6b8`(`eacc65a`) |
> | W27 | 删只被测试养的 `availablePages` + **图片 base64 上限公式去重** + `03` 四处文档更正 | `b7ac6b8`(`805d8b5`) |
> | — | `WsEvent.SessionInterrupted.dropped` 的过时 KDoc 更正 | `85a922c` |
>
> ### 三个「假开关 / 静默」的真相(这批最值钱的东西)
>
> 1. **设置页的「她正忙时按发送的行为」是个假开关**:`InterruptMode` 能读能写能显示,
> **全仓没有任何发送路径读它**(连 `ChatSocket.sendInterrupt` 都是零调用方的死 API)。
> 用户切成「打断」,行为与「追加队列」一模一样。⇒ 现已接上:默认档**零行为变化**,
> 「打断」档且会话正忙时先发 `interrupt{scope:"current"}`(**保留**待处理队列)。
> 判据抽成纯函数 `interruptScopeForSend(mode, busy)`,8 条测试钉住两档 × 忙闲穷举。
> 2. **`stream_chunk` 被静默丢掉**:契约明写它是**非流式回退路径**(「未映射/无法确定的事件」),
> 而 `onEvent` 的兜底是 `else -> Unit` ⇒ **只要服务端走那条路,整段回复一个字都不显示、且没有任何信号**。
> 现已把它当 `delta` 喂进同一个流式解析器。(它与结构化帧**互斥**,依据是契约那句「回退路径」。)
> 3. **分享多图会自相打断**:W25 接线后暴露 —— 分享 N 张图 = N 次投递,第 2 张起各发一条 `interrupt`
> 把前一张刚起的回合取消掉。⇒ 分享路径一律 `allowInterrupt = false`(批量动作自相残杀一定是错的)。
>
> ### 两条「查完发现不是 bug」的(**别再返工**)
>
> 1. **「删除后抽屉里的最后消息预览不刷新」——不是 bug**(W19 留下的待办)。
> 证据两面:① **服务端实时重算** —— 我用 REST 实测:删掉某会话最后一条 assistant 消息后,
> `GET /sessions` 的 `lastMessage` **立刻**退回前一条 user 消息;② **客户端每次打开抽屉都会
> `refresh(silent = true)`**(`ChatHomeScreen` 的 `onOpenDrawer`)⇒ 用户看到的是新预览。
> 唯一剩下的极端场景是「抽屉**正开着**时别的端删了消息」——那属于「抽屉不订阅 `message_deleted`」,
> 是**另一个**(更小的)话题,别和这条混起来。
> 2. **`session_interrupted{dropped}` 的「已打断,丢弃 N 条」在本端不可达**:契约 v0.56 明文
> `scope:"current"` 时 `dropped` 恒为 0,而本端**从不发 `scope:"all"`**(见上 W25)。
> ⇒ 只更正了文档与 KDoc(`03` §4 与 `WsEvent.kt`),**没有**去实现一个用不上的计数。
>
> ### 装机验证(有设备证据的)
>
> - **错误横幅的关闭叉:验通**。造错手法:**撤掉 `adb reverse` 后重开 App**(老教训:`--remove`
> 断不开已建立的连接,得让 App 重开会话)⇒ `ConnectException` ⇒ 抽屉底部出现红字「无法连接到服务器」
> + 右侧 ✕(`app/build/e2e-shots/x2-drawer-error.png`);点 ✕ 后那条消失、
> 而「网络异常,显示 刚刚 的结果」**正确保留**(`x3-dismissed.png`)—— 两件事本来就是两个信号。
> - **`stream_chunk` 的端到端仍未验**(要服务端真走进回退路径;本端没法自己造),
> 收益侧是「与 `MessageDelta` 逐字同路」,风险侧只有契约那两句话作依据。
> - **W25 的打断帧未在真机上看过发出**(要「偏好切成打断 + 会话正忙 + 发一条」三条同时满足;
> 后端在另一个会话手里,我没有主动去占它的回合)。逻辑侧由 8 条纯函数测试钉住。
> - **登录页的输入框是否被 IME 遮挡:未验**。审计把它标成「存疑」(字面违反「输入框在 `bottomBar` +
> `imePadding()`」这条约定,但 `LoginScreen` 有 `verticalScroll` + Manifest 是 `adjustResize`,
> `TextField` 通常自己会把光标带进可视区)。**要验就得清空 App 数据回到登录页**——
> 风险是万一登不回去,今晚所有装机验证能力一起没了,所以我**没做**,如实留在这里。
> 判断:**风险低但不是零**;主人下次在干净设备上装一次就能一眼定论。
>
> ### 本批**没做**的(含一个我刻意不做的)
>
> - **连接所有权(共享 socket + 按会话路由)**:**没有动手**。它是架构级改动,
> 动到四条 VM 的帧路由,而今晚模拟器状态不稳、后端在另一个会话手里 ——
> 一旦引入回归,我**未必能当场发现**(这比不做更糟)。已写设计要点与验收清单(见本节末)。
> - **`DocumentDelivery` 里那组「正文转文件」的判决 API**(`plan` / `textAsFile` / `textNeedsRest`
> + `AsDocuments.textAsDocument` 字段)**没有删**,也**没有接线**:所有权归属有**口径冲突**——
> `05` 的三条定案写着「**不做**长正文自动转文件」(理由:`POST /chat` 没有正文长度上限,长正文
> 已经走 REST 了),而主人后来在 G 条里点名「**超限走文件也要做**」。两者多半指的不是同一件事
> (前者 = 用户**在 App 里打的字**自动变附件;后者 = 从别的 App **分享**进来的超长内容走文件),
> 但我**不替主人合并这两个口径**。已登记进 `02` 请他拍板。
> - 另有一条 W26 留下的边界记在这里备查:**回退路径没有 `message.start`** ⇒
> `MessageStart(index==0)` 里那句 `freshTurnIfFinished()` 在回退轮次不触发;日常看不见问题
> (落库/`stream_end` 各自会收尾),只有「上一轮被取消且没落库 + 紧接着来一轮回退流」才会让
> 上一轮的思维链/工具卡滞留。加它属于行为增强,当时按「只加消费分支」停手了。
>
> ### 连接所有权的设计要点(**下一批做它之前先读这段**)
>
> - 现状:`ChatViewModel`(**每个会话一个 VM**)、`SessionsViewModel`、`AdminViewModel` **各建一条**
> `ChatSocket`,而 chat 的 VM 活在 Activity 级 `ViewModelStore` 里 ⇒ **访问过的每个会话都永久留一条
> 连接**(各自带 `OkHttpClient` + 连接池 + 重连循环),删会话也不回收。
> - 自然解法:**共享一条 socket + 按 `sessionId` 路由帧**(`WsEvent.isForSession` 已经是现成的判据)。
> - 验收清单(缺一不可):① 主会话流式正常;② 切到次级会话时**不串台**(旧连接收到的别的会话帧必须被丢);
> ③ 管理页三帧(metrics/trace/log)仍到;④ 抽屉未读角标仍对;⑤ 断线重连后 `after` 补洞仍跑;
> ⑥ 登出时要断开且重登要接上(现有 `authManager.state` 那条 `connect()/close()` 逻辑要跟着搬);
> ⑦ 删会话后那条连接不再留着。