From f9555fba85b8a7e84ebf22665fda66bcc9feb848 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 6 Oct 2026 22:23:09 +0800 Subject: [PATCH] =?UTF-8?q?docs(05):=20=E9=94=9A=E7=82=B9=E8=A1=A5?= =?UTF-8?q?=E8=AE=B0=E4=B8=89=E5=8D=81=E5=85=AD=20=E2=80=94=E2=80=94=20W57?= =?UTF-8?q?=20=E9=80=9A=E7=9F=A5=E6=B6=88=E8=B4=B9=E7=82=B9=E6=8F=90?= =?UTF-8?q?=E5=88=B0=E8=BF=9B=E7=A8=8B=E7=BA=A7=EF=BC=88=E6=A0=B9=E5=9B=A0?= =?UTF-8?q?=E9=93=BE=EF=BC=9A=E8=BF=9E=E6=8E=A5=E6=98=AF=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E7=BA=A7/=E8=AE=A2=E9=98=85=E6=8C=82=20Activity=20=E7=BA=A7=20?= =?UTF-8?q?VM/replay=3D0=20=E4=B8=A2=E5=B8=A7=EF=BC=89+=20=E9=95=BF?= =?UTF-8?q?=E8=B7=91=E7=AC=AC3=E6=AC=A1=E5=9F=BA=E7=BA=BF=EF=BC=88?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E7=A1=AE=E8=AE=A4=E6=98=AF=E5=BC=80=E7=9A=84?= =?UTF-8?q?=EF=BC=8C=E6=8E=A8=E7=BF=BB=E4=B8=8A=E8=8A=82=E9=82=A3=E6=9D=A1?= =?UTF-8?q?=E7=8C=9C=E6=83=B3=EF=BC=89+=20W57=20=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E9=AA=8C=E6=94=B6=E6=AD=A5=E9=AA=A4=E4=B8=8E=E5=89=AF=E4=BD=9C?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devdocs/05-开发进度与交接.md | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index 9072b8f..5c8866f 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -1913,3 +1913,50 @@ adb reverse --remove tcp:80 && adb reverse tcp:80 tcp:10942 3. 复查那一刻(及现在)App 进程**根本不在**(`pidof` 空、前台是 launcher、`KeepAliveService` 0 条)——**最可能的解释是保活开关当时没开**(没开 ⇒ 没有前台服务 ⇒ App 在后台待了几十分钟被系统正常回收,这正是「不保活」的预期)。⚠️ **这一条我没有实证**(App 已不在,读不到开关状态),**不要当结论**。 **要重做才有效**:得先把 `tcp:80` 切回假后端 `18090`(`adb reverse --remove tcp:80 && adb reverse tcp:80 tcp:18090`),再在真机/模拟器上开保活开关、建立新基线。⚠️ **但切回假后端会打断主人此刻在模拟器上用真后端看真实会话**,所以**没做,等主人定**。 + +## ★★ 锚点补记三十六 · W57(通知消费点提到进程级)+ 保活长跑第 3 次基线(2026-10-06 22:25) + +### 1. W57:Activity 被划掉后也能弹通知 —— **根因与修法**(`0e03eba` → 合并 `900d191` = `origin/android`) + +**结论先行**:帧**到得了**连接,是**到了没人消费**。改动面因此很小(没搬连接所有权、没新起连接)。 + +根因链(子代理逐条给了行号证据,改前状态): + +1. **连接是进程级的**:`AppContainer.chatSocket`(`AppContainer.kt:282`,`by lazy`)在 `CyreneApplication.onCreate` 建;`ChatSocket.connect()` 把连接循环跑在它自己的 scope 上(`ChatSocket.kt:141,358-359`,`CoroutineScope(SupervisorJob() + Dispatchers.IO)`),**只有 `close()` 会取消它**(`ChatSocket.kt:165-175`)。 +2. **没有 ViewModel 会关它**:`SessionsViewModel.onCleared()` 是空的,KDoc 明写「刻意不关连接」。 +3. ⇒ 划掉 Activity 后连接照旧活着、`onMessage` 照旧 `_events.tryEmit(...)`。 +4. ⚠️ **但 `_events` 是 `MutableSharedFlow(replay = 0, extraBufferCapacity = 512, DROP_OLDEST)`**(`ChatSocket.kt:101-105`)——`replay = 0` 意味着**不为将来的订阅者留任何东西,没有订阅者时 emit 就是当场丢**。 +5. 而 proactive 帧当时的订阅者是 `SessionsViewModel.kt:232` 的 `viewModelScope.launch { socket.events.collect(::onEvent) }`,`viewModelScope` 随 VM `onCleared` 取消,该 VM 由 Activity 的 `ViewModelStore` 持有 ⇒ **划掉 Activity = 订阅被取消 = 帧被丢**。 + +**修法**(`app/ProactiveFrameIntake.kt` 新增): +- 进程级帧消费点:订 `ChatSocket.events`,把两类帧(实时 `proactive_message` / 带 `proactive` 的 `message_persisted`)交给 `ProactiveNotifier`;纯函数 `proactiveFrameOf(event)` 决定「哪些帧算她主动说的话、四个入参各取哪个键」。 +- **构造即订阅,刻意不提供 start/stop**(理由写进 KDoc:唯一会想调 stop 的调用方是 VM,那正好把通路又绑回 Activity;留一个能忘调的 `start()` 只是多一种「通知整个哑掉且不报错」的坏法)。 +- `AppContainer` 新增 `proactiveFrameIntake`;`SessionsViewModel.onEvent` 里那两处转交**删掉**;`ProactiveNotifier` / `AppPresence` **只改 KDoc**(判据、去重、`post` 签名零改动)。 +- 登出**不需要**取消订阅:帧只有连接活着才可能到,而连接在 `LoggedOut` 与令牌失效两处都会断;去重账本刻意不清(换账号后同一条再推也不该弹第二遍)。 + +**验收**:全量真跑 **113 suites / 1370 tests / 0 failures / 0 errors**(基线 112/1361,±0 其它形状变化)。新增 `ProactiveFrameIntakeTest` 9 条,其中 `her words still notify when no activity is alive` 就是本条的现场。 + +**⚠️ 一个需要主人知道的副作用**:`AppContainer` 现在会在 `Application.onCreate` 时**提前把 `chatSocket` 建出来**(改前 `by lazy`,第一次被 VM 取用才建)。代价是进程启动多造一个 `OkHttpClient` 对象(线程池仍懒建)。若不接受,改法是把 intake 的参数换成 `() -> SharedFlow` 保留懒性——**没做**,因为那会让「订阅时刻」与「连接建出时刻」分开,多一处要解释。 +**另一处必要项**:`deliver` 里接住了落地异常(改前异常会打死 VM 那条 collect,但 VM 重建会自愈;现在它是唯一消费点,异常逃出去就是**进程崩溃 + 通知永久哑掉**,所以必须接住,只 DEBUG 打日志)。 + +**设备验收步骤(等长跑结束再做;⚠️ 装包必然杀进程)**: +``` +adb root && adb reverse tcp:80 tcp:18090 # 注意顺序:adb root 会冲掉 reverse,要放在前面 +adb install -r app/build/outputs/apk/debug/app-debug.apk +# 打开 App → localhost 登录 → 授通知权限 → 设置页开「后台保持连接」 +# 进主会话 → Home 退后台 → 最近任务里把卡片划掉(等 2~3s) +pidof top.yeij.cyrene # 记下 PID +dumpsys notification --noredact | grep keep_alive # 保活通知还在 = 进程确实被撑着 +python devtools/fake-backend/proactive_inject.py --port 18090 --text "划掉之后也要弹" +dumpsys notification --noredact | grep 划掉之后 # 期望:有「昔涟 / 划掉之后也要弹」 +pidof top.yeij.cyrene # 期望:与前面同一个 PID(证明是同一进程,不是重启后补弹) +``` +**判据前提**(不满足则本次无效):注入**之前** PID 未变 **且** 保活通知还在;通知权限已授予。 + +### 2. 保活长跑第 3 次(假后端,基线 `22:16:08`) + +主人 2026-10-06 晚授权「允许你切」,于是把 `tcp:80` 切回假后端 `18090` 重做。基线:前台=launcher(App 在后台)、**pid `31092`**、`KeepAliveService` **3 条**(`isForeground=true` / `foregroundId=1` / `channel=keep_alive` / `ONGOING_EVENT|NO_CLEAR|SILENT`)、假后端 `wsClients=1`。挂了后台任务:**30 分钟抽查**(22:46)+ **2 小时复查**(00:16,含注入一条 proactive 看后台弹不弹)。 + +**★ 顺带推翻了一条猜想**:锚点三十五 §7 猜「上次复查时进程不在」可能是**保活开关没开**。这次 App 一启动我就查了——**服务立刻就在跑**(开关是开的)。所以那条猜想**是错的**,上次进程消失是**别的**原因(可能是系统低内存强杀,也可能是被手动划掉——**仍未定**,别当结论)。 + +**当前转发**:`tcp:80 → tcp:18090`(假后端)、`tcp:10942 → tcp:10942`(真后端)。要回真后端改这一条即可。