From 541a70f1269ab06ab38aaf43f78f25abcd4e49f5 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 7 Oct 2026 11:19:21 +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=9B=9B=E5=8D=81=20=E2=80=94=E2=80=94=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=9B=9E=E6=89=A7=E4=BA=8C=E5=8D=81=E5=85=AB=E5=9B=9B?= =?UTF-8?q?=E6=9D=A1=E7=AD=94=E6=A1=88=20+=20=E7=95=B8=E5=BD=A2=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E9=80=9A=E8=BF=87=EF=BC=88=E9=83=A8=E7=BD=B2=E5=90=8E?= =?UTF-8?q?=E6=97=A0=E6=96=B0=E7=95=B8=E5=BD=A2=EF=BC=89+=20W60=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=87=8D=E5=90=AF=E8=87=AA=E6=84=88=EF=BC=88?= =?UTF-8?q?=E6=A0=B9=E5=9B=A0=E4=B8=A4=E5=8D=8A=E3=80=81=E6=8B=89=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E6=96=B0=E6=B6=88=E6=81=AF=E7=9A=84=E7=9C=9F=E7=9B=B8?= =?UTF-8?q?=EF=BC=89+=20W61=20=E5=88=86=E4=BA=AB=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=EF=BC=88=E5=BD=93=E5=89=8D=20HEAD=20=E6=97=A0=E6=AE=8B?= =?UTF-8?q?=E7=95=99=E5=B4=A9=E6=BA=83=E7=82=B9=EF=BC=8C=E4=B8=BB=E4=BA=BA?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E6=98=AF=E6=97=A7=E5=8C=85=EF=BC=9B=E4=B8=89?= =?UTF-8?q?=E5=A4=84=E5=A6=82=E5=AE=9E=E7=99=BB=E8=AE=B0=E7=9A=84=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3=EF=BC=89+=20W62=20=E8=AF=AD=E9=9F=B3=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E4=BE=A7=E7=9C=9F=E6=9C=BA=E9=AA=8C=E6=94=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87=EF=BC=88=E4=BA=94=E6=A1=A3=E5=A4=B9=E5=85=B7=20+=20?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=87=BA=E5=A3=B0=EF=BC=89+=20=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E6=B5=81=E7=A8=8B=E6=95=99=E8=AE=AD=EF=BC=88=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E2=89=A0=E8=AE=BE=E5=A4=87=E4=B8=8A=E6=98=AF=E6=96=B0?= =?UTF-8?q?=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devdocs/05-开发进度与交接.md | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index 871ba9d..1eec39e 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -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. 空/边界输入:空串/空白/`<`/`

`/半个代理对/`\u0000`/10 万字符 —— **全不抛**。 +3. 面板渲染:面板里**只有两个 `verticalScroll`**,各自前面都有一层**有限** `heightIn`;**反编译 miuix-ui-0.9.3 全部 545 个 class**,`WindowDialog`/`DialogLayout`/`TextField` **一个 scroll 节点都没有** ⇒ 「无界高度 + 滚动」在面板里不可能成立。 +4. 出队闸门与并发:闸在 `take()` **之前**,认领落进 VM ⇒ 重建不会再取。 +5. 发送后渲染:含 `

`/``/星号时按既有口径走;新增 **12 条跨层串测**(`ShareTextPipelineTest.kt`)——**此前跨层接缝完全空缺,而事故恰恰出在接缝上**。 +6. 通知/服务:与 `ProactiveFrameIntake`/`KeepAliveService` 无任何调用关系。 + +**顺带加固一处**:`shareConfirmWindowCap` 加 `takeIf { value.isFinite() }` —— 把 W58 崩过两次的那条不变量从「靠调用方好习惯」变成**由唯一出口自己保证**(今天不可触发,是防御性收紧)。 + +**三处如实登记、未改的缺口**(都留给主人定): +- ⚠️ **`domain/parse` 有一处 O(N²)**:每个落单 `

` 都调 `hasContentAfter` 扫到串尾 ⇒ 实测 1000→189ms、4000→1107ms、**16000 个 → 17.9 秒**(**跑在主线程**的 `MessageBubble` `remember` 里)。触发需要「上千个落单 `

`」的文本(粘贴 HTML 残骸可能),网易云不是这个形状。修它要动 `domain/parse` 既有口径。 +- ⚠️ **只有 `EXTRA_SUBJECT`/`EXTRA_TITLE` 而 `EXTRA_TEXT` 为空的分享会静默**(「点分享什么都没发生」)。**可达性未证实**,故只报不改。 +- ⚠️ **面板预览不走解析器、气泡走** ⇒ `"

正文

"` 预览里是一串标签、气泡里只剩「正文」(不一致,改它会动已定稿的预览口径)。 + +### 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 + 缓存到 `/media/`、原子落盘、**刻意不落 `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 进程**仍在跑**(带语音夹具;在后台任务里,到点会被系统收)。