适用版本:
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.目录结构速览
WeFlow/
├── electron/ # 主进程
│ ├── main.ts # 入口:窗口/托盘/IPC 注册/自动更新 (4795 行)
│ ├── preload.ts # contextBridge API (686 行)
│ ├── wcdbWorker.ts # 数据库 Worker(消息协议分发)
│ ├── exportWorker.ts # 导出 Worker
│ ├── annualReportWorker.ts / dualReportWorker.ts # 报告 Worker
│ ├── transcribeWorker.ts # 语音识别 Worker
│ ├── imageDecryptWorker.ts / imageSearchWorker.ts # 图片链路 Worker
│ ├── apiMessageWorker.ts # HTTP API 消息映射 Worker
│ ├── windows/ # 通知玻璃窗口等子窗口
│ ├── services/ # ~50 个业务服务 (chat/sns/export/key/...)
│ │ └── export/ # 导出编排 + 8 种 Formatter
│ └── utils/ # LRUCache、pathUtils
├── src/ # 渲染进程 (React)
│ ├── pages/ # 30+ 路由页面(Chat 51 万行的大文件)
│ ├── stores/ # Zustand store
│ ├── services/ # ipc.ts / config.ts / cloudControl.ts
│ ├── components/ # 通用组件 + LiquidGlass 玻璃特效
│ └── types/ # 领域模型(models.ts 等)
├── resources/ # 原生二进制:wcdb / wedecrypt / key / welive
│ ├── wcdb/{win32,macos,linux}/$arch # WCDB C API 数据服务
│ ├── wedecrypt/… # .dat 图片解密 native addon
│ ├── key/… # 密钥抓取 dll(含 mac source)
│ └── welive/… # Rust 实现的离线导出器
├── shared/groupSummaryPrompt.json # 前后端共享 Prompt
├── scripts/ # after-pack / prepare-electron-runtime
└── vite.config.ts # 10 个 Electron 构建入口
2. 构建体系:Vite 多入口 + Electron
WeFlow 用 vite-plugin-electron 把主进程、preload 和 8 个 worker全部纳入一个 Vite 构建图,各自输出独立 bundle:
// vite.config.ts(节选)
export default defineConfig({
plugins: [
react(),
electron([
{ entry: 'electron/main.ts', onstart: handleElectronOnStart, ... },
{ entry: 'electron/wcdbWorker.ts', output: { entryFileNames: 'wcdbWorker.js' } },
{ entry: 'electron/exportWorker.ts',
plugins: [exportWorkerElectronShimPlugin()], ... },
{ entry: 'electron/preload.ts', ... },
// annualReportWorker / dualReportWorker / transcribeWorker /
// imageDecryptWorker / imageSearchWorker / apiMessageWorker ...
])
],
resolve: { alias: { '@': resolve(__dirname, 'src') } }
})
三个关键工程手段:
原生模块 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'。构建时一个自定义插件注入替换:
// vite.config.ts — exportWorkerElectronShimPlugin
const next = code
.replace(/from\s+(['"])electron\1/g, `from '${virtualId}'`) // import … from 'electron'
.replace(/require\s*\(\s*(['"])electron\1\s*\)/g, `require('${virtualId}')`)
虚拟模块 virtual:weflow-export-worker-electron 内部返回一个纯 Node 实现的假 app(getPath 由 WEFLOW_USER_DATA_PATH 环境变量驱动、safeStorage 直通),并将 BrowserWindow.getAllWindows 等替换为 no-op:
// 生成的虚拟模块(示意)
export const app = {
isPackaged: Boolean(process.resourcesPath && ...),
getPath: (name) => name === 'userData'
? workerUserDataPath() || join(appDataPath(), 'WeFlow')
: ...,
on: () => app, // Worker 中不存在 will-quit 等 App 生命周期事件
}
这使同一份 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 是典型代表:
// electron/services/wcdbService.ts — 通用 RPC 客户端
private callWorker<T>(type: string, payload: any = {}): Promise<T> {
if (!this.worker) this.initWorker()
if (!this.worker) return Promise.reject(new Error('WCDB Worker 不可用'))
return new Promise((resolve, reject) => {
const id = ++this.messageId
this.pending.set(id, { resolve, reject })
this.worker!.postMessage({ id, type, payload })
})
}
// Worker 端按 type 分发
case 'getMessages':
result = await core.getMessages(payload.sessionId, payload.limit, payload.offset)
break
case 'startMonitor':
core.setMonitor((type, json) => parentPort!.postMessage({
id: -1, type: 'monitor', payload: { type, json } }))
break
三点容错设计:
- 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…)。典型模式:
// 主进程:注册
ipcMain.handle('chat:getMessages',
async (_, sessionId, offset, limit, startTime, endTime, ascending) =>
chatService.getMessages(sessionId, offset, limit, startTime, endTime, ascending))
// preload:转发
chat: {
getMessages: (sessionId, offset, limit, startTime, endTime, ascending, cursor) =>
ipcRenderer.invoke('chat:getMessages', sessionId, offset, limit, startTime, endTime, ascending, cursor)
}
// 渲染进程:再包一层薄 JS 门面
export const chat = { getMessages: (...) => window.electronAPI.chat.getMessages(...) }
渲染端所有窗口统一 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 库处理。
// electron/services/wcdbCore.ts — FFI 绑定(节选)
this.wcdbInit = this.lib.func('int32 wcdb_init()')
this.wcdbOpenAccount = this.lib.func('... wcdb_open_account(...)')
this.wcdbGetSessions = this.lib.func('... wcdb_get_sessions(...)') // 会话列表
this.wcdbGetMessages = this.lib.func('... wcdb_get_messages(...)') // 消息分页
this.wcdbStartMonitorPipe = this.lib.func('int32 wcdb_start_monitor_pipe()') // 文件监听
this.wcdbGetVoiceData = this.lib.func(
'int32 wcdb_get_voice_data(int64 handle, const char* sessionId, int32 createTime, int32 localId, int64 svrId, const char* candidatesJson, _Out_ void** outHex)')
this.wcdbInstallMessageAntiRevokeTrigger = this.lib.func(...) // 防撤回(SQL 触发器)
4.2 会话/消息域 (chatService.ts ≈ 12,800 行)
关键机制:
- 游标分页(cursor pagination):
openMessageCursor → fetchMessageBatch → closeMessageCursor。向上/向下翻页各自持有 cursor,切换会话时trimMessageCursorStates回收;15 秒强制重开冷却,以规避长游标失效。 - 跨库去重:微信按年/按会话分库(
message_*.db)导致localId / serverId不保证全局唯一。渲染端chatStore为每条消息构造多个 alias key:
// src/stores/chatStore.ts — 关键去重逻辑
function buildMessageAliasKeys(message: Message): string[] {
const sourceScope = String(message._db_path || '').trim() // 来源分库
const keys = [buildPrimaryMessageKey(message, sourceScope)]
if (localId > 0) {
if (sourceScope) keys.push(`lid:${sourceScope}:${localId}`) // 跨 message_*.db 时 local_id 可能重复
else keys.push(`lid_fallback:${localId}:${createTime}:${sender}:…`) // 保守组合防误去重
}
if (serverId > 0) {
if (sourceScope) keys.push(`sid:${sourceScope}:${serverId}`)
else keys.push(`sid_fallback:${serverId}:${...}`)
}
return keys
}
- 多级内存缓存:
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” 窗口,枚举子窗口最后一判稳态(防扫码页误抓):
// electron/services/keyService.ts — 窗口就绪启发式
private hasReadyComponents(children) {
const readyTexts = ['聊天', '登录', '账号']
const readyClassMarkers = ['WeChat', 'Weixin', 'TXGuiFoundation', 'Qt5', 'ChatList', 'MainWnd', ...]
// 满足「标题/类名」命中数、子窗数量 ≥14、类名总数 ≥3 等任意启发式即判"已登录"
}
以及登录态探测:识别“扫码/二维码/请在手机上确认”截断输入,提示用户先完成登录。60s 超时内找不到进程或密钥则返回失败 + 日志。
macOS 走 keyServiceMac.ts(lldb 附加进程读内存 + 板级 candidate 搜索),Linux 走 keyServiceLinux.ts(@vscode/sudo-prompt 提权用 grep 在微信进程内存里搜密钥特征)。
5.2 图片密钥:deriveImageKeys 逆向推导
微信 4.0 的图片密钥不是独立存储,而是从「码 code × wxid」确定性推导,并用真实密文验证:
// electron/services/keyService.ts
private deriveImageKeys(code: number, wxid: string): { xorKey: number; aesKey: string } {
const cleanedWxid = this.cleanWxid(wxid)
const xorKey = code & 0xFF // XOR 密钥 = code 低 8 位
const md5Full = crypto.createHash('md5')
.update(code.toString() + cleanedWxid)
.digest('hex')
const aesKey = md5Full.substring(0, 16) // AES-128 key = MD5 前 16 字符
return { xorKey, aesKey }
}
private verifyDerivedAesKey(aesKey: string, ciphertext: Buffer): boolean {
const decipher = crypto.createDecipheriv('aes-128-ecb',
Buffer.from(aesKey, 'ascii').subarray(0, 16), null)
// 对 16 字节已知密文解密,解成功且产生合法明文 → 验证通过
}
这套推导配对来源:
- 缓存法(
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 回调获取原始密钥流:
// electron/services/wasmService.ts
mockGlobal.wasm_isaac_generate = (ptr: number, size: number) => {
const buffer = new Uint8Array(mockGlobal.Module.HEAPU8.buffer, ptr, size)
this.capturedKeystream = new Uint8Array(buffer) // 拷贝 WASM 线性内存
}
public async getKeystream(key: string, size = 131072): Promise<Buffer> {
const alignSize = Math.ceil(size / 8) * 8 // ISAAC-64 是 8 字节块,必须对齐
const buffer = await this.getRawKeystream(key, alignSize)
const reversed = new Uint8Array(buffer)
reversed.reverse() // 微信实现按"倒序读流"
return Buffer.from(reversed).subarray(0, size)
}
倒序对齐是逆向工程得出的隐式约定: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 侧只负责路径定位与缓存:
// electron/services/nativeImageDecrypt.ts
function addonCandidates() { // 按平台/arch/asar unpack 位置多路尝试
roots = [cwd/resources/wedecrypt/$plat/$arch, process.resourcesPath/…]
}
export function decryptDatViaNative(
inputPath: string, xorKey: number, aesKey?: string
): { data: Buffer; ext: string; isWxgf: boolean; meta: NativeDatMeta } | null {
const addon = loadAddon()
...
const result = addon.decryptDatNative(inputPath, xorKey, aesKey)
...
}
主进程用单独 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:
// electron/services/voiceTranscribeService.ts
const SENSEVOICE_MODEL = {
model: 'model.int8.onnx',
tokens: 'tokens.txt',
model: 'https://modelscope.cn/models/pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue/resolve/master/model.int8.onnx',
...
}
- Worker 内做富文本后处理: SenseVoice 输出的标签(
<|HAPPY|>,<|SAD|>,<|BGM|>,<|Laughter|>…)通过映射表转 Emoji,技术标签(<|itn|>,<|zh|>…)剥离:
// electron/transcribeWorker.ts
const RICH_TAG_MAP = {
'<|HAPPY|>': '😊', '<|SAD|>': '😔', '<|BGM|>': '🎵', '<|Laughter|>': '😂', ...
}
function richTranscribePostProcess(text: string): string {
let processed = text
for (const [tag, replacement] of Object.entries(RICH_TAG_MAP)) {
processed = processed.replace(new RegExp(tag.replace(/[|<>]/g, '\\$&'), 'gi'), replacement)
}
for (const tag of TECH_TAGS) { processed = processed.replace(..., '') }
return processed.replace(/\s+/g, ' ').trim()
}
7. 实时链路:数据库监听 → 推送 → 通知
这是 WeFlow 与静态“截图导出工具”的核心差异——它是准实时应用。
实现要点(wcdbCore.ts):
- 生产者在 C++ 数据服务内,而不是 Node 的
fs.watch:主进程只连一个管道(\.\pipe\weflow_monitor_<pid>),避免多窗口多进程重复监听; - 自动重连:
connectMonitorPipe里 socketclose时scheduleReconnect循环重试; - 归一化拆包:macOS 侧分隔符可能是
\0或相邻 JSON(} {),统一归一化拆行:
const normalizedChunk = rawChunk
.replace(/\u0000/g, '\n')
.replace(/}\s*{/g, '}\n{')
buffer += normalizedChunk
const lines = buffer.split(/\r?\n/)
buffer = lines.pop() || ''
- 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(...):
// electron/services/export/core/ExportOrchestrator.ts
export class ExportOrchestrator {
constructor(public context: ExportContext) {}
async exportSessionToChatLab(sessionId, outputPath, options, onProgress, control) {
const formatter = new ChatLabFormatter(this.context)
return formatter.export(sessionId, outputPath, options, onProgress, control)
}
async exportSessionToExcel(...) { return new ExcelFormatter(this.context).export(...) }
async exportSessionToHtml(...) { return new HtmlFormatter(this.context).export(...) }
// Markdown / Json / Sql / Txt / WeCloneCsv 同结构
}
8.2 Worker 级进度与暂停协议
exportWorker.ts 用批量缓冲限制 postMessage 频率(避免几十万条小消息击穿 IPC):
const CREATED_PATH_FLUSH_INTERVAL_MS = 200
const CREATED_PATH_BATCH_LIMIT = 256
const PROGRESS_POST_INTERVAL_MS = 180
function queueCreatedFile(filePath: string) {
queuedCreatedFiles.push(normalized)
if (queuedCreatedFiles.length + queuedCreatedDirs.length >= CREATED_PATH_BATCH_LIMIT) flush()
else scheduleCreatedPathFlush()
}
父进程侧同样支持 pause / resume / cancel(exportTaskControlService),通过向 Worker post 一条控制消息,Worker 在每个消息循环边界检查 controlState.stopRequested。
8.3 两套导出引擎并存
除了内置 TypeScript 引擎(ExportOrchestrator),也支持把整个导出外包给 Rust 侧的 welive 可执行体(resources/welive/<platform>/<arch>/welive[.exe]),通过子进程 JSON 事件流进度协议:
// electron/services/weliveBridge.ts — welive 子进程 JSON 事件协议
export type WeliveExportEvent =
| { type: 'ready'; total?: number; output_dir?: string }
| { type: 'progress'; phase?: string; current?: number; total?: number; ... }
| { type: 'created_file'; path?: string; session_id?: string }
| { type: 'session_error'; session_id?: string; error?: string }
| { type: 'result'; success?: boolean; success_count?: number; ... }
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:
// electron/services/httpService.ts — 端点映射 /health /api/v1/*
if (pathname === '/health' || pathname === '/api/v1/health') handleHealth()
else if (pathname === '/api/v1/push/messages') handleSse(res)
else if (pathname === '/api/v1/messages') handleMessages(req, res)
else if (pathname === '/api/v1/sessions') handleSessions(...)
else if (pathname.startsWith('/api/v1/sessions/') ...) // ChatLab Pull
else if (pathname === '/api/v1/contacts') handleContacts(...)
else if (pathname === '/api/v1/group-members') handleGroupMembers(...)
else if (pathname === '/api/v1/sns/timeline' ...) handleSns*(...)
else if (pathname.startsWith('/api/v1/media/')) handleMedia(...)
亮点:
- 启动即预热数据库 + markdown 映射线程池,避免首批大请求因原生库冷缓存整页丢消息:
this.server.listen(this.port, this.host, () => {
void this.ensureDbReady().catch(...) // 预热 wcdb
try { this.getApiMapperPool().warmup() } catch {} // 预热映射 worker 池
this.startMessagePushHeartbeat()
})
- 消息映射线程池:
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:
// 全部页面懒加载:主窗口首屏只解析 App 壳 + HomePage;
const ChatPage = lazy(() => import('./pages/ChatPage'))
const SnsPage = lazy(() => import('./pages/SnsPage'))
const NotificationWindow = lazy(() => import('./pages/NotificationWindow'))
// 独立窗口(可能是多 BrowserWindow 单 SPA)
const isNotificationWindow = location.pathname === '/notification-window'
const isAnnualReportWindow = location.pathname === '/annual-report/view'
一个关键架构模式:单 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 + 组件拆分:
src/pages/Export/
├── ExportPage.tsx # 顶层编排
├── components/
│ ├── ExportDialog/index.tsx # 导出弹窗
│ ├── SessionTable/index.tsx # 会话表格 (react-virtuoso 虚拟滚动)
│ ├── TaskCenter/index.tsx # 后台任务中心
│ └── Automation/… # 定时导出
├── hooks/ # useExportConfig / useExportSessions / useAutomation…
├── utils/ # avatar.ts / format.ts / performance.ts
└── constants.ts
其中 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处理位移映射。
// electron/main.ts
app.commandLine.appendSwitch('webrtc-max-cpu-consumption-percentage', '100')
// 但回退采集只在通知展示的数秒内运行且分辨率已降为逻辑尺寸
渲染层会向主进程上报卡片实测几何(notification:glassRect)+ 亮度带回调(notification:luma),原生层据此动态调整折射参数,使通知卡片在亮/暗桌面下都能保持可读性(useNotificationAdaptiveTheme.ts、systemNotificationService)。
13. 打包、分发与更新
13.1 electron-builder 多平台资源映射
mac: extraResources: [ wcdb/macos/universal, wedecrypt/macos/$arch, key/macos/universal, welive/macos/$arch ]
win: extraFiles: [ MSVC 运行时 dll ]
extraResources: [ wcdb/win32/$arch, wedecrypt/win32/$arch, key/win32, welive/win32/$arch ]
linux: extraResources: [ wcdb/linux/$arch, wedecrypt/linux/x64, key/linux, welive/linux/$arch ]
asarUnpack: [ silk-wasm, sherpa-onnx-*, ffmpeg-static, electron-liquid-glass, wedecrypt/**/*.node ]
关键挑战:平台 × 架构 × 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 自动更新
// electron/main.ts — 更新通道推断
const inferUpdateTrackFromVersion = (version) => {
if (/^0\.(\d{2})\.(\d+)$/.test(normalized)) return 'preview' // 0.YY.N
if (/^\d{2}(\.\d{1,2}){2}$/.test(normalized)) return 'dev' // YY.M.D
if (/(alpha|beta|rc)/i.test(version)) return 'dev'
return 'stable'
}
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.disableDifferentialDownload = true // 禁差分,强制全量
发布源是 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),行号与字节大小会随版本漂移;引用的代码片段均为原文摘录并做删节,以提升可读性。