Commit ab5af8da3d07b5acca7fb2752e573eb47432dc29

Authored by yibo
1 parent a536104c

docs(lifelog): 同步 block-based diarization-first 管线到 HANDOFF/CLAUDE

更新录音管线描述(3512a84 的架构改动):
- §一/概述:一句话管线改为「攒块→整块 pyannote 分轮→逐 turn 转写+认人」,标注无实时字幕
- §四 架构链路:RecorderService 攒块 + BlockBuffer/BlockProcessor + Channel 解耦 + 崩溃 listPending;
  补块边界参数(SILENCE_MS=5min / MAX_MS=10min);标注旧 DiarizationWorker 已删 + WM 10min 上限
- 文件地图:加 BlockBuffer/BlockProcessor/DiarizationEngine;SpeakerClusterer 标 L1/L2
- 接口表:VAD→BlockBuffer→BlockProcessor→DiarizationEngine→逐 turn 入库
- 声纹纠偏 L1 冻结 anchor + L2 多 anchor 重建;阈值默认 0.45
- changelog 加 v0.7;多 anchor 待办标完成;logcat tag 加 BlockBuffer/BlockProcessor
- 版本号 v0.6 → v0.7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CLAUDE.md
... ... @@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
10 10 |---|---|---|---|
11 11 | **lemonPCRM** | `server/` + `frontend/` + `packages/` | Node.js + Express 后端 + 静态前端 | 主线 |
12 12 | **wechat_plain** | `wechat_plain/` | Android Xposed/A11y 模块 + Pixel 部署脚本 | 主线 |
13   -| **lifelog-recorder** | `lifelog-recorder/` | Pixel 原生 Android 应用(Kotlin + Compose) | v0.6 (2026-06) |
  13 +| **lifelog-recorder** | `lifelog-recorder/` | Pixel 原生 Android 应用(Kotlin + Compose) | v0.7 (2026-06) |
14 14  
15 15 **lemonPCRM** — lemonAI 出品的 Local-First AI 个人人脉管理系统(Personal CRM)。以人为中心、以对话为链接,通过知识图谱管理微信、LinkedIn、X 等多平台社交关系,将每个联系人与其社媒动态、会议纪要、文件、照片全面关联。
16 16  
... ... @@ -163,7 +163,7 @@ Launcher (ProcessLauncher spawn / NoneLauncher 无操作)
163 163  
164 164 详见 `server/services/wechat/README.md`。
165 165  
166   -## lifelog-recorder(Pixel 8 Pro 24/7 录音 + 本地中文 ASR + 本地 LLM,v0.6)
  166 +## lifelog-recorder(Pixel 8 Pro 24/7 录音 + 本地中文 ASR + 本地 LLM,v0.7)
167 167  
168 168 > 完整接手地图、踩坑清单、命令速查见 `lifelog-recorder/HANDOFF.md`。本节只勒住整个项目的**架构骨架 + 关键决策来源**,不重复细节。
169 169  
... ... @@ -175,18 +175,26 @@ Launcher (ProcessLauncher spawn / NoneLauncher 无操作)
175 175  
176 176 ### 架构骨架
177 177  
  178 +**block-based diarization-first(2026-06-07 起,commit 07f3d22)**:弃掉「录一句·转一句·实时聚类」(短句 embedding 噪声大、实时滑动聚类污染 anchor → 声纹串人)。改为攒块 → 整块 diarize → 逐 turn 转写,从源头产出单说话人干净 embedding。**没有实时字幕**,文字在块关闭后出。
  179 +
