docs(05): 锚点补记四十 —— 后端回执二十八四条答案 + 畸形回归通过(部署后无新畸形)+ W60 后端重启自愈(根因两半、拉不到新消息的真相)+ W61 分享审查(当前 HEAD 无残留崩溃点,主人手机是旧包;三处如实登记的缺口)+ W62 语音接收侧真机验收通过(五档夹具 + 播放出声)+ 一条流程教训(合并≠设备上是新包)
This commit is contained in:
1 parent
3d8bc61912
commit
541a70f126
1 file changed
+70
@@ -2127,3 +2127,73 @@ infinity maximum height constraints, which is disallowed.
|
||||
|
||||
**W59 这一批实际上只有单测证据**,两个独立原因:① 后端 **未部署** ⇒ 真机上 `resultSummary` **恒缺席**,「有值」那档验不了;② 后端**当前没配工具** ⇒ **连工具卡本身都触发不出来**,「缺席时与老版一致」这一半也没有可点的入口。
|
||||
⇒ 等后端部署 + 配了工具,再做真机验收(要造一次真的工具调用)。**在那之前不要把这一批报成「已真机验证」。**
|
||||
|
||||
## ★★ 锚点补记四十 · 后端回执二十八 + W60/W61/W62 三批落地(2026-10-07 中午)
|
||||
|
||||
### 1. 后端回执二十八(回我们的四问)—— 逐条
|
||||
|
||||
1. **`resultSummary` 的 500 是 rune(码点)不是字节** ⇒ **我们不需要宽容裁切**(不会出现半个中文字)。顺带登记一处口径差异:**实时帧 500 rune / 回看面 80 rune**。
|
||||
2. **`8051906` 已部署**(10:37:44 新产物 / PID 630540);同一批还上了 `2c56bc9`(修「智谱无链接结果被当无效丢光」)。他们**纠了我一个前提**:「没配工具」不成立(工具全注册),他们给的解释是「那一轮 `tool_calls=0`、模型自己没调」。
|
||||
- ⚠️ **但我们截图里见过「工具调用失败 · web-search」** ⇒ 工具确实会调、只是**失败**过。`2c56bc9` 已随这次重启带上 ⇒ 值得再试一次搜索。
|
||||
3. **未读面**:没有独立「未读增量端点」,**但消息前向增量 `?after=`(毫秒 `createdAt`)早就在册** ⇒ **我们 W49 那次整表拉取可以直接换掉**(他们明确批了)。
|
||||
4. **语音**:`POST /api/v1/audio/transcribe`(U5,**owner-only + 审计、同步**、base64、32 MiB)**早就有**;**没有**的是「上传音频变成一个带 `voice` 的消息」。还逐条答了 6 个子问(时长上限未定义 / 格式白名单未定义 / `voice_input` 那条转写文本只进 user `content` / 失败两套口径别混用 / 转写同步但回合异步)。
|
||||
|
||||
### 2. 畸形回归:**通过**(拿他们部署后的产物验的)
|
||||
|
||||
拉主会话最近 60 条(覆盖 `10-06 13:52` → `10-07 10:39`,含部署后的消息):**畸形只有 1 条**,`id = b88e55d6…`,`createdAt = 10-06 21:05:11` —— **正是我最初上报的那条历史行**(他们说「历史只有 1 行」,对上了)。⇒ **部署后没有产生新的畸形** ✓
|
||||
|
||||
### 3. W60:后端重启后自愈(`83a2a88` → 合并 `55612a3`)
|
||||
|
||||
**根因(子代理推翻了我的一个前提)**:`allClosed()` **不是孤儿**(`SessionIdle` / `SessionInterrupted` 会调它)—— 真正的缺口是**那两帧在后端重启时永远不会到**。而且这 bug 有**两半**:
|
||||
- **回合绑定**:`TurnBinding.closed` 是唯一收口闸,只由 `turn_end` 类帧驱动;
|
||||
- **流式卡(第二半)**:`turns` 的两个写点**都不碰 `streams`** ⇒「思考中…」「执行 X…」底栏「正在思考…」三条闸**全都不收**。
|
||||
|
||||
**「拉不到新消息」的真相(这条最精彩)**:补拉**跑了**(`reconnectPull` 命中 `Forward`),但**那条永远 `active` 的流式路永远是列表最后一项** ⇒ 「跟随底部」的触发键(`items.lastOrNull()?.key`)**不再变化** ⇒ **新消息进得来,屏幕不跟着滚**。
|
||||
|
||||
**修法**:新增 `ui/chat/ReconnectReset.kt` —— `isReconnect(previous, current, everConnected)`(用**只置不翻的 `everConnected`**,因为 `ws.state` 是 StateFlow,`Connected → Reconnecting → Connected` 的中间跳会被**合流吞掉**);触发「连接重建」时把未收尾的**回合 / 流式卡 / 工具卡**一次性收口(**不删任何内容**,只把 `active/thinkingActive/phase` 收掉);**第一次连接不算重建**。17 条新单测。全量 **115 suites / 1405 tests**。
|
||||
- ⚠️ **设备复现验证还没做**(子代理给了可照抄的假后端步骤)。**别把这一批报成已真机验证。**
|
||||
- 一条如实登记的代价:`busyMode = null` 会让「重建后第一次发送」不再先发 `interrupt`(服务端下次报 `session_busy` 即恢复)。
|
||||
|
||||
### 4. W61:分享路径审查(`22d655b` → 合并 `3d8bc61`)—— **主人报的「网易云分享文本闪退」归因**
|
||||
|
||||
**结论:当前 HEAD 这条路径没找到残留崩溃点** ⇒ 与已知 P0(`6d4f2cd`/`df7b504`:`Dp.Infinity` 当哨兵值 + 展开态没去掉 `verticalScroll`)**完全吻合**,证据指向**主人手机上的包早于那个修复**。
|
||||
|
||||
逐项(6 项全查过,证据都在回报里):
|
||||
1. Intent 多样性:`EXTRA_SUBJECT`/`EXTRA_TITLE` **本端不读**;非 `CharSequence` 的 `EXTRA_TEXT` 由 SDK 的 `getCharSequence` **catch `ClassCastException`** 兜住(不抛);`content://` 在接收那刻就拷进 cache(`runCatching`)。
|
||||
2. 空/边界输入:空串/空白/`<`/`<p><p>`/半个代理对/`\u0000`/10 万字符 —— **全不抛**。
|
||||
3. 面板渲染:面板里**只有两个 `verticalScroll`**,各自前面都有一层**有限** `heightIn`;**反编译 miuix-ui-0.9.3 全部 545 个 class**,`WindowDialog`/`DialogLayout`/`TextField` **一个 scroll 节点都没有** ⇒ 「无界高度 + 滚动」在面板里不可能成立。
|
||||
4. 出队闸门与并发:闸在 `take()` **之前**,认领落进 VM ⇒ 重建不会再取。
|
||||
5. 发送后渲染:含 `<p>`/`<action>`/星号时按既有口径走;新增 **12 条跨层串测**(`ShareTextPipelineTest.kt`)——**此前跨层接缝完全空缺,而事故恰恰出在接缝上**。
|
||||
6. 通知/服务:与 `ProactiveFrameIntake`/`KeepAliveService` 无任何调用关系。
|
||||
|
||||
**顺带加固一处**:`shareConfirmWindowCap` 加 `takeIf { value.isFinite() }` —— 把 W58 崩过两次的那条不变量从「靠调用方好习惯」变成**由唯一出口自己保证**(今天不可触发,是防御性收紧)。
|
||||
|
||||
**三处如实登记、未改的缺口**(都留给主人定):
|
||||
- ⚠️ **`domain/parse` 有一处 O(N²)**:每个落单 `<p>` 都调 `hasContentAfter` 扫到串尾 ⇒ 实测 1000→189ms、4000→1107ms、**16000 个 → 17.9 秒**(**跑在主线程**的 `MessageBubble` `remember` 里)。触发需要「上千个落单 `<p>`」的文本(粘贴 HTML 残骸可能),网易云不是这个形状。修它要动 `domain/parse` 既有口径。
|
||||
- ⚠️ **只有 `EXTRA_SUBJECT`/`EXTRA_TITLE` 而 `EXTRA_TEXT` 为空的分享会静默**(「点分享什么都没发生」)。**可达性未证实**,故只报不改。
|
||||
- ⚠️ **面板预览不走解析器、气泡走** ⇒ `"<p><p>正文</p>"` 预览里是一串标签、气泡里只剩「正文」(不一致,改它会动已定稿的预览口径)。
|
||||
|
||||
### 5. W62:语音消息接收侧(`2fbf24a` → 合并 `9ced744`)—— **已真机验收通过**
|
||||
|
||||
契约三条硬纪律(`Docs/api-client.md` §4.4 / §6):**判据 = `voice` 键存在**(绝不从 `content` 认)、**按 `status` 分支**(`transcribed`/`empty`/`failed`)、**`name`/`mime`/`size`/`url` 四者同生共死**;`durationMs` **只有渠道如实给出时才有(绝不估算)**。🔴 **`voice` 只在 4 个 REST 读取面填充** ⇒ **实时帧上不要等 `voice`**(落库帧在「落库那一刻」发,而 voice 列是**落库之后**回填的)。
|
||||
|
||||
**实现**:`VoiceDto` + `MediaApi`(`@Streaming @GET @Url`)+ `MediaRepository`/`VoiceStore`(Bearer + 缓存到 `<cacheDir>/media/<name>`、原子落盘、**刻意不落 `documents/`** 以免扩大 FileProvider 暴露面)+ `domain/chat/VoiceMessage.kt`(判据唯一落点)+ `VoicePlayback.kt`(纯状态机)+ `VoicePlayer.kt`(**`android.media.MediaPlayer`,零新依赖**)+ `VoiceBubble.kt`。播放音源是**本地缓存文件**(不放网络流,因此不必把 Bearer 塞进播放器)。生命周期三处都收(点另一条 / 换会话 / 组件销毁)。**55 条新单测**(含 `MediaRepositoryTest` 的**线级**用例:真 OkHttp + 真 JDK HttpServer,断言 `Authorization: Bearer` 与 404/416 行为)。
|
||||
|
||||
**真机验收(假后端夹具,`emulator-5554`)**:
|
||||
|
||||
| 夹具 | 屏幕 |
|
||||
|---|---|
|
||||
| `transcribed` + 副本 + 2s | ▶ 语音 0:02 + 转写正文 |
|
||||
| `empty` + 副本 | ▶ 语音 0:02 + 正文(非空不补话) |
|
||||
| `failed` + 副本 | ▶ 语音 0:02 + **服务端那行提示原样** |
|
||||
| **无副本** | 「语音 0:02」,**没有播放键** ✓ |
|
||||
| **无 `durationMs`** | ▶ 语音,**不显示时长** ✓ |
|
||||
|
||||
**播放出声**(logcat 硬证据):`NuPlayerDriver created, clientPid(9122)` → `MediaCodec async mode` → `onAudioDeviceUpdate deviceIds: 2` → `AudioTrackShared: releaseBuffer`,**无 error**;点击后按钮变 `||` ✓。
|
||||
|
||||
**⚠️ 一条流程教训(我自己犯的)**:第一次验收时语音气泡**没出现**,原因是我**只合并推送、忘了重新装包** —— 模拟器上还是 W58 时期的 APK。**「合并」不等于「设备上是新包」**:验收前先确认装的 APK 是哪次构建的。
|
||||
|
||||
### 6. 环境(收工状态)
|
||||
- HEAD = **`3d8bc61`** = `origin/android`;worktree / 分支 / 构建锁**全清**;主仓唯一脏 = `devdocs/02`(后端在途 + 我的登记与四问)。
|
||||
- 模拟器装的**是最新包**(含 W58~W62);App 已重启并连回**真后端**;`adb reverse`:`tcp:80 → 10942`(真后端)、`tcp:10942 → 10942`。
|
||||
- 假后端 18080 进程**仍在跑**(带语音夹具;在后台任务里,到点会被系统收)。
|
||||
Reference in new issue
Block a user