# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 项目概述 本仓库(`aipcrm/`)现在托管 **3 个互相关联但独立的子项目**: | 子项目 | 路径 | 形态 | 状态 | |---|---|---|---| | **lemonPCRM** | `server/` + `frontend/` + `packages/` | Node.js + Express 后端 + 静态前端 | 主线 | | **wechat_plain** | `wechat_plain/` | Android Xposed/A11y 模块 + Pixel 部署脚本 | 主线 | | **lifelog-recorder** | `lifelog-recorder/` | Pixel 原生 Android 应用(Kotlin + Compose) | v0.7 (2026-06) | **lemonPCRM** — lemonAI 出品的 Local-First AI 个人人脉管理系统(Personal CRM)。以人为中心、以对话为链接,通过知识图谱管理微信、LinkedIn、X 等多平台社交关系,将每个联系人与其社媒动态、会议纪要、文件、照片全面关联。 AI 对话与编排层基于 ACP 协议驱动本地 Claude CLI 进程,提供单 Agent 执行和 Master → Sub-Agent 多轮编排能力。Node.js + Express,无数据库依赖(使用 Node 22.5+ 内置 `node:sqlite`),数据完全本地化。 **wechat_plain** — Pixel 上微信 bot 的 Xposed/A11y 落地(A11y UI 自动化是当前唯一能真把消息推进 message 表的发送链路;详见下文 §微信集成架构)。 **lifelog-recorder** — Pixel 8 Pro 上的 24/7 录音 + 本地中文 ASR + 时间轴 lifelog 应用(2026-06 新增,详见下文 §lifelog-recorder)。 ## 常用命令 ```bash npm run setup # 首次初始化:cp .env.example .env + pnpm install npm start # 生产模式(node server/index.js) npm run dev # 开发模式(node --watch server/index.js,热重载) ./dev.sh # 杀掉端口占用进程 + npm run dev(推荐开发用) ``` 无测试框架,无 lint 脚本。使用 pnpm workspaces(`packageManager: pnpm@10.28.1`)。 ## 架构 ### 技术栈 - **后端**:Node.js (≥18) + Express,pnpm workspace monorepo - **数据库**:Node 22.5+ 内置 `node:sqlite`(`DatabaseSync`),位于 `server/db/lemon_crm.db` - **AI**:Claude CLI(本地子进程,`--dangerously-skip-permissions` 自动批准) - **文件解析**:mammoth(.docx)、pdf-parse(.pdf) ### 目录速查 | 目录 | 说明 | |------|------| | `packages/acp-core/` | ACP 协议层:Protocol / Transport / Launcher 三层抽象 + ClaudeCliProtocol / AcpProtocol | | `packages/agent-core/` | Agent 运行时(ESM):目录驱动的 Agent、AgentBus、AgentRegistry、AgentScaffold | | `server/index.js` | Express 入口(路由 + 静态前端) | | `server/src/core/` | 编排引擎:orchestrator、master-session、agent-manager、session-pool、stream-executor、prompt-builder、parser、runtime-resolver | | `server/src/engine/` | 进程层:run-registry、command-queue、process-guard、idle-timer、failover-error | | `server/src/tools/` | Master 工具:call-agent-tool、tool-registry、graph/(图谱 MCP 工具集) | | `server/src/standalone-entry.js` | 编程 API:`runMasterSession` / `runOrchestration` / `runSingleAgent` | | `server/routes/` | API 路由:master-chat、config、graph、wechat、import、analysis、photo | | `server/services/` | conversation-logger、file-parser、memory-service、claude-chat-service、wechat/、linkedin/、meeting/、photo/、x/ | | `server/db/GraphStore.js` | 本地知识图谱存储层(SQLite):Person / Organization / Project / Event / Asset 节点 + KNOWS / BELONGS_TO / PARTICIPATED 等边 | | `frontend/` | 静态前端(单 HTML 文件,由 server/index.js 托管) | | `server/config/backends.json` | ACP Launcher / Client 定义 | | `server/config/agents.json` | 静态 Agent 注册表(可选,也可由 `system/agents/` 目录发现) | ### API 路由 | 路径 | 说明 | |------|------| | `POST /api/master/chat` | Master Agent 多轮对话(SSE) | | `POST /api/master/upload` | 文件上传注入对话 | | `GET /api/master/sessions` | 列出活跃 session | | `DELETE /api/master/sessions/:id` | 关闭 session | | `GET/PUT /api/config/items/:key` | 读写配置文件 | | `GET /api/health` | 健康检查 | | `GET /api/graph/nodes` | 列出图谱节点(?type=Person&limit=100) | | `GET /api/graph/nodes/:id` | 查询单个节点(含边) | | `POST /api/graph/nodes` | 创建/更新节点 | | `GET /api/graph/search?q=关键词` | 全文搜索 | | `POST /api/graph/edges` | 创建关系边 | | `GET /api/graph/path?from=id&to=id` | 查询关系路径 | | `POST /api/wechat/import-contacts` | 导入微信联系人到图谱 | | `POST /api/wechat/import-chat` | 导入微信聊天记录 | | `GET /api/wechat/contacts?q=搜索` | 查询微信联系人 | | `GET /api/wechat/sessions` | 最近会话列表 | | `POST /api/import/...` | 多平台数据导入 | | `POST /api/analysis/...` | 图谱分析(关系链路、聚类等) | | `POST /api/photo/...` | 照片服务(人脸聚类、向量检索) | ### 配置文件 | 文件 | 说明 | |------|------| | `.env` | PORT(默认 3010)、HOST、CORS_ORIGINS 等 | | `server/config.js` | 端口、CORS、目录路径的运行时配置对象 | | `server/config/backends.json` | ACP Launcher 定义(command、args、protocol) | | `server/config/agents.json` | 静态 Agent 注册(可选) | ## 关键架构模式 ### ACP 协议四层结构 ``` Application (AgentManager) ↓ Protocol (ClaudeCliProtocol 直接 spawn / AcpProtocol JSON-RPC) ↓ Transport (ProcessTransport stdio / PipeTransport / WebSocketTransport) ↓ Launcher (ProcessLauncher spawn / NoneLauncher 无操作) ``` `runtime-resolver.js` 根据 `backends.json` + `agents.json` 配置自动推断使用哪一层。 **ClaudeCliProtocol 调用方式**: - 生成命令:`claude --input-format stream-json --output-format stream-json --verbose --include-partial-messages --dangerously-skip-permissions` - 输入格式:`{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}}` - 输出:NDJSON 事件流 - 单进程跨多轮复用(session reuse) **Mock 模式**:设置 `LEMONBOSS_CLAUDE_MODE=mock` 可读取 fixture 文件而不启动真实进程,用于调试。 ### Master → Sub-Agent 调度流程 1. 用户消息 → `MasterSession.handleMessage()` 2. `PromptBuilder` 组装系统提示(含工具描述 + 可用 Agent 列表) 3. Claude 响应中的工具调用用 `[TOOL_CALL: tool_name] {...json...} [/TOOL_CALL]` 格式标记 4. `ToolRegistry.parseToolCalls()` 解析后交给 `AgentManager.callAgent()` 5. `SessionPool` 按 `agentName:client:deploy:workDir[:scope]` 为 key 复用已有 session 6. 工具结果注回对话,最多循环 10 轮(`SAFETY_MAX_LOOPS=15`) **Orchestrator** 额外提供格式自动修正(`MAX_REPAIRS=5`)与结果验证(`ResultTracker`)。 ### Agent 发现与记忆 - Agent 优先从 `system/agents/` 目录发现(目录驱动),`agents.json` 作为静态补充 - 每个 Agent 的记忆存储在其目录下的 `memory/` 子目录,纯文件系统 - `AgentScaffold` 用于创建新 Agent 目录结构 ### 知识图谱(GraphStore) `server/db/GraphStore.js` 使用 `node:sqlite`(Node 22.5+ `DatabaseSync`),无需额外依赖。 - **节点类型**:Person、Organization、Project、Event、Asset - **边类型**:KNOWS、BELONGS_TO、PARTICIPATED、CONTAINS、MENTIONED_IN、ASSOCIATED - **分析状态**:`analysis_status` 字段跟踪节点是否已被 AI 分析(none / pending / done) - **Master 工具**:`server/src/tools/graph/` 提供 `create_person`、`get_person`、`link_persons`、`search_graph`、`get_relationship_path` 等工具供 AI 调用 ### 微信集成架构 `server/services/wechat/` 提供从 Mac 和 Pixel 双端点与微信交互的能力: - **发送消息(实际工作链路)**:`am broadcast com.hermes.wechat.SEND_MSG` → **`HermesAccessibilityService`**(lsposed-wechat-sendmsg 模块内)→ 启动微信 LauncherUI → 点搜索按钮 → 搜索 `toUserDisplayName`(**必须传 nickname**,如"火星人")→ 点搜索结果 → 找 `bkk` 容器内的 EditText 输入文本 → 点 `bql` 发送按钮。纯 UI 自动化,绕开所有反射 / hook 失败。 - **消息跟踪**:`android-wechat-tracker-sqlcipher.js` 直接轮询解密后的 `EnMicroMsg.db`(SQLCipher) - **自动回复**:`auto-reply.js` 结合 tracker + broadcast,在 Pixel 上独立运行 - **数据导入**:`wechat-importer.js` / `wechat-reader.js` 将联系人和聊天记录导入知识图谱 > [!WARNING] 关于其他两条已废弃的发送路径 > - **LSPosed 反射 API**(`HermesWeChatModule.sendTextMessageDirect`):监听同一广播但走 `u01.r1.a → m1.a()`。`Send result: true` 是**误报**——`builder.a()` 后主线程异步提交触发 null 异常被 swallow,消息不进 `message` 表。 > - **Frida daemon 队列**(`android-wechat-sender-frida.js` + `wechat_plain/scripts/frida-daemon/frida_daemon.js`):Frida 17 + 微信 8.0.72 注入大体积 bundle 触发反调试,微信进程崩溃,已废弃。 > 这两条路径的命名让人误解;真正能把消息推进 message 表的只有 A11y service。 > [!IMPORTANT] A11y service 启用方式 > `enabled_accessibility_services` 系统设置易被关闭。检查与重新启用: > ```bash > adb shell "settings get secure enabled_accessibility_services" > 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'" > ``` 详见 `server/services/wechat/README.md`。 ## lifelog-recorder(Pixel 8 Pro 24/7 录音 + 本地中文 ASR + 本地 LLM,v0.7) > 完整接手地图、踩坑清单、命令速查见 `lifelog-recorder/HANDOFF.md`。本节只勒住整个项目的**架构骨架 + 关键决策来源**,不重复细节。 ### 项目目标 把 Pixel 8 Pro 当成**永远开机的麦克风**:7×24 后台录音 → VAD 切段 → 中文 ASR 转文字 → 入库 → 按时间轴显示和搜索 → LLM 总结成纪要。最终目的是把所有「我说过 / 别人对我说过」的内容沉淀成可搜索、可回放的个人记忆库,下接 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 = 「我管理的人物」;微信通讯录是池子(勾选纳入);相册人脸标注/绑声纹自动入库 ``` **声纹纠偏 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「微信通讯录全部联系人」(池子,选人页里勾选纳入)。`vision/`(`FaceEngine`/`FaceMatcher`/`PhotoProcessor`/`PhotoScheduler`/`PhotoWorker`/`WeChatEnrollWorker`/`VisionClient`)做相册扫描 + 人脸聚类 + 微信头像锚点入库;微信头像配方与「锚点 embedding 必须端上算」见 memory `wechat-avatar-face-enrollment.md`。声纹绑人 ↔ 人脸标注 ↔ 微信联系人三者打通到同一 `Person`。 技术栈: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.7 已完成**: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**(删超 N 天 .opus,文字永留)——设置页尚无「存储用量 / 暂停时段」 - ⭐ **多 anchor 平均**(连选多段标同一人提高匹配率) - ⭐ **回链 lemonPCRM**(Transcript 实体 → GraphStore `MENTIONED_IN`,v2) ### 与 lemonPCRM 的关系 目前**完全独立**——lifelog-recorder 的 Room DB 跟 lemonPCRM 的 GraphStore 没打通。未来计划:把 Transcript / Summary 表里的实体(人名 / 地名 / 项目)抽到 GraphStore 当 `MENTIONED_IN` 边,让你的「某次会议说了什么」可以从图谱里反查。这是 v2 范畴。 ## 注意事项 - `server/src/standalone-entry.js` 的 `loadConfig()` 优先读 `backends.json` + `agents.json`,无需数据库。 - `prompt-builder.js` 的 `getSystemStateXML()` 只返回时间戳(公司/OKR 层已移除)。 - Agent 目录内放 `SKILL.md` 文件会绕过协议封装,直接作为系统提示使用。 - `server/index.js` 包含控制字符净化中间件:Agent 通过 curl 发送的 JSON body 可能含原始换行符,若解析失败会自动清理 `\x00-\x1F` 控制字符后重试。 > [!WARNING] (Agentic Self-Correction Loop Prevention) > **Master Agent 必读**:在 `Bash` tool 中使用含多重双引号的复杂命令(如 `curl -d '{"id":"lemonv"}'`)时,JSON 转义容易崩溃并陷入死循环。 > **解决方案**:任何需要提交 JSON Payload 的操作,先写入 `/tmp/*.json` 文件,再通过 `curl -d @/tmp/payload.json` 发送。