# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 项目概述 本仓库(`aipcrm/`)的**主线 = `lifelog-recorder/`** —— Pixel 8 Pro 上一个**完全端上、零云端**的 Local-First 个人记忆 + 人脉管理(Personal CRM)原生应用:24/7 录音 + 本地中文 ASR + 声纹/人脸识别 + 端上 LLM 纪要/对话 + 以「人」为中心的联系人档案 + 全相册语义索引。所有能力跑在设备上,**不依赖任何 Mac/PC 服务**。 | 子项目 | 路径 | 形态 | 状态 | |---|---|---|---| | **lifelog-recorder** | `lifelog-recorder/` | Pixel 原生 Android 应用(Kotlin + Compose) | **主线 v0.8** | | **wechat_plain** | `wechat_plain/` | Pixel 上微信 bot 的 Xposed/A11y 模块(端上) | 辅助 | | **lemonPCRM** | `server/` + `frontend/` + `packages/` | Node.js + Express 后端 + 静态前端 | **已废弃** | > [!IMPORTANT] 方向已转向纯端上 App > 早期主线是 **lemonPCRM**(Mac 上 Node/Express + 知识图谱 + ACP 驱动 Claude CLI 的服务端 CRM)。 > 现已**废弃**:其 Personal CRM 能力已在 `lifelog-recorder` 里**原生端上重实现**(联系人档案、人脸/声纹识别、 > AI 印象、RAG 对话、照片语义检索),不再依赖 Mac。`server/` `frontend/` `packages/` 代码仅作历史留仓, > **新功能不要往里加,也不要把 app 回链到它**。App 与旧 lemonPCRM 的能力对比 / 仍存差距见下文 > §lifelog-recorder「App 即 Personal CRM:与旧 lemonPCRM 的能力对比」。 ### wechat_plain(端上微信 bot,辅助) Pixel 上微信发送/跟踪的落地,纯设备内运行(不依赖 Mac): - **发送消息(唯一可用链路)**:`am broadcast com.hermes.wechat.SEND_MSG` → **`HermesAccessibilityService`**(lsposed-wechat-sendmsg 模块内)→ 启动微信 LauncherUI → 点搜索按钮 → 搜索 nickname(如"火星人")→ 点结果 → `bkk` 容器内 EditText 输入文本 → 点 `bql` 发送按钮。纯 A11y UI 自动化,绕开所有反射 / hook 失败。 - **已废弃的两条发送路径(命名误导,勿用)**:LSPosed 反射 API(`HermesWeChatModule.sendTextMessageDirect`,`Send result: true` 是误报,消息不进 `message` 表);Frida daemon(Frida 17 + 微信 8.0.72 注入大 bundle 触发反调试崩溃)。真正能推进 message 表的只有 A11y service。 > [!IMPORTANT] A11y service 启用方式 > `enabled_accessibility_services` 系统设置易被关掉,重启: > ```bash > adb shell "su -c 'settings put secure enabled_accessibility_services com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService; settings put secure accessibility_enabled 1'" > ``` 详见 `wechat_plain/` 内文档。 ## lifelog-recorder(Pixel 8 Pro 24/7 录音 + 本地中文 ASR + 本地 LLM,v0.8) > 完整接手地图、踩坑清单、命令速查见 `lifelog-recorder/HANDOFF.md`。本节只勒住整个项目的**架构骨架 + 关键决策来源**,不重复细节。 ### 项目目标 把 Pixel 8 Pro 当成**永远开机的麦克风**:7×24 后台录音 → VAD 切段 → 中文 ASR 转文字 → 入库 → 按时间轴显示和搜索 → LLM 总结成纪要。最终目的是把所有「我说过 / 别人对我说过」的内容沉淀成可搜索、可回放的个人记忆库,并在 **app 内自成端上 Personal CRM**(photolog):某段语音 → 声纹 → 联系人档案,照片 → 人脸 → 同一联系人。**整套 CRM 跑在设备上,已取代旧的 Mac 端 lemonPCRM**。 约束:**完全本地 / 零云端依赖 / 月成本 0 元 / 隐私走 0 网络**。 ### 架构骨架 **block-based diarization-first(2026-06-07 起,commit 07f3d22)**:弃掉「录一句·转一句·实时聚类」(短句 embedding 噪声大、实时滑动聚类污染 anchor → 声纹串人)。改为攒块 → 整块 diarize → 逐 turn 转写,从源头产出单说话人干净 embedding。**没有实时字幕**,文字在块关闭后出。 ``` Foreground Service (foregroundServiceType=microphone, WAKE_LOCK partial) └ AudioRecord 16k mono PCM16 (VOICE_RECOGNITION source, no AGC/NS) └ Silero VAD ONNX (500ms 静音切段 / 30s 强切) ── 段不再直接入库 └ BlockBuffer:speech-only PCM 攒「块」(filesDir/blocks//audio.pcm + manifest) 关块:静音≥5min 或 块≥10min ──Channel──> BlockProcessor (与采集解耦) └ DiarizationEngine: pyannote 分割 + campplus + 聚类 → turns[start,end,speaker] └ 每 turn: paraformer 转写 + campplus 干净嵌入 + SpeakerClusterer.assign 全局认人 └ Room SQLite (turn = 时间轴一行: Segment/Transcript/Summary/SpeakerProfile/Chat*) └ Compose 底部 4-tab (人物 / 录音 / 对话 / 设置);「录音」内含 4 顶部子 tab (录音 / 时间轴 / 声纹 / 纪要) ├ SummaryWorker:按时间间隔切「会话」→ 本地 llama-swap(8080) Qwen3.5-2B 出纪要 ├ ChatScreen:本地 2B/4B(llama-swap 8080) + Hermes(8642→Kimi) 多端点对话 └ photolog/CRM 层 (PhotologScreen/PersonProfileScreen/PhotologViewModel + vision/) 人物 tab = 「我管理的人物」;通讯录(微信+手机两源合并)是池子(勾选纳入);相册人脸标注/绑声纹自动入库 相册 tab = 全相册自动索引(MediaStore 全量):Tier-0 人脸/时间秒级铺 + Tier-1 VL 场景描述充电补 + 搜图(关键词/语义) ``` **声纹纠偏 L1+L2**(`SpeakerClusterer`):L1 = reclusterAll 和 assign 都**冻结人工命名 anchor 不漂移**(止住「秦子琛吸宜博→质心漂→吞更多」雪球);L2 = 重命名/合并时 `rebuildAnchor` 用该人**全部段均值**重建质心(多 anchor,取代单段注册)。用户流程:听一段 → 命名/合并 → 「重新聚类」。阈值默认 0.45(设置页 0.35–0.65)。CAMPPlus 在短中文段上区分度有天花板,详见 memory `lifelog-block-diarization-pipeline.md`。 > ⚠️ 旧的周期 `DiarizationWorker`/`DiarizationScheduler` 已删除(diarize 现在在块关闭时当场做)。WorkManager 每 worker 有 **10min 硬上限**,是把 diarize 放进常驻前台服务而非 WM worker 的原因。 **本地 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。 **photolog / 人脉 CRM 层(v0.7 新增)**:在录音之外把 lifelog 长成端上 mini-CRM。`PhotologScreen`(人物 tab + 相册 tab)、`PersonProfileScreen`(联系人档案:tab 式 + 记录搜索 + 微信聊天/朋友圈/AI 印象 + 「🎤 录一段绑定 TA」当场录声纹)、`PhotologViewModel` 统管。**两个人物池分离**:「我管理的人物」(CRM 关心的人,人物 tab 显示)vs「通讯录全部联系人」(池子,选人页里勾选纳入)。池子有**两个来源**:微信(Mac 推的 enroll 数据)+ 手机系统通讯录(`READ_CONTACTS` → `ContactsContract`,contactRef `phone:`,按手机号/姓名跨源自动合并);`Person` 还存手填 `realName`(真名)/`resume`(简历),档案页可编辑。`vision/`(`FaceEngine`/`FaceMatcher`/`PhotoProcessor`/`PhotoScheduler`/`PhotoFaceWorker`/`PhotoVlWorker`/`PhotoVlState`/`GalleryScanner`/`VisionClient`/`EmbedClient`/`WeChatEnrollWorker`)做相册扫描 + 人脸聚类 + 微信头像锚点入库;微信头像配方与「锚点 embedding 必须端上算」见 memory `wechat-avatar-face-enrollment.md`。声纹绑人 ↔ 人脸标注 ↔ 微信联系人三者打通到同一 `Person`。 **全相册自动索引 / 分层管线(v0.8 新增)**:把手机**整个相册**自动跑成可搜索内容。核心矛盾:本地 VL(Qwen2.5-VL-3B)真机 **~30-40s/张**,几千张全量 VL 按天计 → **分层**。`GalleryScanner` 游标遍历 MediaStore 全量,**只存 `content://` uri 不拷文件**(拷一份撑爆私有目录),`mediaId` 去重增量;`photos` 加 `source`/`mediaId`/`scanState`/`captionEmbedding`(Room **v14**)。**Tier-0 `PhotoFaceWorker`**(15min,battery-not-low):扫描 + ML Kit 人脸 + 512d emb + GPS,秒级全量铺好,立刻按人/时间搜;**Tier-1 `PhotoVlWorker`**(15min,**+charging**):Qwen-VL 场景描述 + caption 语义向量,优先级队列(有人脸>近期),慢慢补。`scanState`:`0 待索引→1 人脸已铺→2 已出描述`;imported 源(手动导入)走同管线,只是 `decode` 走 file path。**搜图**:关键词(caption+人名 LIKE,中文子串比 FTS4 靠谱)+ 语义(`EmbedClient` 打 llama-swap `embed` 模型出 query 向量 → 全库 caption 向量余弦 top-K;没配 embed 模型则优雅降级,只用关键词)。**VL 控制 `PhotoVlState`**(singleton,paused 持久化):相册页运行中显「暂停/停止」、空闲显「立即理解/继续」,正在 VL 的格子转圈,格子角标 待索引/待理解/已描述;暂停连充电时的周期 worker 也尊重。详见 `HANDOFF.md` §四 photolog 段。 技术栈: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。 ### 接口 **内部组件接口**(pipeline,详见 `lifelog-recorder/HANDOFF.md` §四): | 上游 | → | 下游 | 数据形式 | |---|---|---|---| | `AudioCapture` | SharedFlow | VAD | `FloatArray` 30ms 帧 (512 samples) | | `Vad.acceptWaveform()` | poll | `Vad.front()` / `pop()` | `SpeechSegment(start, samples: FloatArray)` | | `BlockBuffer.append()` | sync | filesystem | `filesDir/blocks//{audio.pcm, manifest.json}`(speech-only PCM + run 偏移) | | `BlockBuffer.close()` | Channel | `BlockProcessor.process()` | 块目录(关块条件:静音≥5min 或 块≥10min) | | `DiarizationEngine.diarize()` | sync | — | `List`(pyannote 分轮) | | `ParaformerEngine.transcribeFloat()` | sync | — | `OfflineRecognizerResult(text, tokens, timestamps, lang, emotion, event)`(逐 turn) | | `SegmentWriter` | sync | filesystem | `filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.opus`(turn 切片) | | `Repository.segments/transcripts` | suspend | Room | DAOs in `data/Daos.kt`(turn = 一行) | **外部接口**: - `ModelDownloader` → `https://hf-mirror.com/csukuangfj/sherpa-onnx-paraformer-zh-2023-09-14/`(断点续传,fallback huggingface.co) - `XfyunIatClient` → `wss://iat.xf-yun.com/v1`(讯飞大模型,**仅作云复核 fallback**) - 未来 Summary Worker → `http://127.0.0.1:8642/v1/chat/completions`(hermes gateway 转发到 Kimi) **对外暴露的 IPC**:`RecorderService` 仅做自启/暂停(`startForegroundService` / `stopService`),状态走 `RecorderState` (StateFlow) 推给 UI;没有 binder/广播接口。 ### 历史沿革 / PoC 失败与成功记录 > 这一节是**为什么选了 paraformer 而不是 whisper / 云 ASR** 的完整溯源。后面 session 想换技术栈前必读。 **Round 1 — 全云 ASR 路线(弃)**:最初想用「VAD 段 → 上传讯飞 / 百度 / Groq Whisper」流式 ASR。讯飞「中文识别大模型」WebSocket 客户端已经写完(`asr/XfyunIatClient.kt`,鉴权 + 三段帧协议 + 增量结果解析),但发现按次计费下 24/7 用量会冲到月 5000+ 元。「讯飞流式听写区间包 80 元/月 unlimited」是**消费端「讯飞听见」App 的卡,不是开发者 API**——这是用户和我都踩过的坑。 **Round 2 — whisper.cpp on-device(弃)**:在 Pixel 8 Pro Termux 编了 whisper.cpp + NEON 优化,跑 95s 中文 TTS sample: | 模型 | 线程 | RTF | 速度 | 中文质量 | |---|---|---|---|---| | ggml-tiny.bin | 4 | 0.26 | **3.8× realtime** | ❌ 输出乱码「战亡未来一年」,中文不可用 | | ggml-small-q5_1.bin (181 MB) | **8 (最快)** | 1.66 | **0.60× realtime** | ✓ 转写正确 | | ggml-small.bin (fp16, 465 MB) | 4 | 1.76 | 0.57× | 同 q5_1(量化省的全被 ARM dequant 吃回) | **结论**:whisper-small **任何配置都 < 1× realtime**,连续录会越积越多。tiny 够快但中文崩。**on-device whisper 路线直接砍掉**。 **Round 3 — sherpa-onnx + 中文专门优化模型(采用)**:阿里 DAMO 的 paraformer-zh 跟 SenseVoice 都是 ONNX 部署、Android 友好。同样 95s sample: | 模型 | 测试环境 | RTF | 速度 | 备注 | |---|---|---|---|---| | paraformer-zh int8 (232 MB) t=4 | proot Ubuntu | 0.068 | **14.7× realtime** | 选定 | | paraformer-zh int8 t=8 | proot | 0.099 | 10.1× | 8 线程反而慢,X3+A715 协调开销 | | SenseVoice int8 (228 MB) t=4 | proot | 0.092 | 10.9× | 带情绪/事件 metadata,但慢 | | **paraformer-zh int8 t=4** | **真机 Android app** | **0.048** | **20.94× realtime** | proot 摘掉后再快 43% | | **paraformer-zh int8 t=4** | **真实活语音(用户对手机说)** | — | **20-32×** | 1-10s 段,~50-300ms 转写 | **结论**:on-device paraformer 完胜。月成本 0 元,延迟 <300ms,隐私零网络。讯飞客户端代码留作云复核 fallback。 ### 关键坑(接手必读) 1. **不要把 .onnx 当 asset mmap**:sherpa-onnx v1.13.2 native code 对 asset 压缩兼容差,silero VAD 直接 SIGSEGV。已加 `androidResources.noCompress.add("onnx")` 但保险起见在 `RecorderService.runPipeline` 里把 VAD 模型从 assets 拷到 `filesDir` 后再用 file path 加载。 2. **silero_vad.onnx 别从 snakers4 的 GitHub raw 拉**:会被 LFS 重定向成 HTML。用 `https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx`。 3. **Tensor G3 paraformer 线程甜点 = 4,不是 8**。 4. **AID_INET (3003) gotcha**:跨 root + adb 调试 Termux 用户态时必须 `su -G 3003 10288 -c "..."`,否则 DNS 全断。详见 `~/.claude/projects/-Users-yibo-Documents-aipcrm/memory/magisk-su-android-inet-group.md`。 5. **`keys.properties` gitignored**:讯飞凭证从这里读到 `BuildConfig.XFYUN_*`。模板 `keys.properties.example`。 6. **232 MB paraformer 模型不进仓库**:首次启动从 hf-mirror.com 下到 `app.filesDir/models/paraformer-zh/`。dev 加速可以预先 `adb push` 进去(HANDOFF.md §三)。 ### 未来计划(按优先级) 完整待办、估时、技术路线见 `lifelog-recorder/HANDOFF.md` §五。 ✅ **v0.2–v0.9 已完成**:Opus 编码 / BOOT 自启 + 电池白名单 / FTS4 中文搜索 / 段播放 / **声纹在线聚类** / **会话级纪要(按时间间隔切会话)** / **底部 4-tab + 子 tab UI** / **App 内对话(本地 2B/4B + Hermes)** / **本地 LLM 栈(llama-swap)** / **Room 迁移(去 destructive)** / **block-based diarization-first 录音管线(v0.7,攒块→整块分轮→逐 turn 转写)** / **声纹纠偏 L1 冻结 anchor + L2 多 anchor 重建** / **设置页(`SettingsScreen`:纪要模型后端 / 聚类阈值 / llama-swap 端点 / 电池豁免)** / **photolog 人脉 CRM 层(人物档案 + 相册人脸 + 微信通讯录/朋友圈/AI 印象)** / **存储 TTL(超期删 .opus,文字永留)** / **全相册自动索引(v0.8:MediaStore 全量扫描 + Tier-0 人脸/Tier-1 VL 分层管线 + 关键词/语义搜图 + VL 暂停/停止控制,Room v14)** / **微信抽取完全端上化(v0.8 P2 #6:SQLCipher key 端上推导 + 联系人/头像/聊天/朋友圈全不依赖 Mac)** / **Transcript→Person 提及关系(v0.9 P0 #5:词典反查 + MentionScanner 实时扫 + mentions 表 + 「被提到」tab)** / **人↔人共现边(v0.9 P1 #1:RelationBuilder + relations 表 + 「相关」tab + CommunitiesScreen)** / **待跟进 & 话题实体(FollowupExtractor + TopicExtractor,4B LLM 抽取)**。 剩余摘要: - ⭐ **语义搜图落地**(代码已完成:`EmbedClient` + `SettingsScreen` embed 字段;**需在 Pixel llama-swap config.yaml 加 embed 模型**才生效,中文 GGUF 如 bge-small-zh,参见 HANDOFF.md §七 调试陷阱) - ✅ **内部 Transcript 实体 → Person 表**(已完成:`crm/MentionExtractor.kt` + `MentionScanner` 词典反查 + `BlockProcessor` 实时扫 + `PersonProfileScreen` 「被提到」tab + `RelationBuilder` 共现边) - ✅ **微信抽取端上化**(已完成 2026-06-10:SQLCipher 端上解密 + 联系人/头像/朋友圈全端上,不依赖 Mac) ### App 即 Personal CRM:与旧 lemonPCRM 的能力对比 App(photolog)已覆盖、并在「联系人」维度超过旧 lemonPCRM 的核心: | 能力 | 旧 lemonPCRM(Mac/server) | 现 app(端上) | |---|---|---| | 联系人实体 | `Person` 图节点 | `Person`(融合 **声纹/人脸/微信/手机** 四模态,更丰富) | | 多源导入 | 微信 / LinkedIn / X / 会议 / 照片 | 微信 + 手机通讯录 + 手动 + **24/7 录音(独有)** | | 每人聚合视图 | 社媒动态 / 纪要 / 文件 / 照片 | 聊天 / 朋友圈 / 照片 / **录音** / AI 印象 / 简历(`PersonProfileScreen` 四 tab) | | AI | Master Agent over 图谱(工具编排) | RAG 对话 over 转写/纪要(`LifelogRetriever`)+ AI 印象 | | 照片 | 人脸聚类 + CLIP 检索 | 人脸聚类 + 语义检索(端上 ML Kit + ArcFace + Qwen-VL + embed) | | 部署 | Mac Node 服务(依赖 Claude CLI) | **纯端上**(更隐私 / 零成本 / 零网络) | **仍存差距**(旧 lemonPCRM 有、app 还没有): 1. **人↔人关系图**:app 只有 *人↔模态*(声纹/人脸/微信/手机)链接,**没有 person↔person 边**(KNOWS / 同事 / 同项目)。app 现在是「带富资料的扁平联系人表」,不是知识图谱。 2. **Organization / Project / Event 实体**:app 只有 `Person`/`Photo`/`Face`,没有组织/项目/事件节点。 3. **关系分析**:旧有 关系路径(`path`) / 社群发现(`communities`) / 待跟进(`followups`);app 三者皆无。 4. **AI agent / 工具编排**:app 是**纯 RAG 对话**,AI 不能对数据做动作(建人 / 连边);旧有 Master→Sub-Agent + 图谱工具(`create_person`/`link_persons` 等)。 5. **Transcript 内容 → 实体**:app 只按**声纹**把录音连到 `Person`(谁说的),**没有从转写文本抽人名/项目**做关联(旧 lemonPCRM 的 `MENTIONED_IN`)——这正是下一步要补的。 6. **微信抽取端上化**:微信联系人/头像/聊天/朋友圈仍由 **Mac 一次性抽取**(解密 `EnMicroMsg.db` + 抠头像)推到 `filesDir/wx_enroll/`,再由 `WeChatEnrollWorker` 端上入库。手机系统通讯录已完全端上;微信抽取是「纯 App」最后一块拼图。 ### 下一步:Transcript 实体 → 内部 Person 表(取代旧「回链 lemonPCRM」) **不再回链外部 server**,改在 **app 内部**打通:从 `Transcript.text` / `Summary` 抽实体(人名/项目/地点,端上 LLM 或规则 NER)→ 与本地 `Person` 表匹配 → 建一张「提及」关系(新表 `mentions(segmentId, personId, ...)` 或 Person↔Segment 边)。让 `PersonProfileScreen` 除了现有「TA 自己说的」(声纹匹配段)之外,还能显示「**TA 被哪些录音提到、说了什么**」。这直接补差距 #5;同段被同时提到的人 → 共现边,又为差距 #1(人↔人关系图)打基础。 > 上面 6 项差距的**可执行 TODO 清单(带优先级 P0–P3 + 做法)**见 `lifelog-recorder/HANDOFF.md` §「端上 CRM 补齐 — TODO」。路线:P0 #5 → P1 #1 → P2 #6。 ## 注意事项 - **构建**:`cd lifelog-recorder && JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew :app:assembleDebug`,`adb install -r ...`。无测试框架。改 Room schema **必须写 Migration**(`AppDatabase` 已去 destructive 改 fail-loud;24/7 录音库静默清掉是大事故,见 memory `lifelog-room-migration-wipe.md`)。 - **改完即真机验证**:app 是 24/7 录音的命脉,改 `RecorderService` / 管线后务必装机看 logcat(`reconcile →` / `pipeline running` / `BlockProcessor`)确认录音没断。 - **`keys.properties` / `local.properties` gitignored**:讯飞凭证 + Hermes 端点 → `BuildConfig`;`sdk.dir` 换机要改。模板见 `keys.properties.example`。 - **`server/` `frontend/` `packages/`(旧 lemonPCRM)已废弃**:不再维护、不要往里加功能、不要让 app 依赖它。 > [!NOTE] 旧 lemonPCRM 的服务端架构(ACP 四层 / Master→Sub-Agent / GraphStore / 微信 server 链路)已从本文件移除。 > 如确需考古,查 git 历史里本文件的早期版本。