From c149389174f8373d5ce3083cb2bfd8fc774b361b Mon Sep 17 00:00:00 2001 From: zlab Date: Wed, 22 Apr 2026 22:32:02 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=AF=AD=E9=9F=B3=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=A4=9A=E5=88=86=E7=89=87=20media=20DB=20+?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E5=90=8D=E5=94=AF=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解决 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 --- mcp_server.py | 78 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/mcp_server.py b/mcp_server.py index 9851d22..7e500b0 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -1744,8 +1744,21 @@ def get_chat_images(chat_name: str, limit: int = 20) -> str: DECODED_VOICE_DIR = os.path.join(SCRIPT_DIR, "decoded_voices") -def _get_media_db_path(): - return _cache.get("message/media_0.db") +# media DB 与 message DB 同样会分片(media_0.db、media_1.db…), +# 每个分片各有独立的 Name2Id / VoiceInfo 表。 +MEDIA_DB_KEYS = sorted([ + k for k in ALL_KEYS + if any(v.startswith("message/") for v in key_path_variants(k)) + and any(re.search(r"media_\d+\.db$", v) for v in key_path_variants(k)) +]) + + +def _iter_media_db_paths(): + for rel_key in MEDIA_DB_KEYS: + path = _cache.get(rel_key) + if path: + yield path + def _get_chat_name_id(conn, username): row = conn.execute( @@ -1754,32 +1767,31 @@ def _get_chat_name_id(conn, username): return row[0] if row else None -def _fetch_voice_row(username, local_id=None): - """Query VoiceInfo from media_0.db. Returns (voice_data, create_time) or None.""" - media_db = _get_media_db_path() - if not media_db: - return None - with closing(sqlite3.connect(media_db)) as conn: - chat_name_id = _get_chat_name_id(conn, username) - if chat_name_id is None: - return None - if local_id is not None: - return conn.execute( +def _fetch_voice_row(username, local_id): + """遍历所有 media DB 分片,返回 (voice_data, create_time);找不到返回 None。""" + for media_db in _iter_media_db_paths(): + with closing(sqlite3.connect(media_db)) as conn: + chat_name_id = _get_chat_name_id(conn, username) + if chat_name_id is None: + continue + row = conn.execute( "SELECT voice_data, create_time FROM VoiceInfo " "WHERE chat_name_id = ? AND local_id = ?", (chat_name_id, local_id), ).fetchone() - return None + if row: + return row + return None -def _silk_to_wav(voice_data, create_time, username): +def _silk_to_wav(voice_data, create_time, username, local_id): """Decode SILK voice blob to WAV file, return output path.""" import pysilk data = bytes(voice_data) silk_data = data[1:] if data[0] == 0x02 else data os.makedirs(DECODED_VOICE_DIR, exist_ok=True) time_str = datetime.fromtimestamp(create_time).strftime('%Y%m%d_%H%M%S') - out_path = os.path.join(DECODED_VOICE_DIR, f"{username}_{time_str}.wav") + out_path = os.path.join(DECODED_VOICE_DIR, f"{username}_{time_str}_{local_id}.wav") inp = io.BytesIO(silk_data) out = io.BytesIO() pysilk.decode(inp, out, 24000) @@ -1809,24 +1821,28 @@ def get_voice_messages(chat_name: str, limit: int = 20) -> str: names = get_contact_names() display_name = names.get(username, username) - media_db = _get_media_db_path() - if not media_db: - return "找不到 media_0.db" + if not MEDIA_DB_KEYS: + return "找不到 media DB" - with closing(sqlite3.connect(media_db)) as conn: - chat_name_id = _get_chat_name_id(conn, username) - if chat_name_id is None: - return f"{display_name} 无语音消息" - - rows = conn.execute( - "SELECT local_id, create_time, length(voice_data) FROM VoiceInfo " - "WHERE chat_name_id = ? ORDER BY create_time DESC LIMIT ?", - (chat_name_id, limit), - ).fetchall() + # 从每个分片各取最多 limit 条后合并再截断:分片若有时间重叠也不会漏最新消息 + rows = [] + for media_db in _iter_media_db_paths(): + with closing(sqlite3.connect(media_db)) as conn: + chat_name_id = _get_chat_name_id(conn, username) + if chat_name_id is None: + continue + rows.extend(conn.execute( + "SELECT local_id, create_time, length(voice_data) FROM VoiceInfo " + "WHERE chat_name_id = ? ORDER BY create_time DESC LIMIT ?", + (chat_name_id, limit), + ).fetchall()) if not rows: return f"{display_name} 无语音消息" + rows.sort(key=lambda r: r[1], reverse=True) + rows = rows[:limit] + lines = [] for local_id, create_time, size in rows: time_str = datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M') @@ -1860,7 +1876,7 @@ def decode_voice(chat_name: str, local_id: int) -> str: return f"找不到 local_id={local_id} 的语音消息" voice_data, create_time = row - out_path, pcm_len = _silk_to_wav(voice_data, create_time, username) + out_path, pcm_len = _silk_to_wav(voice_data, create_time, username, local_id) duration_s = pcm_len / (24000 * 2) return ( f"解码成功!\n" @@ -1909,7 +1925,7 @@ def transcribe_voice(chat_name: str, local_id: int) -> str: return f"找不到 local_id={local_id} 的语音消息" voice_data, create_time = row - wav_path, _ = _silk_to_wav(voice_data, create_time, username) + wav_path, _ = _silk_to_wav(voice_data, create_time, username, local_id) model = _get_whisper_model() result = model.transcribe(wav_path)