From 219c50b7c27546b6b7d3a470fb6aef7a1a1a991e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 4 Oct 2026 22:40:43 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=87=AA=E4=B8=BB=E6=8E=A8=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=89=B9=E7=9A=84=E7=99=BB=E8=AE=B0=EF=BC=88=E5=81=87?= =?UTF-8?q?=E5=BC=80=E5=85=B3=20/=20=E9=9D=99=E9=BB=98=E4=B8=A2=E6=AD=A3?= =?UTF-8?q?=E6=96=87=20/=20=E5=88=86=E4=BA=AB=E8=87=AA=E6=89=93=E6=96=AD?= =?UTF-8?q?=20=E5=85=A8=E4=BF=AE=E5=AE=8C=20+=20=E4=B8=80=E6=9D=A1?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E5=86=B2=E7=AA=81=E8=A6=81=E4=B8=BB=E4=BA=BA?= =?UTF-8?q?=E6=8B=8D=E6=9D=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主人睡前说「按主线继续推,或者先按模块检查」,我把两件都做了:先做了一次只读自审 (假开关 / 死代码 / 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 原样留着。 本次提交只动文档,无代码改动。 --- devdocs/02-前后端协同事项.md | 58 +++++++++++++++++++++++++ devdocs/05-开发进度与交接.md | 83 +++++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/devdocs/02-前后端协同事项.md b/devdocs/02-前后端协同事项.md index 38b2b86..204550c 100644 --- a/devdocs/02-前后端协同事项.md +++ b/devdocs/02-前后端协同事项.md @@ -1391,3 +1391,61 @@ v0.61 让**扫描件/图片型 PDF** 落到 `status:"unreadable"`,而**原因 ### 四、主人侧一句 主人这两天提的「分享到 App」那条路的**附件部分**已经走通(选文件 / 待发预览 / 上传 / 落库 / 气泡芯片 / 预览弹层),**IM 渠道那条路按你们 §三 的结论仍算不通**,我们在登记里保持「未验证」。 + +--- + +## 登记(客户端会话,2026-10-04 深夜续 · 一条请你们确认 + 一条告知 + 一条要主人拍板) + +> 这一批是客户端做了一次**只读自审**(假开关 / 死代码 / UI 规范 / 文档漂移)之后清出来的问题, +> 绝大多数是**我方自己的**,不需要你们动作。下面只挑**与你们有关**的三条。 + +### 一、⚠️ 请你们确认:`stream_chunk` 今天还会不会发? + +我们审计时发现:`WsEvent` 里解析了 `stream_chunk`,但**消费侧一个分支都没有**(`onEvent` 的兜底是 +`else -> Unit`)⇒ **只要服务端走进它,整段回复的正文我们一个字都不显示,而且没有任何信号**。 + +我们已经按契约把它接上了(当作 `delta` 喂进同一个流式解析器)。**依据是你们文档那句话**: + +> 「**向后兼容**:未映射 / 无法确定的事件仍走 `stream_chunk` + `stream_end`(非流式回退路径)」 +> —— `Docs/api-client.md` §3.1 附近 + +**要确认的三点**(如果是我误读了文档,请直接说): + +1. 该帧**今天仍在产**吗?(还是已经名存实亡 —— v0.50 之后结构化帧补齐了,回退路径可能已经没人走) +2. 它的 `delta` 是**增量**(我们按增量追加)还是**整段**(那我们就是在重复拼接,得改成覆盖)? +3. 它会不会**与结构化帧并存**?我们按「互斥」实现(回退路径的语义就是「结构化帧没能承载这次事件」)。 + 若在某些条件下两者会同时到,请给一句判据,我们好去重。 + +你们回一句就行;若答案是「已经不发」,我们把这半条分支删掉,也就少一条会漂移的代码。 + +### 二、告知:客户端**开始发** `interrupt` 帧了(契约 v0.56 的 `scope` 首次被我们用上) + +- 触发条件(**窄**):设置页选「打断」**且**该会话 **`busyMode != null`**(有回合在跑)时按发送。 + 发的是 **`{sessionId, scope:"current"}`** —— 只取消在跑回合、**保留待处理队列**。 +- **默认档(「追加队列」)一个 `interrupt` 帧都不发**,线上请求与之前**逐字节相同**。 +- **多图分享不打断**:分享是「一次把东西倒进去」,对每张图各发一条会互相取消(第 2 张把第 1 张刚起的 + 回合打掉),所以分享路径一律不发。批量动作自相残杀是客户端侧的判断,**不需要**你们做任何事。 +- **已知边界(我方,坦承)**:REST 路径是「WS 发打断 + HTTP 发消息」**两条道**,极端时序下打断可能 + 打在这条消息刚起的那一轮上。`scope:"current"` 至少保证待处理队列不被清空,但被取消的那一轮按契约 + 不落库。要谈**强序**得 `interrupt` 有回执帧 —— **我们不会为它发明等待时长**(凭空加延迟换不来保证)。 + 你们若将来打算给 `interrupt` 加回执,请顺带在这里说一声,我们会用。 +- 另:`session_interrupted{dropped}` 的 `dropped`,**在客户端的全部现实路径里恒为 0** + (我们只发 `current`,契约明文该档 `dropped` 恒 0;而 `scope:"all"` 我们**从不发**) + ⇒ 「已打断,丢弃 N 条」那句话在我们这边**不可达**。我们已更正自己的文档与 KDoc,**不要求**你们改任何东西。 + +### 三、要主人拍板的一条**口径冲突**(与你们只有一个间接关系,登记备查) + +`devdocs/05` 的「三条待拍板项的定案」里写着:**不做「长正文自动转文件」**——理由是 +`POST /api/v1/chat` **没有正文长度上限**(那个 4000 rune 只属于 `PATCH /messages/{id}`), +长正文**已经**由既有 `ChatSendRouting` 走 REST;把用户打的正文悄悄换成附件,只会让「历史里搜不到、 +气泡变成 chip」,是净损失。 + +但主人后来在任务书 G 条里点名「**超限走文件也要做**」。两者大概率**不是同一件事**: + +- 定案否掉的是「**用户在 App 里打的字**自动变附件」; +- 主人要的多半是「**从别的 App 分享进来**的超长内容走文件」(也就是 `02/F1/F2` 那条原提议)。 + +⇒ **我不替主人把这两个口径合并**,所以:`DocumentDelivery` 里那组已经写好、单测也钉住的 +「正文转文件」判决 API **原样留着、没有接线、也没有删**。等主人拍板后再动。 +**对你们的影响**只有一条:若最后决定「分享超长文本走文件」,走的就是既有的 +`POST /api/v1/documents` + `chat.documents`,**不需要新端点**,所以你们**现在什么都不用做**。 diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index d8e836d..6cb2aa8 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -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()` 逻辑要跟着搬); +> ⑦ 删会话后那条连接不再留着。