178 180 ```
179 181 Foreground Service (foregroundServiceType=microphone, WAKE_LOCK partial)
180 182 └ AudioRecord 16k mono PCM16 (VOICE_RECOGNITION source, no AGC/NS)
181   - └ Silero VAD ONNX (sherpa-onnx 包装,500ms 静音切 / 30s 强切)
182   - ├ paraformer-zh int8 ONNX (sherpa-onnx JNI, 4 threads, ~20× realtime)
183   - └ 3dspeaker campplus 192-d 嵌入 → SpeakerClusterer 在线聚类(自动「说话人N」,方案B)
184   - └ Room SQLite v3 (Segment/Transcript/Summary/SpeakerProfile/Chat*; addMigrations, 已去 destructive)
185   - └ Compose 5-tab UI (录音 / 对话 / 时间轴 / 声纹 / 纪要)
186   - ├ SummaryWorker:按时间间隔切「会话」→ 本地 llama-swap(8080) Qwen3.5-2B 出纪要
187   - └ ChatScreen:本地 2B/4B(llama-swap 8080) + Hermes(8642→Kimi) 多端点对话
  183 + └ Silero VAD ONNX (500ms 静音切段 / 30s 强切) ── 段不再直接入库
  184 + └ BlockBuffer:speech-only PCM 攒「块」(filesDir/blocks/<ts>/audio.pcm + manifest)
  185 + 关块:静音≥5min 或 块≥10min ──Channel──> BlockProcessor (与采集解耦)
  186 + └ DiarizationEngine: pyannote 分割 + campplus + 聚类 → turns[start,end,speaker]
  187 + └ 每 turn: paraformer 转写 + campplus 干净嵌入 + SpeakerClusterer.assign 全局认人
  188 + └ Room SQLite (turn = 时间轴一行: Segment/Transcript/Summary/SpeakerProfile/Chat*)
  189 + └ Compose 5-tab UI (录音 / 对话 / 时间轴 / 声纹 / 纪要)
  190 + ├ SummaryWorker:按时间间隔切「会话」→ 本地 llama-swap(8080) Qwen3.5-2B 出纪要
  191 + └ ChatScreen:本地 2B/4B(llama-swap 8080) + Hermes(8642→Kimi) 多端点对话
188 192 ```
189 193  
  194 +**声纹纠偏 L1+L2**(`SpeakerClusterer`):L1 = reclusterAll 和 assign 都**冻结人工命名 anchor 不漂移**(止住「秦子琛吸宜博→质心漂→吞更多」雪球);L2 = 重命名/合并时 `rebuildAnchor` 用该人**全部段均值**重建质心(多 anchor,取代单段注册)。用户流程:听一段 → 命名/合并 → 「重新聚类」。阈值默认 0.45(设置页 0.35–0.65)。CAMPPlus 在短中文段上区分度有天花板,详见 memory `lifelog-block-diarization-pipeline.md`。
  195 +
  196 +> ⚠️ 旧的周期 `DiarizationWorker`/`DiarizationScheduler` 已删除(diarize 现在在块关闭时当场做)。WorkManager 每 worker 有 **10min 硬上限**,是把 diarize 放进常驻前台服务而非 WM worker 的原因。
  197 +
190 198 **本地 LLM 栈(v0.6,零云端)**:Termux 自编 llama.cpp HEAD + llama-swap v223 监听 `127.0.0.1:8080`,按 model 名(`2b`/`4b`)热切换 Qwen3.5-{2B,4B}-Q4_K_M。纪要 worker + App 对话都打 8080;Magisk `service.d` 开机自启。完整部署/坑见 memory `pixel-local-llm-stack.md` + HANDOFF.md。
191 199  
192 200 技术栈:Kotlin 2.1 + Compose BOM 2024.12.01 + Room 2.6.1 + WorkManager 2.10 + OkHttp 4.12,AGP 8.7.3,minSdk 30 / targetSdk 35(Android 14 强制 `foregroundServiceType` 起步),arm64-v8a 单 ABI。
... ... @@ -199,9 +207,12 @@ Foreground Service (foregroundServiceType=microphone, WAKE_LOCK partial)
199 207 |---|---|---|---|
200 208 | `AudioCapture` | SharedFlow | VAD | `FloatArray` 30ms 帧 (512 samples) |
201 209 | `Vad.acceptWaveform()` | poll | `Vad.front()` / `pop()` | `SpeechSegment(start, samples: FloatArray)` |
202   -| `ParaformerEngine.transcribeFloat()` | sync | — | `OfflineRecognizerResult(text, tokens, timestamps, lang, emotion, event)` |
203   -| `SegmentWriter` | sync | filesystem | `filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.wav` |
204   -| `Repository.segments/transcripts` | suspend | Room | DAOs in `data/Daos.kt` |
  210 +| `BlockBuffer.append()` | sync | filesystem | `filesDir/blocks/<startTs>/{audio.pcm, manifest.json}`(speech-only PCM + run 偏移) |
  211 +| `BlockBuffer.close()` | Channel | `BlockProcessor.process()` | 块目录(关块条件:静音≥5min 或 块≥10min) |
  212 +| `DiarizationEngine.diarize()` | sync | — | `List<Turn(startSec, endSec, speaker)>`(pyannote 分轮) |
  213 +| `ParaformerEngine.transcribeFloat()` | sync | — | `OfflineRecognizerResult(text, tokens, timestamps, lang, emotion, event)`(逐 turn) |
  214 +| `SegmentWriter` | sync | filesystem | `filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.opus`(turn 切片) |
  215 +| `Repository.segments/transcripts` | suspend | Room | DAOs in `data/Daos.kt`(turn = 一行) |
