feat(export): add incremental mode, date range filter, and dry-run

Three new flags for export_all_chats.py:

- -i / --incremental: reads existing JSON, appends only new messages
  (deduplicates by local_id, preserves transcription field on merge)
- --start / --end: filter messages by date range (YYYY-MM-DD or timestamp)
  passes start_ts/end_ts directly to mcp_server._query_messages
- --dry-run: preview mode (shows counts without writing files)

Voice transcription in incremental mode only processes newly appended
voice messages — existing transcribed entries are untouched.
This commit is contained in:
Davy
2026-05-12 18:42:29 -07:00
committed by ylytdeng
parent e20682c3dd
commit de4cb092d9
2 changed files with 263 additions and 65 deletions

View File

@@ -7,8 +7,9 @@ WeChat 4.0 数据库解密器
"""
import hashlib, struct, os, sys, json
import hmac as hmac_mod
from Crypto.Cipher import AES
from Crypto.Cipher import AES
import argparse
import functools
print = functools.partial(print, flush=True)
@@ -20,12 +21,12 @@ HMAC_SZ = 64
RESERVE_SZ = 80 # IV(16) + HMAC(64)
SQLITE_HDR = b'SQLite format 3\x00'
from config import load_config
from key_utils import get_key_info, strip_key_metadata
_cfg = load_config()
DB_DIR = _cfg["db_dir"]
OUT_DIR = _cfg["decrypted_dir"]
KEYS_FILE = _cfg["keys_file"]
from config import load_config
from key_utils import get_key_info, strip_key_metadata
_cfg = load_config()
DB_DIR = _cfg["db_dir"]
OUT_DIR = _cfg["decrypted_dir"]
KEYS_FILE = _cfg["keys_file"]
def derive_mac_key(enc_key, salt):
@@ -106,23 +107,41 @@ def decrypt_database(db_path, out_path, enc_key):
def main():
parser = argparse.ArgumentParser(
description="WeChat 4.0 数据库解密器"
)
parser.add_argument(
"-i", "--incremental",
action="store_true",
help="增量模式:仅当源 .db 更新于已解密文件时才重新解密",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="预览模式:显示将要解密的数据库列表",
)
args = parser.parse_args()
print("=" * 60)
print(" WeChat 4.0 数据库解密器")
print("=" * 60)
# 加载密钥
if not os.path.exists(KEYS_FILE):
print(f"[ERROR] 密钥文件不存在: {KEYS_FILE}")
print("请先运行 python main.py decrypt 提取密钥并解密")
print(f"[ERROR] 密钥文件不存在: {KEYS_FILE}")
print("请先运行 python main.py decrypt 提取密钥并解密")
sys.exit(1)
with open(KEYS_FILE, encoding="utf-8") as f:
keys = json.load(f)
keys = strip_key_metadata(keys)
print(f"\n加载 {len(keys)} 个数据库密钥")
print(f"输出目录: {OUT_DIR}")
os.makedirs(OUT_DIR, exist_ok=True)
with open(KEYS_FILE, encoding="utf-8") as f:
keys = json.load(f)
keys = strip_key_metadata(keys)
print(f"\n加载 {len(keys)} 个数据库密钥")
print(f"输出目录: {OUT_DIR}")
if args.incremental:
print(f"模式: 增量 (跳过未变更的数据库)")
os.makedirs(OUT_DIR, exist_ok=True)
# 收集所有DB文件
db_files = []
@@ -141,20 +160,41 @@ def main():
success = 0
failed = 0
skipped = 0
skipped_unmodified = 0
total_bytes = 0
for rel, path, sz in db_files:
key_info = get_key_info(keys, rel)
if not key_info:
print(f"SKIP: {rel} (无密钥,如已安装微信补丁可能需要重新运行密钥提取)")
skipped += 1
continue
enc_key = bytes.fromhex(key_info["enc_key"])
out_path = os.path.join(OUT_DIR, rel)
for rel, path, sz in db_files:
key_info = get_key_info(keys, rel)
if not key_info:
print(f"SKIP: {rel} (无密钥,如已安装微信补丁可能需要重新运行密钥提取)")
skipped += 1
continue
print(f"解密: {rel} ({sz/1024/1024:.1f}MB) ...", end=" ")
out_path = os.path.join(OUT_DIR, rel)
# 增量模式:检查 mtime
if args.incremental and os.path.exists(out_path):
src_mtime = os.path.getmtime(path)
dst_mtime = os.path.getmtime(out_path)
if src_mtime <= dst_mtime:
skipped_unmodified += 1
if args.dry_run:
print(f"SKIP: {rel} (未修改)")
continue
elif args.dry_run:
print(f"NEW: {rel} (源较新)")
elif not args.dry_run:
print(f"更新: {rel} ({sz/1024/1024:.1f}MB) ...", end=" ")
elif args.dry_run:
print(f"NEW: {rel} ({sz/1024/1024:.1f}MB)")
else:
print(f"解密: {rel} ({sz/1024/1024:.1f}MB) ...", end=" ")
if args.dry_run:
skipped_unmodified += 1
continue
enc_key = bytes.fromhex(key_info["enc_key"])
ok = decrypt_database(path, out_path, enc_key)
if ok:
# SQLite验证
@@ -186,8 +226,14 @@ def main():
except OSError:
pass
if args.dry_run:
print(f"\n{'='*60}")
print(f"预览: 需要解密 {skipped_unmodified} 个数据库")
return
print(f"\n{'='*60}")
print(f"结果: {success} 成功, {failed} 失败, {skipped} 跳过(无密钥), 共 {len(db_files)}")
inc_note = f" (跳过 {skipped_unmodified} 个未变更)" if skipped_unmodified else ""
print(f"结果: {success} 成功, {failed} 失败, {skipped} 跳过(无密钥){inc_note}, 共 {len(db_files)}")
print(f"解密数据量: {total_bytes/1024/1024/1024:.1f}GB")
print(f"解密文件在: {OUT_DIR}")