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:
100
decrypt_db.py
100
decrypt_db.py
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user