205 216  
206 217 **外部接口**:
207 218 - `ModelDownloader` → `https://hf-mirror.com/csukuangfj/sherpa-onnx-paraformer-zh-2023-09-14/`(断点续传,fallback huggingface.co)
... ... @@ -251,7 +262,7 @@ Foreground Service (foregroundServiceType=microphone, WAKE_LOCK partial)
251 262  
252 263 完整待办、估时、技术路线见 `lifelog-recorder/HANDOFF.md` §五。
253 264  
254   -✅ **v0.2–v0.6 已完成**:Opus 编码 / BOOT 自启 + 电池白名单 / FTS4 中文搜索 / 段播放 / **声纹在线聚类(方案B 自动 diarization)** / **会话级纪要(按时间间隔切会话)** / **5-tab UI** / **App 内对话(本地 2B/4B + Hermes)** / **本地 LLM 栈(llama-swap)** / **Room 迁移(去 destructive)**。
  265 +✅ **v0.2–v0.7 已完成**:Opus 编码 / BOOT 自启 + 电池白名单 / FTS4 中文搜索 / 段播放 / **声纹在线聚类** / **会话级纪要(按时间间隔切会话)** / **5-tab UI** / **App 内对话(本地 2B/4B + Hermes)** / **本地 LLM 栈(llama-swap)** / **Room 迁移(去 destructive)** / **block-based diarization-first 录音管线(v0.7,攒块→整块分轮→逐 turn 转写)** / **声纹纠偏 L1 冻结 anchor + L2 多 anchor 重建**。
255 266  
256 267 剩余摘要:
257 268 - ⭐ **存储 TTL**(删超 N 天 .opus,文字永留)
... ...
lifelog-recorder/HANDOFF.md
... ... @@ -19,7 +19,9 @@
19 19  
20 20 ## §一 这是什么
21 21  
22   -24/7 后台录音 → VAD 切段 → 本地 paraformer 中文 ASR → 在线声纹聚类(自动「说话人N」)→ 入库 → 5-tab UI(录音/对话/时间轴/声纹/纪要)→ 按时间间隔切「会话」→ 本地 Qwen3.5 LLM 出纪要 + App 内对话。
  22 +24/7 后台录音 → VAD 攒「块」(静音≥5min 或 块≥10min 切)→ 整块 pyannote 分轮 → 每 turn 本地 paraformer 转写 + CAMPPlus 嵌入 + 全局认人 → 入库(**turn = 时间轴一行**)→ 5-tab UI(录音/对话/时间轴/声纹/纪要)→ 按时间间隔切「会话」→ 本地 Qwen3.5 LLM 出纪要 + App 内对话。
  23 +
  24 +> **2026-06-07 架构改动**:录音从「录一句·转一句·实时聚类」改为 **block-based diarization-first**(commit 07f3d22)。短句(0.3-30s) embedding 区分度弱、实时滑动聚类把噪声平均进 anchor 是声纹串人根因;整块 diarize 出单说话人干净切片再嵌入才根治。**没有实时字幕**——文字在块关闭后才出。详见 §四 pipeline + memory `lifelog-block-diarization-pipeline.md`。
