* 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>
121 lines
3.6 KiB
Python
121 lines
3.6 KiB
Python
"""
|
||
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()
|