docs(05): 锚点补记十六——W45 把探测换成直接试 REST(判据/两个超时/被证伪的那条担心/删除与测试承接),并把本文件里所有关于探测的描述显式标为作废

This commit is contained in:
AskaEth committed 2026-10-05 18:34:56 +08:00
1 parent 1d548ddcda
commit b18d864145
1 file changed
+59
+59
View File
@@ -1106,3 +1106,62 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \
> - 有 `-->` 后跟 `HTTP FAILED: java.io.InterruptedIOException: timeout` ⇒ **(乙)**:2.5 秒整通上限被连接吃掉。(**不许**为此放宽 fail-closed;要不要让探测的连接预算与链路对齐,是主人裁决。)
> - 有 `-->` 后跟 `HTTP FAILED: java.net.ConnectException` ⇒ 对端根本没在听(`adb reverse` 被冲掉 / 代理没起)⇒ **这不是 App 的 bug**。
> 3. 同时确认**发完消息之后**代理日志里还有没有新的 `BLOCKED /ws/chat`:没有就说明路已经断了(⇒ 第 2 条的第三种),那一轮阶梯其实从没在「HTTP 通」的条件下被考过 —— W40 那一轮就是用 `adb kill-server`/`adb root` 造「真断线」的,这两件都会把 `adb reverse` 一起冲掉(见本文件 W39/W40 两处踩坑记录)。
> ## ★★ 锚点补记十六(2026-10-06 · W45 把「先探 health」换成「直接试 REST」)
>
> ### 状态
> `android` = `origin/android` = **`1d548dd`**(W45 合并提交;分支提交 `ec8d54f`);主树 **1153 测试 / 0 失败 / 0 错误**。W45 worktree 已清。
>
> ### ⚠️ 先说作废:本文件里关于「连通性探测」的描述**全部不再成立**
> 下面这些句子是**历史记录**,保留但**已作废**,别再照着做:
> - **补记十三(W41)**:三档路由里「`Offline` 连探测都不做」「未就绪 + 探测通 ⇒ Rest」、`探测 2500 ms(callTimeout)+ 回落 REST 10000 ms`、无网上界那段「0 次探测 / 一次 REST 都不会发」——**探测这套机制已被删除**。
> - **补记十三**里「fake-backend 补上 `GET /api/v1/health`(否则探测吃 404)」——理由随探测消失(那条路由仍在假后端里,属无害的开发夹具,未清)。
> - **补记十五·补 + W44 那一整段**:`probeReachability` / `createProbe` / `ProbeClientTest` / `sendRouteFor(..., reachability = …)` 以及「logcat 看 `--> GET /api/v1/health`」的判据**全部作废**。
> **保留的结论只有一条**:那个 `Unreachable` 生在**连接之前**——正是它逼出了这次替换。
>
> ### W45 做了什么(设计替换,不是修 bug)
> **判据**:`sendRouteFor(wsDispatched, connection)`(`ui/chat/SendFallback.kt:112`)——
> 1. `wsDispatched` ⇒ `Ws`(成功路径逐字不变)
> 2. `connection == Offline` ⇒ `Queue`,**一个请求都不发**(保留 W41 的理由:该档是「握手 401 且续期也失败」,试了只会拿 401 并落到回滚,比「已排队不会丢」更差)
> 3. 其余三档 ⇒ **`Rest`(直接试)**
>
> **结果分档**:`restAttemptLanding(attempt)`(`:206`)四档 —— `null`(整通上限到点)⇒ `Unsent` 进队列(fail-closed);`success` 且 `isAccepted()` ⇒ `Accepted`(**不进队列**);`success` 但被拒 ⇒ `Refused`(回滚 + 既有文案);`failure` ⇒ 交给 `restFailureLanding`(网络族/映射不出 ⇒ `Unsent`;Server/Unauthorized/配置族 ⇒ `Errored` 回滚)。
> **判据里已没有任何 `Reachability` 输入**(签名本身就是这条事实)。
>
> ### 两个超时(**与任务书的措辞有出入,取舍已写进 KDoc**)
> - **连接超时 2.5 秒**挂在**那个共用 client** 上(`data/api/ApiClientFactory.kt:39`):短到「无网快速进队列」,又不会连累长文本/图片/文档下载。
> - **整通上限 10 秒仍在调用点**(`REST_FALLBACK_TIMEOUT_MILLIS` + `withTimeoutOrNull`,`ChatViewModel.kt:1662`)。
> **为什么没做成 client 级 `callTimeout`**:这个 client 是共用的,client 级 `callTimeout` 会把长文本 / 内联图片那两条(本轮不碰的路)一并框住,还连累 `DocumentsApi` 的字节下载 ⇒ 等于给它们引入新失败面。而「无网快速进队列」**只靠连接超时就保住了**。
> - **无网上界:完全无网 ≤ ~2.5 秒进队列**(与 W41 持平,没有变长)。
>
> ### 一条被证伪的担心(记下来免得再犯)
> 我曾担心「`stream:true` 的回执是内容流,所以要先关掉 `stream` 才能判『收下了』」。W45 核实后**不成立**:
> `POST /api/v1/chat` 的 `stream:true` 回执是**小 JSON**(`chat_stream_dispatch.go:201` 的 `writeChatStreamAck`:`200 {stream:true}` / `202 {queued:true}` / `200 {duplicate:true}` / `409` / `503`),回合跑在自己派生的 goroutine 上,回执写完即返回 ⇒ **读完这段 body 就有 `ChatAck`**,不等整轮生成、不用改契约。
>
> ### 删了什么 / 测试怎么承接
> **删除**(删前 grep 全仓确认无其它调用方):`data/repository/ConnectivityRepository.kt`、`data/api/HealthApi.kt`、`data/api/dto/HealthDto.kt`、`ProbeClientTest.kt`、`ApiClientFactory.createProbe` 与 `PROBE_TIMEOUT_MILLIS`、`AppContainer` 的 `healthApi`/`connectivityRepository` 接线。
> **新增** `RestAttemptClientTest`(6 例,真 OkHttp + 真 Retrofit + JDK HttpServer)**承接仍然成立的事实**:请求真的写出去(现在是 `POST /api/v1/chat`)、请求体带 `clientMsgId`/`stream:true`、**这次带令牌**(`Authorization` + `X-API-Version`,与探针相反)、三种「收下了」的回执 ⇒ `Accepted`、`409` ⇒ `Refused`、连不上 ⇒ 快速失败且 `Unsent`。
> **丢掉的四条**(钉的都是已不存在的机制):`GET /api/v1/health` 落到线上、探针不带令牌、`200 但 data 空 ⇒ Unreachable`、探针 `callTimeout` 处失败。**fail-closed 这条事实没有丢**(由「整通上限到点」与「异常映射不出」两条承接)。
> 唯一略降级:连接超时的**数值**现在由纯断言钉(2s ≤ x ≤ 3s),运行时那侧只用「没人听的端口 ⇒ 很快失败 ⇒ 队列」兜住。
>
> ### 可观测性(下一轮设备验证就靠它)
> **主 REST client 的 OkHttp `BASIC` 日志本来就开着**(`ApiClientFactory.kt:88-94`,DEBUG 下所有 `create` 都挂),**不需要改动**:
> ```
> --> POST http://<host>/api/v1/chat (NNN-byte body)
> <-- 200 OK http://<host>/api/v1/chat (12ms, 41-byte body)
> ```
> 失败时第二行是 `<-- HTTP FAILED: java.net.ConnectException…`(或被短连接超时掐断的 `SocketTimeoutException: connect timed out`)。
> `-->` 打在 `chain.proceed()` **之前**(连接都还没开始建)⇒ **「这条请求有没有发出去」与「死在连接还是死在回执」当场可分**。
> `BASIC` **不打印任何请求头**(`HEADERS`/`BODY` 才会;`redactHeader("Authorization")` 是双保险),级别没有上抬。
> 注:**WS 那条连接(`ChatSocket`)没挂日志**,所以 `-s OkHttp` 里出现的都是 REST 调用——正好是下一轮要的判据。
>
> ### ⚠️ 一条事故记录(已核实无害)
> W45 子代理的**文件工具把相对路径解析到了主仓库**(不是它的 worktree),最初三次编辑落在主仓的 `AppContainer.kt` / `ApiClientFactory.kt` / `SendFallback.kt` 上。它自己发现后用 `git checkout --` 还原,并用绝对路径重做。
> **我亲自核过**:主仓 `git status` 只有 `devdocs/02`(后端会话的在途追加)、`HEAD` 仍是当时的 `2bee0b5`、那三个文件 `git diff HEAD` **零差异** ⇒ **确实还原干净**。
> **教训**:给子代理的路径一律写绝对路径,且收工后**必查主仓 `git status`**。
>
> ### 待办(更新)
> 1. 🔴 **设备验证(下一轮第一件)**:`ws_block_proxy.py` + `adb reverse tcp:10942 tcp:18098`(WS 断、HTTP 通),发一条纯文本 ⇒ 用 **`adb logcat -s OkHttp`** 确认 `--> POST /api/v1/chat` 与 `<-- 200`,并确认**库里落库、没进队列**。**先 `dumpsys window | grep mCurrentFocus` 确认前台是我们的 App**(补记十五·补的教训)。
> 2. `devtools/fake-backend` 里的 `GET /api/v1/health` 路由与 README 的说法现在是死的(无害夹具,未清)。
> 3. 待主人过目:静默持续到某次补发成功 vs 每轮结束恢复告知(改一行)。
> 4. 待办:子会话流程跑一遍出清单;W42 多选器真机观感。