Files
zWorkFlow/main.py
btc-z 02bc9c1840 feat: 新增语音 MCP 工具 + macOS 密钥提取修复 (#53)
* feat: 新增语音 MCP 工具 + macOS 密钥提取修复

- 新增 get_voice_messages / decode_voice / transcribe_voice MCP 工具
  - 语音数据存储在 media_0.db VoiceInfo 表(SILK v3 格式)
  - decode_voice 解码为 WAV 文件(saved to decoded_voices/)
  - transcribe_voice 通过 Whisper 自动识别语言转录
- 新增 get_chat_history oldest_first 参数,支持从最早消息开始分页
- 修复 macOS 下 check_wechat_running / ensure_keys 逻辑
  - 改用 pgrep 检测微信进程,绕过不支持 macOS 的 Python 扫描器
  - 无 all_keys.json 时打印清晰引导,提示运行 C 版扫描器
- 新增 Makefile(build / keys / decrypt / web 快捷命令)
- .gitignore 补充 find_all_keys_macos 二进制和 decoded_voices/

* fix: 语音查询支持多分片 media DB + 文件名唯一化

解决 PR #53 review 的阻塞项 #1,顺手修 #3、#6。

#1 `_get_media_db_path()` 硬编码 `media_0.db`
  - 新增模块级 `MEDIA_DB_KEYS`,镜像 `MSG_DB_KEYS` 的分片发现逻辑
  - `_fetch_voice_row` 遍历所有分片,按 `(chat_name_id, local_id)`
    首个命中即返回;单条语音在 media DB 家族内唯一,命中即可停
  - `get_voice_messages` 从每个分片各取 `LIMIT limit`,合并排序后
    截断到 `limit`。选择"每分片取 limit 条再合并"而非"按
    max(create_time) 排序后逐个取到 limit 即停止":后者假设分片
    间时间不重叠,一旦 WeChat 改分片策略就会静默丢消息;前者工作
    量 O(N 分片 × limit),在任何分片布局下都正确

#3 输出文件名冲突
  - `_silk_to_wav` 增加 `local_id` 参数,输出 `{user}_{time}_{lid}.wav`,
    同一秒内两条语音不会互相覆盖;两个调用方都已在作用域内持有
    `local_id`

#6 `_fetch_voice_row` 的 `local_id=None` 死分支
  - 随 #1 的重写一并删除,`local_id` 改为必填位置参数

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor: macOS 密钥提取分层下沉到 find_all_keys.py

解决 PR #53 review 的阻塞项 #2。

review 里提到"跟 PR #51 冲突"实测不存在 —— PR #51 当前 0 文件改动
(fork 分支已与上游同步),但架构建议本身是对的:macOS 处理应集中
在 `find_all_keys.py`,而不是在 `main.py` 提前 return 截胡。

- `main.py:ensure_keys()` 移除 darwin 专属提前返回分支,macOS 走
  和其他平台相同的 `extract_keys()` 路径
- `find_all_keys.py:_load_impl()` 在 darwin 分支抛出带
  `sudo ./find_all_keys_macos` 操作指引的 RuntimeError;非 macOS
  的平台兜底分支保留
- `main.py` 里已有 `except RuntimeError` 会打印并 `sys.exit(1)`,
  用户可见行为不变

未来若有 PR 在 `find_all_keys.py` 加 macOS 自动编译 / dispatch,
直接替换这段 RuntimeError 即可,不再需要改 `main.py`。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: Makefile 支持 PYTHON 变量覆盖

解决 PR #53 review 的非阻塞项 #7。

原 Makefile 硬编码 `.venv/bin/python3`,没有 venv 的用户跑 `make
decrypt` 直接报错。引入 `PYTHON ?= .venv/bin/python3`:默认行为
不变(仍走 venv),想用系统 Python 的用户 `PYTHON=python3 make
decrypt` 即可。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: 回应 PR #53 review #4 — 澄清 silk-python 与 pysilk 包名关系

验证:本项目 import 的 `pysilk` 实际由 `pip install silk-python`
(synodriver/pysilk) 提供;pypi 上另有同名 `pysilk==0.0.1` 是无内容
的占位包,不可用。错误消息里 `pip install silk-python` 已经是对的,
但 reader 看到 `import pysilk` 仍会困惑,所以:

- `_silk_to_wav` 的 import 处加一行注释,点名所用的是
  synodriver 版本,并提醒 pypi 上还有 pilk / pysilk 两个同类包
- `decode_voice` / `transcribe_voice` 的 docstring 加 "依赖:" 行,
  明确 "pip install silk-python (import 名为 pysilk)",MCP 客户端
  读 tool 描述就能看到正确的安装命令

未新增 requirements.txt 条目:voice 支持是可选功能(tool 内
try/except ImportError 懒加载),保持非必需依赖的语义。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 14:10:22 +08:00

121 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
WeChat Decrypt 一键启动
python main.py # 提取密钥 + 启动 Web UI
python main.py decrypt # 提取密钥 + 解密全部数据库
"""
import json
import os
import platform
import sys
import subprocess
import functools
print = functools.partial(print, flush=True)
from key_utils import strip_key_metadata
def check_wechat_running():
"""检查微信是否在运行,返回 True/False"""
if platform.system().lower() == "darwin":
return subprocess.run(["pgrep", "-x", "WeChat"], capture_output=True).returncode == 0
from find_all_keys import get_pids
try:
get_pids()
return True
except RuntimeError:
return False
def ensure_keys(keys_file, db_dir):
"""确保密钥文件存在且匹配当前 db_dir否则重新提取"""
if os.path.exists(keys_file):
try:
with open(keys_file, encoding="utf-8") as f:
keys = json.load(f)
except (json.JSONDecodeError, ValueError):
keys = {}
# 检查密钥是否匹配当前 db_dir防止切换账号后误复用旧密钥
saved_dir = keys.pop("_db_dir", None)
if saved_dir and os.path.normcase(os.path.normpath(saved_dir)) != os.path.normcase(os.path.normpath(db_dir)):
print(f"[!] 密钥文件对应的目录已变更,需要重新提取")
print(f" 旧: {saved_dir}")
print(f" 新: {db_dir}")
keys = {}
keys = strip_key_metadata(keys)
if keys:
print(f"[+] 已有 {len(keys)} 个数据库密钥")
return
print("[*] 密钥文件不存在,正在从微信进程提取...")
print()
from find_all_keys import main as extract_keys
try:
extract_keys()
except RuntimeError as e:
print(f"\n[!] 密钥提取失败: {e}")
sys.exit(1)
print()
# 提取后再次检查
if not os.path.exists(keys_file):
print("[!] 密钥提取失败")
sys.exit(1)
try:
with open(keys_file, encoding="utf-8") as f:
keys = json.load(f)
except (json.JSONDecodeError, ValueError):
keys = {}
if not strip_key_metadata(keys):
print("[!] 未能提取到任何密钥")
print(" 可能原因:选择了错误的微信数据目录,或微信需要重启")
print(" 请检查 config.json 中的 db_dir 是否与当前登录的微信账号匹配")
sys.exit(1)
def main():
print("=" * 60)
print(" WeChat Decrypt")
print("=" * 60)
print()
# 1. 加载配置(自动检测 db_dir
from config import load_config
cfg = load_config()
# 2. 检查微信进程
if not check_wechat_running():
print(f"[!] 未检测到微信进程 ({cfg.get('wechat_process', 'WeChat')})")
print(" 请先启动微信并登录,然后重新运行")
sys.exit(1)
print("[+] 微信进程运行中")
# 3. 提取密钥
ensure_keys(cfg["keys_file"], cfg["db_dir"])
# 4. 根据子命令执行
cmd = sys.argv[1] if len(sys.argv) > 1 else "web"
if cmd == "decrypt":
print("[*] 开始解密全部数据库...")
print()
from decrypt_db import main as decrypt_all
decrypt_all()
elif cmd == "web":
print("[*] 启动 Web UI...")
print()
from monitor_web import main as start_web
start_web()
else:
print(f"[!] 未知命令: {cmd}")
print()
print("用法:")
print(" python main.py 启动实时消息监听 (Web UI)")
print(" python main.py decrypt 解密全部数据库到 decrypted/")
sys.exit(1)
if __name__ == "__main__":
main()