23 25  
24 26 **完全本地,零云端依赖**(讯飞客户端只作云复核 fallback;纪要/对话走本机 llama-swap,Hermes→Kimi 仅作对话里一个可选端点)。v0.6(2026-06-04)起接了本地 LLM 栈,详见 §六。
25 27  
... ... @@ -50,17 +52,20 @@ app/src/main/kotlin/com/lemon/lifelog/
50 52 │ ├── SummaryProgress.kt — worker→UI 实时进度 singleton(纪要 tab 的 console)
51 53 │ └── SummaryScheduler.kt — WorkManager periodic 每 15min(UPDATE 策略) + 手动 enqueueUniqueWork
52 54 ├── service/
53   -│ ├── RecorderService.kt — 前台服务,pipeline 主体 ←★ 改 ASR 流程改这里
54   -│ ├── RecorderState.kt — StateFlow 把服务状态推到 UI
  55 +│ ├── RecorderService.kt — 前台服务,pipeline 主体(VAD 攒块 + 块处理协程)←★ 改录音流程改这里
  56 +│ ├── BlockBuffer.kt — ★ 当前块累积器:speech PCM→audio.pcm + manifest,关块条件,listPending 崩溃恢复
  57 +│ ├── BlockProcessor.kt — ★ 块→turns:pyannote 分轮 + 逐 turn 转写/嵌入/认人/入库
  58 +│ ├── RecorderState.kt — StateFlow 把服务状态推到 UI(录音中/处理段落…,无实时字幕)
55 59 │ └── BootReceiver.kt — BOOT_COMPLETED 自启 (Android 14 有 caveat 见 §七)
56 60 ├── audio/
57 61 │ ├── AudioCapture.kt — AudioRecord wrapper,emits FloatArray frames
58 62 │ ├── SegmentWriter.kt — Opus encode via MediaCodec + Ogg muxer,~17× 压缩
59 63 │ └── SegmentPlayer.kt — MediaPlayer 包装,Opus 段原音播放(系统原生支持)
60 64 ├── asr/
61   -│ ├── ParaformerEngine.kt — sherpa-onnx paraformer 包装 ←★ 主 ASR
  65 +│ ├── ParaformerEngine.kt — sherpa-onnx paraformer 包装 ←★ 主 ASR(逐 turn 转写)
  66 +│ ├── DiarizationEngine.kt — ★ sherpa-onnx 离线 diarization(pyannote 分割 + campplus + 聚类)→ turns
62 67 │ ├── SpeakerEmbedder.kt — sherpa-onnx 3dspeaker campplus 192-d 嵌入 + cosineSim + l2normalize
63   -│ ├── SpeakerClusterer.kt — ★ 方案B 在线聚类:自动发现「说话人N」+ reclusterAll 离线重算
  68 +│ ├── SpeakerClusterer.kt — ★ 在线聚类「说话人N」+ reclusterAll;L1 冻结命名 anchor + L2 rebuildAnchor 多 anchor
64 69 │ ├── SpeakerMatcher.kt — 回填工具:用当前 profile 集重算所有段的 speakerId
65 70 │ ├── HermesClient.kt — OpenAI 兼容(纪要 worker 用,指本地 llama-swap;readTimeout 600s)
66 71 │ ├── ModelDownloader.kt — 首次启动下模型(paraformer 232 MB + speaker 28 MB)
... ... @@ -96,8 +101,8 @@ adb shell pm grant com.lemon.lifelog android.permission.RECORD_AUDIO
96 101 adb shell pm grant com.lemon.lifelog android.permission.POST_NOTIFICATIONS
97 102 adb shell am start -n com.lemon.lifelog/.MainActivity
98 103  
99   -# 看 pipeline 日志
100   -adb logcat -v time -s RecorderService:V ParaformerEngine:V AudioCapture:V
  104 +# 看 pipeline 日志(块攒/关/处理)
  105 +adb logcat -v time -s RecorderService:V BlockBuffer:V BlockProcessor:V DiarizationEngine:V ParaformerEngine:V
101 106  
102 107 # 手动启 service(绕开 UI 点按)
103 108 adb shell 'su -c "am start-foreground-service -n com.lemon.lifelog/.service.RecorderService"'
... ... @@ -127,39 +132,60 @@ adb shell &#39;su -c &quot;
127 132  
128 133 ## §四 架构链路
129 134  
  135 +**block-based diarization-first(2026-06-07 起)**:录音端只攒块,处理端整块分轮——两端用 `Channel` 解耦,处理不阻塞采集。
  136 +
