From 97992bdb6ba8bce732587eb403bb730f3ada1627 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 5 Oct 2026 14:35:14 +0800 Subject: [PATCH] =?UTF-8?q?docs(05):=20=E9=94=9A=E7=82=B9=E8=A1=A5?= =?UTF-8?q?=E8=AE=B0=E5=8D=81=E4=B8=89=E2=80=94=E2=80=94W41=20=E4=B8=89?= =?UTF-8?q?=E7=BA=A7=E9=98=B6=E6=A2=AF=EF=BC=88=E6=9C=AA=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=85=88=E6=8E=A2=20health=EF=BC=8C=E9=80=9A=E5=B0=B1=E5=9B=9E?= =?UTF-8?q?=E8=90=BD=20REST=20=E5=8F=91=E5=87=BA=E5=8E=BB=E3=80=81?= =?UTF-8?q?=E4=B8=8D=E9=80=9A=E6=89=8D=E8=BF=9B=E9=98=9F=E5=88=97=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E5=90=AB=20Offline=20=E4=B8=BA=E4=BD=95=E4=B8=8D?= =?UTF-8?q?=E6=8E=A2=E6=B5=8B=E3=80=81gapCursor=20=E9=82=A3=E4=B8=AA?= =?UTF-8?q?=E8=A2=AB=E8=A1=A5=E6=8E=89=E7=9A=84=E6=B4=9E=E3=80=81=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E5=9B=BE/=E9=99=84=E4=BB=B6=E8=B5=B0=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E8=BF=99=E6=9D=A1=E8=B7=AF=E7=9A=84=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devdocs/05-开发进度与交接.md | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index de91f14..c10fff2 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -945,3 +945,45 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \ > | 发送按钮 | **(950, 2170)** | > | 抽屉(左上菜单)| 按 1080 宽重算,约为 **(86, 188)**(未实测,下次用前先截图确认) | > 注意:**软键盘仍是禁用状态**(`ime disable` 写在 userdata 里,跨重启有效),所以布局不会因 IME 位移;这是刻意的(见补记八第 1 条坑)。 + +> ## ★★ 锚点补记十三(2026-10-06 凌晨 · W41 三级阶梯:未连接时**先探 health,通就发出去**) +> +> ### 状态 +> `android` = `origin/android` = **`eb22521`**(W41 合并提交;分支提交 `9f77fb4`);主树 **1135 测试 / 0 失败 / 0 错误**。W41 worktree 已清。 +> ⚠️ **本批只有编译 + 纯判据单测证据,没有真机回归**(子代理按任务书禁碰 adb)。 +> +> ### 主人给的思路(比原来那版好) +> 我原先只做了「未连接 ⇒ 进队列」。主人指出:**「未连接状态你就不能拿 httpAPI 尝试吗?」→「可以走 health 端点验证连通性」**。 +> 关键理由(我原来那份评估的漏洞):我曾担心「REST 分不清有没有送达,所以不敢重发」—— +> 而这条**已被 `clientMsgId` 幂等解掉**(服务端按 `(session_id, client_msg_id)` 唯一索引仲裁,重复到达只回 `chat_duplicate`)。**既然重发安全,就没有理由不试。** +> +> ### 三级阶梯(`ui/chat/SendFallback.kt` 的 `sendRouteFor`) +> 1. **WS 已发出** ⇒ `Ws`(连接状态与探测都不再参与;**成功路径零改动**) +> 2. **`WsState.Offline`** ⇒ `Queue`,**连探测都不做**。理由:该档语义是「握手 401 且续期也失败」,会话已知是死的;而 `/api/v1/health` **不鉴权**(`routes.go:51`、`rest.go:219-221` 只回 `{"status":"ok"}`)⇒ 探它必然 200 ⇒ 会发一次注定 401 的 REST,把「已排队不会丢」换成一次回滚 + 一句误导人的鉴权错误。 +> 3. **未就绪(Idle/Connecting/Reconnecting)+ 探测通** ⇒ **`Rest`**:回落 `POST /api/v1/chat` 把消息**当场发出去**,不进队列。 +> 4. **其余一切**(不通 / 超时 / 抛异常 / **还没探过**)⇒ `Queue`(**fail-closed**,`null` 与「不通」同路)。 +> +> **超时**:探测 **2500 ms**(`callTimeout` 覆盖 DNS+连接+写+读);回落 REST 另加 **10000 ms** 上限。 +> **无网时的上界**:WS 就绪 = 0 次探测;`Offline` = 0(不探);完全无网 ⇒ **≤ ~2.5 秒**进队列且**一次 REST 都不会发**;探测通但 REST 卡住 ⇒ **≤ ~12.5 秒**后进队列(气泡全程「发送中…」,不是无响应)。 +> +> ### ✅ 它顺手补掉了一个真洞(值得记) +> 「回落 REST 之后回复怎么回来」要求核实 —— 结论是**能回来**(`catchUp` → `loadAfter` → `absorb`,`ChatViewModel.kt:923/924-932/2091/2106` + WS 握手 `history` 兜底), +> 但发现 **`gapCursor` 只在「离开 Connected」那一刻记**:若本 VM **从没连上过**(进会话时就是断的)它为 `null`,重连那一步**什么都不会拉**,而回落那一轮的回复恰好只能靠它。 +> ⇒ 新增 `armGapCursorForFallback`(`:1614-1615`),只在回落被受理后、且游标还是 `null` 时补记一次(**绝不改写掉线时记下的起点**)。 +> ⚠️ **仍缺的一段(它如实写了,没假装补上)**:本端一条消息都没有时(`newestKnownAt == null`)游标补不上 —— `after=0` 会从**最旧**方向拉,20 轮上限停在最旧 1000 条上,正好错过要看的那条。主会话由 `history` 的 50 条兜住;**次级会话 + 首屏为空 + WS 从未连上过**这个组合下要用户自己刷新。 +> +> ### 边界(它核实后写的,别记错) +> - **有图 / 有附件的消息走不到这条阶梯**:`ChatSendRouting.channelFor` 对「有图或有附件」恒回 `Rest`,而只有 WS 那一支才进阶梯 ⇒ 进阶梯的**必然是纯文本**。 +> ⇒ 不存在「本地 URI 授权失效」「图片重编码」被这条新路踩到的可能;有图/有附件的行为与 W40 之前**逐字相同**。 +> - **REST 失败分档**:`null`(超时/映射不出)与网络族(NoNetwork/Dns/Timeout/ConnectFailed)⇒ **进队列**;Server/Unauthorized 与配置族 ⇒ 仍回滚 + 文案(服务端给过裁决、等下去不会变好)。 +> - **队列仍不持久化**(进程被杀就丢)——W40/W41 一致,未改。 +> - ⚠️ **「任何 REST 失败都进队列」没做**(主人若指这个,需先设计「补发时怎么安全重放图片/附件」):本次只覆盖**阶梯第 2 步那次回落 REST**。 +> +> ### 顺带改了两处(任务书外,有 KDoc/commit 说明) +> `devtools/fake-backend` 补上 `GET /api/v1/health`(否则本机跑假后端时探测吃 404、阶梯退化成直接排队);补发由 W40 的「同步无挂起点」改成**单飞协程 + 串行出队**(阶梯要挂起,必然)。 +> +> ### 待办(本节的) +> 1. **W41 的真机回归**:用假后端 `--hold-replies` + **断 WS 但留着 HTTP**,看是否真走 REST 出去、重连后回复是否补齐。 +> 2. 「任何 REST 失败都进队列」若要做,先设计图片/附件的安全重放。 +> 3. `gapCursor == null` 且本端为空那条残留洞:可加「重连时 `loadLatest` 一次」兜底。 +> 4. **W42(图片按钮改多选)已派单**(`feat/W42-image-multipick`,基线已追到 `eb22521`)。