Files
Cyrene-For-Android/devdocs/07-W50-主动消息-验收与避坑.md
T
AskaEth c905a47d6f docs(05/07): 锚点补记二十七 —— W50 落地并设备验收通过;细节单开 07(证据与避坑)
改了什么:
- 05 新增锚点补记二十七:结论(afee5f6 已推 / 1193 测试真跑 0 失败 / APK 尺寸)、四条硬要求逐条
  对应实现处、红线核对、设备四条判据全绿、环境坑一句话版、真后端那条主动路径是活的(副产品)、
  待办变化(W48/W49 验收环境已就绪;W50 的窄尾巴)。
- 新增 devdocs/07-W50-主动消息-验收与避坑.md:验收判据与逐条证据、完整环境坑(含绕法与命令)、
  复现整套验收的最短路径、随批次修掉的 timestamp 解码缺陷、以及「落库失败档」那条尾巴的收口建议。

为什么单开 07:05 已 1400+ 行,验收的操作细节与避坑属参考材料而非状态;05 只留结论与指针,
下一个我读 05 就能接手,需要动手复现时再翻 07。
2026-10-06 08:11:26 +08:00

81 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# W50「她主动说的话实时到安卓」——验收证据与避坑
> 状态:**W50 已合并并验证**(`android` 分支 `afee5f6`,已推)。本文件是该批次的**证据与操作细节**;
> 状态与待办仍以 `05-开发进度与交接.md` 的锚点补记二十七为准(那里只放结论与指针)。
---
## 1. 验收判据与证据(真机 = 模拟器 `emulator-5554`,假后端)
场景是怎么造出来的:`python devtools/fake-backend/proactive_inject.py --port 18090 --text "主动消息验收一"`
(它按真后端的顺序**先落一行 assistant 历史行、再广播带同一 `id`/`createdAt` 的 `proactive_message` 帧**)。
| 判据 | 操作 | 证据 | 结果 |
|---|---|---|---|
| ① 主会话**实时**出现 | 注入后 5s 取 dump | 广播回执 `delivered=1`;dump 里有 `[78,406][400,472] \| 主动消息验收一`,其下 `发送至 网页` | ✅ |
| ② 次级会话**不**出现 | 抽屉 → 「新会话」 | 该页 dump 里 `grep -c 主动消息验收一` ⇒ **0**(页面显示「还没有消息」) | ✅ |
| ③ 刷新/重启后**仍在且只一条** | ①切回主会话 ②同 `id` 重投一帧 ③`force-stop` 后重启 | ①计数 **1** ②重投后仍 **1**(按 `id` upsert 生效)③重启后 **1** | ✅ |
| ④ 无 `id`(后端落库失败)**不静默丢**、也不假装落库 | `--without-id` 注入 → 再重启 | 注入后计数 **1**(本地气泡);重启后 **0**(历史里没有它),而落库那条仍 **1** | ✅ |
截图(主会话单条助手气泡 + 左下对齐)与 dump 一致,渲染无重叠。判据以 **dump 计数**为准(比截图更硬,也更省上下文)。
## 2. 环境坑(这次花掉的时间都在这,别再重复)
1. **`input text` / `input keyevent` 打出的标点会变全角**(`:`→`:`、`.`→`。`、`/`→`/`)。
原因:模拟器**系统 locale 是 zh-CN**,Gboard 走中文标点;字母数字不受影响。
**绕法(不必改 locale)**:`normalizeBaseUrl`(`app/.../data/api/ApiClientFactory.kt:109`)在缺 scheme 时
会补 `http://` ⇒ 服务器地址栏只填**纯字母** `localhost`,再用 `adb reverse tcp:80 tcp:<后端端口>`
把设备的 80 端口接到后端。**附带好处:切假后端/真后端只要改 reverse,一个字都不用重打。**
2. **`adb reverse tcp:80` 需要 root**:`adb root` 在本 AVD(userdebug)可用,
但它**会重启 adbd ⇒ 所有 `adb reverse` 丢失**,之后必须重新加(包括 `tcp:10942`)。
3. **点太快会 ANR 被系统强杀**:实测 `ANR in top.yeij.cyrene … Waited 5065ms for MotionEvent(… (367.0, 780.0))`
紧跟 `Force finishing activity`。App 冷启动要跑类校验(日志里每条 100~170ms),
**等 25~35 秒再动**;并把三个动画开关置 0(`window_animation_scale` / `transition_animation_scale` / `animator_duration_scale`)。
4. **屏幕会自己黑**(`svc power stayon true` + `screen_off_timeout` 都不一定挡得住):
交互前 `input keyevent 224`,并用 `dumpsys window | grep mCurrentFocus` **确认前台是本 App**——
黑屏时点击会落到桌面(这次白跑过两轮),回到桌面后必须重新 `am start`。
5. **`adb shell … /sdcard/…` 的路径会被 MSYS 改写**(变成 `C:/Program Files/Git/sdcard/…`)⇒ 命令前
`export MSYS_NO_PATHCONV=1`。
6. **Python 输出 GBK 会炸**(`UnicodeEncodeError: 'gbk' codec can't encode character`)⇒
`export PYTHONIOENCODING=utf-8`(`ws_probe.py` 里那段 `reconfigure` 就是同一件事)。
7. **`uiautomator dump` 时好时坏**(`ERROR: null root node returned by UiTestAutomationBridge`):失败就重试;
文字/坐标判据一律优先用 dump(`bounds | text`),截图只在需要看观感时用。
8. **滑动手势会被系统返回手势吃掉**:从屏幕底部附近起滑会直接回桌面。真要滚动就避开底部区域
(例如 `input swipe 540 1500 540 800 300`),别用 `1900 → 700`。
9. 验收结束时的状态:`adb root` 仍是开的;reverse 为 `tcp:80 → tcp:10942` 与 `tcp:10942 → tcp:10942`
(都是真后端);App 拿的是假后端签发的 token ⇒ 会在真后端的 401 上登出,登录页地址栏留着 `localhost`。
## 3. 复现整套验收的最短路径
```bash
export PATH="$PATH:/c/Users/Aska/AppData/Local/Android/Sdk/platform-tools"
export MSYS_NO_PATHCONV=1 PYTHONIOENCODING=utf-8
# 1) 假后端 + 反向端口(80 ⇒ 假后端,地址栏填 localhost 即可)
python devtools/fake-backend/fake_backend.py --port 18090 &
adb root && adb reverse tcp:80 tcp:18090
# 2) App 侧:清数据 → 登录页填 localhost / 任意用户名密码 → 登录(假后端不校验)
adb shell pm clear top.yeij.cyrene
adb shell am start -n top.yeij.cyrene/.app.MainActivity # 等 25~35s 再动
# 3) 注入 + 看判据(四条判据见第 1 节)
python devtools/fake-backend/proactive_inject.py --port 18090 --text "主动消息验收一"
python devtools/fake-backend/proactive_inject.py --port 18090 --text "落库失败验收" --without-id
```
## 4. 这个批次顺带发现的客户端缺陷(已随 W50 修掉)
`proactive_message` 载荷里的 `timestamp` 在服务端是 Go `time.Time` ⇒ 线上是 **RFC3339 字符串**,
而客户端旧声明是 `Long?` ⇒ kotlinx 解码抛异常、被 `WsEvent.parse` 的 `runCatching`
(`app/.../data/ws/WsEvent.kt:654`)**吞成 null** ⇒ 整帧在成形之前就没了。
也就是说:**只加一条 `onEvent` 分支根本碰不到帧**。现在按字面量收(字符串/数字都吃);
该字段全仓零消费点,排序去重一律用 `createdAt`。
## 5. W50 留下的已知窄尾巴(**只在「落库失败」那一档可达**)
降级气泡是一条 `role=assistant` 的 `messages` 行,而删除入口的判据 `canDeleteMessage` 只看 role ⇒
长按能删它,删除会用本地 id(`proactive-local:` 前缀)打服务端并失败报错。
**没修的理由**:要收口得给每条消息加「是否服务端行」的标志位,会动 `ChatDisplayItem` 与气泡渲染,
超出该批次范围。**最省的收口方式**:在 UI 层按 `proactive-local:` 前缀屏蔽动作入口。