docs(05): 锚点补记十一——W40 普通发送未连接时进队列(主人定案落地),并登记 REST 未覆盖与三条待办

记下这一批的口径与证据,供下一个会话接续:

- 分支 feat/W40-send-queue-offline(基线 e60ae7a),代码与测试提交 df9fc85;未合并、未推送。
- 证据:assembleDebug 成功;testDebugUnitTest 1118 / 0 失败 / 0 错误 / 0 跳过(83 套件)。
  本批没有真机回归。
- 判据:wsSendOutcome 三档(Dispatched / Queued / Failed);补发触发点是连接状态流变成
  Connected 那一刻,无轮询;顺序 = 用户发送先后顺序;只发一次的保证点是队头加出队恰好一次,
  第二道是复用同一个 clientMsgId。
- 状态表达:不新增第七态(等连接仍是 ③ 排队中),气泡小标加成「排队中 · 还没连上」,
  另有一条柔和的 sendQueueNotice 告知(一轮队列只说一次,Offline 那一档说的是
  重新登录 / 检查服务器地址)。
- REST 通道(长文 / 图片 / 附件)本批未覆盖,仍走回滚加既有网络错误文案,理由与影响面写在
  补记里。
- 待办里更新了四条:真机回归、REST 要不要也排队(需主人定案)、Offline 立刻告知、
  以及那一处刻意未动的失败文案字面量。
This commit is contained in:
AskaEth committed 2026-10-05 13:58:20 +08:00
1 parent df9fc85f76
commit 67b2e58fdf
1 file changed
+28
+28
View File
@@ -870,3 +870,31 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \
> 3. `Offline` 立刻告知(不等 3 秒)。
> 4. **W38/W39 的真机回归**:W38 已验(补记九);W39 这三条只在真机上扫一眼即可(触发条件都少见)。
> 5. 旧账:子会话流程(主人搁置)、图片/工具卡实机验证(后端未配工具)、**相册多选→分享**(图片路径仍需真机;App 一个权限都没声明 ⇒ adb 造不出可读 URI,这条**只能**走真机或驱动相册 UI)。
> ## ★★ 锚点补记十一(2026-10-05 · W40:普通发送「未连接 ⇒ 进队列」,不再回滚)
>
> ### 状态
> - worktree 分支 `feat/W40-send-queue-offline`(基线 `e60ae7a`),分支提交 **`df9fc85`**;**未合并、未推送**(主树与 `origin/android` 都没动)。
> - 本批证据:`assembleDebug` 成功;`testDebugUnitTest` **1118 测试 / 0 失败 / 0 错误 / 0 跳过**(83 套件)。**没有真机回归**(这一批只有单测与编译证据)。
> - 净变化:W39 加的 `SendFailureMessageTest`(4 条)**删掉**,其断言改成新事实后并入新的 `SendQueueTest`(21 条)⇒ 1101 − 4 + 21 = 1118。
>
> ### ① 主人的裁决落地:未连接 ⇒ 进队列(上一批的「待办 1」已定案并做完)
> - 新纯函数 `wsSendOutcome(sent, connection)`(`ui/chat/SendQueue.kt`)三档:`Dispatched`(帧交给 socket;**成功路径一个字没动**)/ `Queued`(未就绪四档:Idle / Connecting / Reconnecting / Offline)/ `Failed`(连接就绪却仍发不出去 ⇒ 今天那条 `rollback` + `SEND_FAILED_MESSAGE`)。
> - `deliver` 的 WS 分支**只改「未就绪」这一支**:不回滚、不进 `error` 横幅,改由 `queueForConnection` 留在待发态(`ChatUiState.queuedForConnection`:条目只有键与并行声明,正文仍从气泡上现取)。
> - **触发点**:`onConnectionChanged` 里 `connection == WsState.Connected` 那一格(由 `socket.state` 驱动,**无轮询**)+入队后的竞态兜底。
> - **顺序**:FIFO = 用户发送的先后顺序;补发只看队头。**只发一次**:队头 + 出队恰好一次(`dropQueuedSend`);第二道是补发复用同一个 `clientMsgId`(服务端 `chat_duplicate` 判重)。`flushQueuedSends` 是同步函数(无挂起点),两个触发点不会交错。
> - ⚠️ **已知边界**:打断声明(`interrupt{scope:"current"}`)不随补发重放(补发时在跑的那一轮早已不是用户按发送时看到的那一个);内容仍只在**进程内存**里,一直连不上又杀进程就丢(既有语义)。
>
> ### ② 状态表达:③ 排队中 + 小标「还没连上」,**不新增第七态**
> - `phaseOf(待发气泡)` 的 ③ 判据扩为「收到过入队回执 **或** 本端因未连接留住」;气泡小标由新纯函数 `pendingBubbleFooter` 给:`排队中 · 还没连上`(服务端忙时排队仍是 `排队中`)——挂在气泡上而不是只靠告知,因为告知 8 秒就收走,而这是会持续几十秒的状态。
> - 柔和告知走**新通道** `ChatUiState.sendQueueNotice`(与 `shareNotice` 分开:两条说不同的动作):`SEND_QUEUED_NOTICE = "还没连上,这条排队等着,连上后自动发出"`;`Offline` 那一档换 `SEND_QUEUED_OFFLINE_NOTICE = "现在处于离线,需要重新登录或检查服务器地址;这条还没发出去,已排队不会丢"`(**不提**「连上」「自动发」)。**不刷屏**:一轮「队列从空到空」只说一次。
>
> ### ③ REST 通道(长文 / 图片 / 附件)**未覆盖**,仍是回滚 + 网络错误文案
> 理由:它给不出「未连接」这个信号(只有一次网络错误,请求可能已被服务端收到),且补发要重跑图片重编码与附件上传(本地 URI 授权可能已失效)。影响面仅这一条通道。
>
> ### 待办(更新)
> 1. **W40 的真机回归**:断网发普通消息 ⇒ 应看到「排队中 · 还没连上」+ 柔和告知(**没有**红色横幅、输入栏不退回)⇒ 恢复连接 ⇒ 自动补发且只落库一条。
> 2. REST 通道(长文 / 图片 / 附件)未连接时要不要也排队——需主人定案(代价见 ③)。
> 3. `Offline` 立刻告知(不等 3 秒)——W39 遗留,未动。
> 4. **未纳入新判据的一处字面量**:REST 回执被拒那条路仍是 `rejected.message.ifBlank { "发送失败,请重试" }`,刻意未动。
> 5. 旧账:子会话流程(主人搁置)、图片/工具卡实机验证(后端未配工具)、**相册多选→分享**(只能走真机或驱动相册 UI)。