130 137 ```
131 138 RecorderService.onCreate()
132 139 ├─ startForeground(NOTIFICATION_ID, notification, TYPE_MICROPHONE)
133 140 ├─ wakeLock.acquire()
134 141 └─ scope.launch { runPipeline() }
135 142 │
136   - ├─ ModelDownloader.ensure() — 232 MB 模型在 filesDir 就 skip
137   - ├─ ParaformerEngine.load(numThreads=4) — 加载 ~1.3s 一次性
  143 + ├─ ModelDownloader.ensure() — paraformer 232 MB + speaker 28 MB + pyannote 6 MB
  144 + ├─ ParaformerEngine.load(numThreads=4) — 一次性常驻
  145 + ├─ SpeakerEmbedder.load() / SpeakerClusterer().load()
  146 + ├─ DiarizationEngine.load(pyannote, campplus, thr=0.45) — 常驻(缺则降级单 turn)
  147 + ├─ BlockProcessor + 处理协程:for (dir in pendingBlocks) processor.process(dir)
  148 + ├─ BlockBuffer.listPending(filesDir/blocks) → pendingBlocks ← 崩溃恢复
138 149 ├─ Vad(silero, 500ms silence, 300ms min, 30s max)
139 150 ├─ capture.start(scope) — AudioRecord 16k mono PCM
140 151 └─ capture.frames.collect { samples ->
141 152 vad.acceptWaveform(samples)
142   - while (!vad.empty()) {
143   - val seg = vad.front(); vad.pop()
144   - handleSegment(seg.samples)
145   - }
  153 + while (!vad.empty()) { appendSpeech(vad.front().samples); vad.pop() }
  154 + if (blockBuffer.shouldClose(now)) closeBlock() ← 静音/超长也要每帧查
146 155 }
147 156 │
148   - handleSegment(samples):
149   - ├─ segWriter.write(samples) → WAV 到 filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.wav
150   - ├─ engine.transcribeFloat(samples) → text
151   - ├─ repo.segments.insert(Segment(..., audioPath, durationMs))
152   - ├─ repo.transcripts.insert(Transcript(segmentId, text, "paraformer-zh-on-device"))
153   - └─ RecorderState.onSegment(text, audioMs, wallMs) → UI 实时刷新
  157 + appendSpeech(seg): blockBuffer.append(seg, startTs) — 跨静音/超长先关旧块
  158 + └─ BlockBuffer:speech-only PCM 追加 filesDir/blocks/<startTs>/audio.pcm
  159 + + manifest.json 记每段 {ts, off, len}(off→真实时间映射)
  160 + closeBlock(): blockBuffer.close() → pendingBlocks.trySend(dir)
  161 + │
  162 + BlockProcessor.process(dir): — 整块一次性处理
  163 + ├─ BlockBuffer.load() → 整块 FloatArray + runs
  164 + ├─ diar.diarize(samples) → turns[start,end,speaker](pyannote 分轮)
  165 + └─ 每 turn 切片:
  166 + ├─ asr.transcribeFloat(slice) → text
  167 + ├─ embedder.embed(slice) → 干净单说话人 embedding
  168 + ├─ clusterer.assign(emb) → 全局 speakerId(L1 命名 anchor 冻结)
  169 + ├─ segWriter.write(slice) → filesDir/segments/.../<ts>.opus
  170 + └─ repo.segments.insert + repo.transcripts.insert(turn = 时间轴一行)
  171 + └─ dir.deleteRecursively() — 处理完即删(= 已处理标记)
154 172 ```
155 173  
  174 +**没有实时字幕**:`RecorderState` 只反映「录音中 / 处理段落…」;时间轴行由 DB Flow 在块处理完后刷新。
  175 +
156 176 VAD 参数(在 `RecorderService.runPipeline` 里调):
157 177 - `threshold=0.5` — Silero 阈值
158   -- `minSilenceDuration=0.5s` — 500ms 静音才切段
  178 +- `minSilenceDuration=0.5s` — 500ms 静音才切「段」(段攒进块,非直接入库)
159 179 - `minSpeechDuration=0.3s` — 短于 300ms 的丢
160   -- `maxSpeechDuration=30s` — 强切上限
  180 +- `maxSpeechDuration=30s` — 段强切上限
