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)。
常用命令
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 调度流程
- 用户消息 →
MasterSession.handleMessage() PromptBuilder组装系统提示(含工具描述 + 可用 Agent 列表)- Claude 响应中的工具调用用
[TOOL_CALL: tool_name] {...json...} [/TOOL_CALL]格式标记 ToolRegistry.parseToolCalls()解析后交给AgentManager.callAgent()SessionPool按agentName:client:deploy:workDir[:scope]为 key 复用已有 session- 工具结果注回对话,最多循环 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系统设置易被关闭。检查与重新启用: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/<ts>/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/<startTs>/{audio.pcm, manifest.json}(speech-only PCM + run 偏移) |
BlockBuffer.close() |
Channel | BlockProcessor.process() |
块目录(关块条件:静音≥5min 或 块≥10min) |
DiarizationEngine.diarize() |
sync | — | List<Turn(startSec, endSec, speaker)>(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 任何配置都 ,连续录会越积越多。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 元,延迟
关键坑(接手必读)
- 不要把 .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 加载。 - silero_vad.onnx 别从 snakers4 的 GitHub raw 拉:会被 LFS 重定向成 HTML。用
https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx。 - Tensor G3 paraformer 线程甜点 = 4,不是 8。
- 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。 keys.propertiesgitignored:讯飞凭证从这里读到BuildConfig.XFYUN_*。模板keys.properties.example。- 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 Master Agent 必读:在
Bashtool 中使用含多重双引号的复杂命令(如curl -d '{"id":"lemonv"}')时,JSON 转义容易崩溃并陷入死循环。 解决方案:任何需要提交 JSON Payload 的操作,先写入/tmp/*.json文件,再通过curl -d @/tmp/payload.json发送。