适用版本:
package.jsonv5.0.0 · Electron 43 · React 19 · TypeScript 6 · Vite 8WeFlow 是一个完全本地的微信(4.0+)聊天记录实时查看 / 分析 / 导出工具。它不经过任何服务器——所有数据读取、解密、分析均在用户本机完成。本文试图从整体架构、进程模型、数据链路、密钥体系、导出管线、AI 能力到工程化构建,完整拆解其原理。
目录
- 1. 项目定位与总体架构
- 2. 构建体系:Vite 多入口 + Electron
- 3. 进程与线程模型
- 4. 数据层:WCDB 原生数据服务
- 5. 密钥体系:从数据库密钥到图片密钥
- 6. 媒体解密管线
- 7. 实时链路:数据库监听 → 推送 → 通知
- 8. 导出子系统
- 9. AI 能力层
- 10. HTTP API 与对外集成
- 11. 渲染端(React)架构
- 12. 可视化与桌面特效
- 13. 打包、分发与更新
- 14. 关键设计取舍总结
1. 项目定位与总体架构
1.1 顶层模块图
1.目录结构速览
2. 构建体系:Vite 多入口 + Electron
WeFlow 用 vite-plugin-electron 把主进程、preload 和 8 个 worker全部纳入一个 Vite 构建图,各自输出独立 bundle:
三个关键工程手段:
原生模块 external 化。
koffi(FFI)、better-sqlite3、silk-wasm、[@hicccc77/electron-liquid-glass] 均声明为 external,运行时经asarUnpack从app.asar.unpacked解析,规避 Electron 打包后 ABI 变化。Worker 内的 Electron shim(虚拟模块)。导出 Worker 复用了主进程的服务代码(如
ConfigService),但这些代码import 'electron'。构建时一个自定义插件注入替换:
虚拟模块 virtual:weflow-export-worker-electron 内部返回一个纯 Node 实现的假 app(getPath 由 WEFLOW_USER_DATA_PATH 环境变量驱动、safeStorage 直通),并将 BrowserWindow.getAllWindows 等替换为 no-op:
这使同一份 Service 代码可以同时跑在主进程和 Worker,无需为 Worker 复制一套"纯 Node 配置"。
- postinstall 运行时准备。
scripts/prepare-electron-runtime.cjs在安装后拉起electron-builder install-app-deps,并准备 Webview 运行时;after-pack.cjs负责在 macOS 上用otool+install_name_tool改写libwcdb_api.dylib对WCDB.framework的链接(@rpath→@loader_path),保证免签名 AppImage/DMG 也能加载。
3. 进程与线程模型
3.1 进程拓扑
为什么大量使用 Worker 线程?
chat:getMessages等大查询、报告聚合、导出(可能产出数千个文件,秒级完成)都涉及重 CPU / 重 IO;- 主进程若阻塞,窗口失去响应(Electron 主进程是单线程事件循环);
- Worker 线程比新开
utilityProcess进程更轻,且通过postMessage传递 Buffer 零拷贝。
3.2 Worker RPC 协议
所有 Worker 都走同一个 4 字段消息协议 { id, type, payload } → { id, result | error }。wcdbService.ts 是典型代表:
三点容错设计:
- exit ≠ 0:Worker 异常退出时 reject 全部 pending Promise,并给出用户可读提示(例如 VC++ Redistributable 缺失);
id = -1保留给主动 push 消息(如数据库监听回调),与请求-响应消息区分;- Worker 崩溃自愈:下一次调用
callWorker时initWorker()会自动重建 Worker,并重放setPaths / setLogEnabled / setMonitor状态。
3.3 IPC 桥(preload)
preload.ts 通过 contextBridge.exposeInMainWorld('electronAPI', …) 暴露约 35 个命名空间(config / chat / sns / insight / export / backup / analytics / http / image / window / auth / …),主进程对应位置有 195 个 ipcMain.handle(chat 45、sns 18、insight 13、window 12、group 10…)。典型模式:
渲染端所有窗口统一 contextIsolation: true, nodeIntegration: false;webSecurity: false 只用于需要本地视频回放的主窗口。
4. 数据层:WCDB 原生数据服务
4.1 关键思想:不依赖 Node 侧的 SQL 引擎
WeFlow 不自己实现 SQLite 解密,而是加载微信生态同源的 WCDB C API 动态库(resources/wcdb/<平台>/<arch>/libwcdb_api),由 Node 通过 koffi(FFI)调用。这样 SQL 密钥解密、SQLCipher 变体、schema 差异全部由微信同款 WCDB 库处理。
4.2 会话/消息域 (chatService.ts ≈ 12,800 行)
关键机制:
- 游标分页(cursor pagination):
openMessageCursor → fetchMessageBatch → closeMessageCursor。向上/向下翻页各自持有 cursor,切换会话时trimMessageCursorStates回收;15 秒强制重开冷却,以规避长游标失效。 - 跨库去重:微信按年/按会话分库(
message_*.db)导致localId / serverId不保证全局唯一。渲染端chatStore为每条消息构造多个 alias key:
- 多级内存缓存:
displayNameCache(TTL 10 min,上限 20000)、avatarUrlCache、hardlinkCache(消息→物理图片路径的硬链接解析)、mediaStreamPageCache(30s TTL + 请求合并inflight去重)都在WcdbCore内实现。
5. 密钥体系:从数据库密钥到图片密钥
WeFlow 要解开三层加密:数据库 SQLCipher、表情/图片 .dat、视频 ISAAC-64。
5.1 Windows 数据库密钥:Hook 微信进程
KeyService(Windows)通过 koffi 加载 resources/key/win32/<arch>/wx_key.dll(一个未经混淆的注入 DLL),步骤:
关键实现细节——等待微信主界面就绪:EnumWindows 找 “微信 / WeChat” 窗口,枚举子窗口最后一判稳态(防扫码页误抓):
以及登录态探测:识别“扫码/二维码/请在手机上确认”截断输入,提示用户先完成登录。60s 超时内找不到进程或密钥则返回失败 + 日志。
macOS 走 keyServiceMac.ts(lldb 附加进程读内存 + 板级 candidate 搜索),Linux 走 keyServiceLinux.ts(@vscode/sudo-prompt 提权用 grep 在微信进程内存里搜密钥特征)。
5.2 图片密钥:deriveImageKeys 逆向推导
微信 4.0 的图片密钥不是独立存储,而是从「码 code × wxid」确定性推导,并用真实密文验证:
这套推导配对来源:
- 缓存法(
autoGetImageKey):微信将code存入账号目录模板文件,读取code后枚举候选 wxid 组合推导 + 密文验证; - 内存扫描(
autoGetImageKeyByMemoryScan):读取一张「已知原图对应密文」的截图对,暴力求 XOR & AES。
推导出的 { xorKey, aesKey } 进 ConfigService,用户可在设置页手动覆写。
5.3 视频密钥:ISAAC-64 + WASM 复现
视频用 ISAAC-64 PRNG 生成的密钥流做流加密(WxIsaac64)。WeFlow 用两条路径复现:
- 纯 TS 实现
electron/services/isaac64.ts(BigInt 位运算); - 微信自带的 WASM 模块(
electron/assets/wasm/wasm_video_decode.{wasm,js}),通过vm.createContext隔离执行,钩住其 Emscripten 回调获取原始密钥流:
倒序对齐是逆向工程得出的隐式约定:ISAAC-64 输出顺序与微信加密 writer 写盘顺序相反,只有 8 字节对齐 + 整流翻转才能得到与原密文一致的字节流。
6. 媒体解密管线
6.1 图片 .dat
.dat 文件有三种组织:单字节 XOR、AES-128-ECB(16 字节密钥)、XOR 与 AES 混合(wxgf 格式)。WeFlow 通过 native addon wedecrypt(decryptDatNative(inputPath, xorKey, aesKey))一次调用完成读盘 → 解密 → 识别真实格式 → 返回 Buffer + 扩展名;Node 侧只负责路径定位与缓存:
主进程用单独 decryptWorker(imageDecryptWorker.ts)执行此调用,避免同步 FFI 卡住事件循环;失败时回退主进程同步路径。上层 imageDecryptService(≈ 2700 行)维护:
resolvedCache(messageKey → 本地解密文件路径,12k 条),配合imagePreloadService在滚动时批量预热(队列优先级 high / normal / low);datNameScanMissAt:负缓存,短 TTL 阻止同一.dat名反复全盘扫;- 去重 in-flight map:同一
sessionId+imageMd5+datName只解一次,第二个等待者直接复用 Promise。
6.2 视频(分段 + 关键流派生)
微信视频按“流号 + shard”分块写入 Disk,视频文件 md5 也存在数据库。videoService.ts(25k 字节)描述了:客户端请求视频元数据 → 找到 video*.db 中该文件的所有 shard → 用 ISAAC-64 依次解密 → ffmpeg(ffmpeg-static)拼接 → 返回本地 mp4。实况照片(live photo)表示为 图 .jpg + .mov 配对,前端用 LivePhotoIcon + 双流同步播放。
6.3 语音(silk-wasm + 本地 ASR)
- 语音原始字节用 WCDB C API
wcdb_get_voice_data直接取(分批版为wcdb_get_voice_data_batch); - silk-wasm 解码 silk → PCM,写入 WAV Buffer;
transcribeWorker加载 SenseVoice ONNX 模型(sherpa-onnx-node),下载源默认 ModelScope:
- Worker 内做富文本后处理: SenseVoice 输出的标签(
<|HAPPY|>,<|SAD|>,<|BGM|>,<|Laughter|>…)通过映射表转 Emoji,技术标签(<|itn|>,<|zh|>…)剥离:
7. 实时链路:数据库监听 → 推送 → 通知
这是 WeFlow 与静态“截图导出工具”的核心差异——它是准实时应用。
实现要点(wcdbCore.ts):
- 生产者在 C++ 数据服务内,而不是 Node 的
fs.watch:主进程只连一个管道(\.\pipe\weflow_monitor_<pid>),避免多窗口多进程重复监听; - 自动重连:
connectMonitorPipe里 socketclose时scheduleReconnect循环重试; - 归一化拆包:macOS 侧分隔符可能是
\0或相邻 JSON(} {),统一归一化拆行:
- messagePushService(55k 字节):增量引擎维护
sessionBaseline(lastTimestamp+unreadCount)和双 Map(recentMessageKeys去重 TTL 10 min);350ms 防抖合并突发消息;撤回检测支持“最近 150 秒原始 token 回扫”,将被撤回的消息还原并发出message.revoke事件,配合 SQL 触发器(wcdbInstallMessageAntiRevokeTrigger)实现消息防撤回。
8. 导出子系统
导出是本项目的另一大块(约占后端代码 30%),架构上是经典的编排器 + 策略模式,但被推到 Worker 线程里执行。
8.1 结构
ExportOrchestrator 是一个策略分发薄层,8 个 exportSessionToXxx 全部 delegate 至对应 Formatter.export(...):
8.2 Worker 级进度与暂停协议
exportWorker.ts 用批量缓冲限制 postMessage 频率(避免几十万条小消息击穿 IPC):
父进程侧同样支持 pause / resume / cancel(exportTaskControlService),通过向 Worker post 一条控制消息,Worker 在每个消息循环边界检查 controlState.stopRequested。
8.3 两套导出引擎并存
除了内置 TypeScript 引擎(ExportOrchestrator),也支持把整个导出外包给 Rust 侧的 welive 可执行体(resources/welive/<platform>/<arch>/welive[.exe]),通过子进程 JSON 事件流进度协议:
exportWorker.ts 首行即 runWeliveExport(...) —— 根据任务配置选择引擎。这是很聪明的做法:Rust 二进制不依赖 Node ABI,能原生并行读盘 + 解密;TS 引擎则更灵活、可自定义 formatter。
8.4 版权/隐私配套 —— 备份与自动化
backupService.ts:微信消息库快照打包(weflow-db-snapshots),把每张表写成.wfsnap,用 zip 组装,上层可做备份/恢复;- 导出自动化(定时任务):渲染端
useAutomation.ts在全球首日 0 点/间隔 N 天/ 30s 调度器内运行定时导出,配置放在exportAutomationTaskMap,保证不访问导出页时也能继续(App.tsx特殊 mount 逻辑)。
9. AI 能力层
WeFlow 的 AI 能力完全本地调度 + 还是用户自带 LLM API(不内置 OpenAI key),核心服务三件套:
| 服务 | 内容 |
|---|---|
insightService.ts | "我的足迹"复盘、AI 洞察推送。触发频率、冷却、名单过滤本地决策;拉取真实聊天上下文(须用户授权)后组装 prompt 调单一 AI 模型 |
groupSummaryService.ts | 群聊画像摘要。系统提示词放在 shared/groupSummaryPrompt.json 供前后端复用,允许用户覆写 |
annualReportService.ts / dualReportService.ts | 年报 / 双人报告。聚合统计由 wcdbGetAnnualReportStats / wcdbGetDualReportStats 原生库计算,Worker 里异步计算再回传 |
groupSummaryRecordService | 摘要记录持久化(topic / trigger / log) |
调用 OpenAI 兼容接口(aiModelApiBaseUrl/aiModelApiKey/aiModelApiModel),端点由 buildApiUrl(apiBaseUrl, '/chat/completions') 拼接,Authorization: Bearer <apiKey>,main 进程直接 fetch,不把秘密放进渲染进程。
此外还支持 mimo 模型特判、wcdbCloudInit/CloudReport/CloudStop(匿名统计上报开关,用户显式同意后生效),属于“发送前征求意见”的隐私设计。
10. HTTP API 与对外集成
httpService.ts(98k 字节)是一个手写的极简 Node http server(不用 Express),端口默认 5031,绑定 127.0.0.1:
亮点:
- 启动即预热数据库 + markdown 映射线程池,避免首批大请求因原生库冷缓存整页丢消息:
- 消息映射线程池:
apiMessageMapperPool把「行 → Message」解码丢到按核数起的 Worker 并行,阈值 300 行以上启用,失败回退主线程。 - SSE 推送
GET /api/v1/push/messages:长连接 + 事件名message.new / message.revoke;重连后通过messagePushReplayBuffer(TTL 10min,上限 1000 条)回放错过事件,用event + rawid去重。 - 鉴权 Token:支持
Authorization: Bearer、?access_token=、JSON body 三种方式,健康检查除外。
11. 渲染端(React)架构
11.1 页面与路由
src/App.tsx(816 行)用 React Router + 全量 lazy:
一个关键架构模式:单 SPA 多 BrowserWindow。所有子窗口(通知 / 视频播放 / 年报查看 / 图片查看 / 协议等)都是同一 index.html 不同 query,靠 location.pathname 分支判断渲染哪个页面。好处是共享同一份 preload/bundle 资源;坏处是页面文件必须尽量小、懒加载拆 chunk。
Export 模块按需挂载(exportMounted state):即使未访问导出页,存在启用的自动化任务时也会挂载(30 秒调度器在导出页内),这种“路由触发 + 条件挂载”的设计减少了内存驻留。
11.2 状态管理
9 个 Zustand store 之上,chatStore 起到跨库去重器的作用(见 §4.2)。渲染端配置全部经 src/services/config.ts,一个 2,363 行的"配置 API 门面"集中导出 300+ CONFIG_KEYS(安全、通知、AI、导出预设、备份、HTTP API 等)。
11.3 超大页面的拆分
最大的四个页面(ChatPage.tsx 515k 字节、SettingsPage.tsx 247k、SnsPage.tsx 141k、ResourcesPage.tsx 113k)都遵循hook + utils + 组件拆分:
其中 react-virtuoso 处理百万级消息流,echarts(-for-react) 处理分析/报告的可视化,react-markdown + remark-gfm 用于 AI 洞察内容渲染,html2canvas 用于报告导出为图片,jszip 处理备份压缩包,jieba-wasm 分词支撑搜索/词云。
12. 可视化与桌面特效
WeFlow 有一套完整的“玻璃质感”自研特效栈(src/components/LiquidGlass/*,含 glassStreamRenderer.ts(WebGL 流渲染)与 glassMotionEstimator.ts(运动估计决定折射强度)),背后是 @hicccc77/electron-liquid-glass 原生 addon(主进程侧)。
Windows 的系统通知走双轨渲染:
- 主路径:原生面板(Windows秋天
Acrylic/NativePanel),主进程用notificationWindow.ts的GlassPanel绘制毛玻璃卡片; - 回退路径:Chromium 采集桌面流 +
webrtc-max-cpu-consumption-percentage=100打开采集频率上限,渲染端LiquidGlass处理位移映射。
渲染层会向主进程上报卡片实测几何(notification:glassRect)+ 亮度带回调(notification:luma),原生层据此动态调整折射参数,使通知卡片在亮/暗桌面下都能保持可读性(useNotificationAdaptiveTheme.ts、systemNotificationService)。
13. 打包、分发与更新
13.1 electron-builder 多平台资源映射
关键挑战:平台 × 架构 × asar 打包三层矩阵。方案是:
- 原生 addon 全走
asarUnpack,路径解析封装在addonCandidates()/resolveWorkerPath()/resolveWeliveExecutable()三个工具函数中; after-pack.cjs在 macOS 特殊处理.dylib链接(install_name_tool -change @rpath/WCDB.framework/... @loader_path/libWCDB.dylib);- Windows 附带
msvcp140.dll等运行时,避免用户环境缺 VC++。
13.2 自动更新
发布源是 GitHub Releases(publish: { provider: 'github' })。UI 端有 UpdateDialog(弹窗)与 UpdateProgressCapsule(下载胶囊),版本号决定默认通道(stable / preview / dev),且区分 Win x64 / arm64 避免清单互踩。
14. 关键设计取舍总结
| 维度 | 决策 | 动机 |
|---|---|---|
| 数据读取 | 加载微信同款 WCDB C API + koffi FFI,而非自研解密 SQLCipher | 与微信 4.0+ Schema 变化同步成本最低,密钥兼容性最好 |
| 计算隔离 | 9 个 worker_threads Worker 而非 utilityProcess | 线程级隔离已足够,节省进程开销;Worker 崩溃可自动重启 |
| 实时性 | 数据库监听放在 C++ 数据服务内,主进程只连管道 | 避免多窗口重复监听占用 JS 线程 |
| 跨库一致性 | 消息主键 + 多别名 key 去重(lid/sid + _db_path 作用域) | 微信分库使 serverId/localId 不全局唯一,误合并会丢消息 |
| 密钥安全 | 密钥抓取全部走独立原生 DLL(wx_key.dll、wedecrypt、welive),主进程仅编排 | 隔离 OS API 注入逻辑,便于跨平台/多 arch 分发 & 维护;核心算法可定期演进 |
| 导出 | 策略模式 + 8 Formatter,同一 Worker 线程池 + 可选 Rust welive 引擎并跑 | 既保持纯 TS 的可扩展性,又用 Rust 拿到磁盘吞吐上限 |
| AI | 无内置 Key;本地调度 + 用户自带 OpenAI 兼容端点;prompt 放 shared/ | 服务器零痕迹;prompt 双端复用 |
| 内存 | 页面全 lazy、Export 模块按需 mount、大页面 hook 化拆分 | 单 SPA / 多窗口模式是内存敏感的(消息多时 JS 堆容易膨胀) |
| 桌面体验 | 原生 GlassPanel(Addon)为主 + Chromium 采集为回退 | Windows 原生混合面板能耗远低于系统性持续屏幕采集 |
| 可靠性 | 监听管道自动重连、Worker 自动重建、增量 basline + 撤回回扫、负缓存 | 解决 WCDB 文件监视器掉线、并发导出暂停/取消等故障场景 |
值得学习的技术点
- 把"逆向 obtained" 的三套密钥算法统一到一个确定性
derive+ 校验模型:任何 code/wxid 候选组合通过真实密文验证才落地,杜绝误推导。 - 一份服务代码,主进程与 Worker 复用:通过 Vite 虚拟模块替换单个
import 'electron',让 Backend 服务天然在两种运行环境中工作。 - Worker RPC + 双工 callback 的 4 字段消息协议:巧用
id=-1保留 "主动推送" 语义,代码极短。 - SSE replay buffer + TTL + 事件去重:使外部消费者简单地在订阅重启后不丢关键事件。
webSecurity:false只放在确实需要本地视频文件的窗口,而不是全局——细粒度的安全权衡。
局限与改进空间
- 代码里存在千行级的大文件(
chatService.ts≈ 12.8k 行、ChatPage.tsx≈ 515k 字节),服务之间通过全局单例耦合,后续切模块/拆包成本会高; - 缺少自动化测试体系(无
testscript),回归主要靠手工; detail层面的 i18n / a11y 基本缺席(installerLanguages仅 zhCN / enUS);- 密钥注入式方案本质上依赖微信未加固的运行内存——微信升级后
keyService*属于最脆弱、最先失效的链路。
本文档基于仓库源码静态分析生成(2026-09),行号与字节大小会随版本漂移;引用的代码片段均为原文摘录并做删节,以提升可读性。