161 181 - `windowSize=512` — Silero 输入窗口(32ms)
162 182  
  183 +块边界参数(`BlockBuffer` companion):
  184 +- `SILENCE_MS=5min` — 距上次语音静音超时切块
  185 +- `MAX_MS=10min` — 块超长强切(长会议保活,避免全程不出文字)
  186 +
  187 +> ⚠️ 旧的周期 `DiarizationWorker`/`DiarizationScheduler` 已删除(diarize 现在在块关闭时当场做,不再事后整体重跑)。声纹纠偏走「声纹」tab 的「重新聚类」(reclusterAll)。WorkManager 每 worker 有 **10min 硬上限**——这也是把 diarize 放进常驻前台服务而非 WM worker 的原因。
  188 +
163 189 ---
164 190  
165 191 ## §五 §六 待办(按优先级)
... ... @@ -170,12 +196,13 @@ VAD 参数(在 `RecorderService.runPipeline` 里调):
170 196 - v0.4 (2026-06):段播放 (MediaPlayer Opus) / bottom sheet 操作菜单 / enroll 后自动回填老段
171 197 - v0.5 (2026-06):hermes api_server 端点 + SummaryWorker + 纪要 UI section
172 198 - v0.6 (2026-06-04):**声纹在线聚类**(SpeakerClusterer,自动「说话人N」+ reclusterAll)/ **5-tab UI**(录音/对话/时间轴/声纹/纪要,时间轴全 CRUD + 批量删导出 + 按说话人/时间/未匹配筛选 + 排序)/ **真暂停**(停采集留模型)/ **会话级纪要**(按时间间隔切会话、可调阈值、要点带 [HH:mm:ss] 点击跳回时间轴、折叠头显示说话人名单)/ **App 内对话**(本地 2B/4B + Hermes 多端点流式)/ **本地 LLM 栈**(llama.cpp HEAD + llama-swap 8080,纪要+对话全本地,见 §六)/ **Room v3 迁移**(去 destructive 改 fail-loud)
  199 +- v0.7 (2026-06-07):**block-based diarization-first 录音管线**(攒块→整块 pyannote 分轮→逐 turn 转写+嵌入+认人,turn=时间轴一行;去实时字幕;BlockBuffer/BlockProcessor;崩溃 listPending 续跑;commit 07f3d22)/ **声纹纠偏 L1+L2**(L1=reclusterAll & assign 冻结命名 anchor 不漂移止串人;L2=重命名/合并时 rebuildAnchor 用该人全部段均值重建多 anchor)/ 阈值默认 0.45、设置页 0.35-0.65 可调
173 200  
174 201 剩下:
175 202  
176 203 1. **存储 TTL**:删超过 N 天的 .opus,文字永留。Periodic WorkManager job。
177 204 2. **设置页**:存储用量、模型切换、provider 切换、暂停时段配置(v1 定的 MVP 是「一键暂停就够」,但 v2 可能要细化)。
178   -3. **多 anchor 平均**:现在 enroll 是单段单 anchor,可以「连选多段都标 我」自动算平均 embedding 提高匹配率。
  205 +3. ✅ **多 anchor 平均**(2026-06-07 完成,L2):重命名/合并某说话人时 `rebuildAnchor` 用该人名下全部段的均值重建 anchor,取代单段注册。
179 206 4. **回链 lemonPCRM**:Transcript 提到的人名 / 项目 → GraphStore `MENTIONED_IN` 边。
180 207 5. ✅ **hermes gateway 自启**(2026-06-04 完成):用 Magisk `service.d` 脚本开机拉起(Termux:Boot 未装,root 方案更稳),见下方「§ 自启(Magisk service.d)」。
181 208  
... ... @@ -323,7 +350,7 @@ bot 现在跑得起来(v0.1 闭环已通),但还差几件让它能脱离 d
323 350  
324 351 ./gradlew :app:assembleDebug --no-daemon
325 352 adb install -r app/build/outputs/apk/debug/app-debug.apk
326   - adb logcat -v time -s RecorderService:V ParaformerEngine:V
  353 + adb logcat -v time -s RecorderService:V BlockBuffer:V BlockProcessor:V ParaformerEngine:V
327 354  
328 355 读完先回我:(a) bot 当前在不在跑(adb shell dumpsys activity services
329 356 com.lemon.lifelog),(b) 你建议先做 §五 里哪 1-2 项,理由。
... ...