fix(image): scope local_id lookup by chat_id + use real column name (#82)
* fix(image): scope local_id lookup by chat_id + use real column name `ImageResolver.get_image_md5` made two wrong assumptions about the production `MessageResourceInfo` schema, which made the decode_image MCP tool always fail with "无法找到 local_id=X 的图片信息": 1. The column is `message_local_id`, not `local_id`. The current query throws `sqlite3.OperationalError: no such column: local_id`, but the exception is swallowed by `except Exception: pass`, masking the real failure as a silent miss. 2. `message_local_id` is not globally unique. In production it repeats across chats, and within an active chat the same local_id can recur up to 7 times (observed on a real DB). The production schema scopes by `chat_id`, resolved from `ChatName2Id.rowid WHERE user_name = ?`. Fix: `get_image_md5` now takes `(username, local_id)`: - Resolve `username -> chat_id` via `ChatName2Id`. - Query `MessageResourceInfo` filtered by `chat_id + message_local_id + message_local_type == 3` (image type; high bits are session flags, so use `% 2^32`), ordered by `message_create_time DESC LIMIT 1`. External callers (`mcp_server.decode_image_tool` / `list_chat_images_tool`) already pass `username` through `ImageResolver.decode_image()` / `list_chat_images()`, so the public API is unchanged. Only the internal helper signature shifts. The existing test fixture in `test_decode_image_v2` used the same wrong schema as the buggy code (`CREATE TABLE MessageResourceInfo (local_id INTEGER PRIMARY KEY, packed_info BLOB)`), so the tests passed against a self-consistent fiction. The fixture is rebuilt to match real columns plus `ChatName2Id`, and three regression tests are added: - cross-chat collision (same local_id in 3 chats; must pick the right one and not the type=43 video row) - same-chat reuse (same local_id, two timestamps; must pick the newer) - unknown chat (username not in ChatName2Id; structured error, no crash) All 154 tests pass locally (151 baseline + 3 new). * fix(image): surface get_image_md5 errors and use read-only DB open Two follow-ups on top of the chat_id scoping fix: 1. The bare `except Exception: pass` was the original failure mode: it silently swallowed `OperationalError: no such column: local_id` when the production schema diverged from the old `local_id` column name, masking the bug this PR fixes. Print the exception to stderr so future schema drift surfaces immediately instead of returning a misleading "image not found" error. 2. Open message_resource.db with `file:...?mode=ro` URI to match the rest of the project (monitor_web.py uses this idiom in 9 places). The DB is read-only for our purposes and a running WeChat may still hold it; using URI ro avoids any chance of lock contention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -335,22 +335,42 @@ class ImageResolver:
|
||||
self.aes_key = aes_key
|
||||
self.xor_key = xor_key
|
||||
|
||||
def get_image_md5(self, local_id):
|
||||
"""通过 local_id 查 message_resource.db 获取图片文件 MD5"""
|
||||
def get_image_md5(self, username, local_id):
|
||||
"""通过 (username, local_id) 查 message_resource.db 获取图片 MD5
|
||||
|
||||
message_local_id 在 MessageResourceInfo 中跨 chat 重复 (不全局唯一),
|
||||
必须用 chat_id 缩小范围;同一 chat 内活跃聊天也会复用 local_id
|
||||
(实测最高同 chat 7 条同 local_id 的记录), 默认取最新一条。
|
||||
|
||||
message_local_type 上 32 bit 是版本/会话 flag, 用 % 2^32 取低位匹配
|
||||
图片类型 3, 同 monitor_web.py 里 push 路径的写法。
|
||||
"""
|
||||
path = self.cache.get("message/message_resource.db")
|
||||
if not path:
|
||||
return None
|
||||
|
||||
conn = sqlite3.connect(path)
|
||||
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
||||
try:
|
||||
chat_row = conn.execute(
|
||||
"SELECT rowid FROM ChatName2Id WHERE user_name = ?",
|
||||
(username,)
|
||||
).fetchone()
|
||||
if not chat_row:
|
||||
return None
|
||||
chat_id = chat_row[0]
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT packed_info FROM MessageResourceInfo WHERE local_id = ?",
|
||||
(local_id,)
|
||||
"SELECT packed_info FROM MessageResourceInfo "
|
||||
"WHERE chat_id = ? AND message_local_id = ? "
|
||||
"AND (message_local_type = 3 OR message_local_type % 4294967296 = 3) "
|
||||
"ORDER BY message_create_time DESC LIMIT 1",
|
||||
(chat_id, local_id)
|
||||
).fetchone()
|
||||
if row and row[0]:
|
||||
return extract_md5_from_packed_info(row[0])
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[get_image_md5] {type(e).__name__}: {e}",
|
||||
file=sys.stderr, flush=True)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -381,10 +401,10 @@ class ImageResolver:
|
||||
Returns:
|
||||
dict with keys: success, path, format, md5, error
|
||||
"""
|
||||
# 1. 获取 MD5
|
||||
file_md5 = self.get_image_md5(local_id)
|
||||
# 1. 获取 MD5 (chat-scoped: 同 local_id 跨 chat 重复)
|
||||
file_md5 = self.get_image_md5(username, local_id)
|
||||
if not file_md5:
|
||||
return {'success': False, 'error': f'无法从 message_resource.db 找到 local_id={local_id} 的图片信息'}
|
||||
return {'success': False, 'error': f'无法从 message_resource.db 找到 {username} local_id={local_id} 的图片信息'}
|
||||
|
||||
# 2. 找 .dat 文件
|
||||
dat_files = self.find_dat_files(username, file_md5)
|
||||
@@ -448,7 +468,7 @@ class ImageResolver:
|
||||
|
||||
results = []
|
||||
for local_id, create_time in rows:
|
||||
file_md5 = self.get_image_md5(local_id)
|
||||
file_md5 = self.get_image_md5(username, local_id)
|
||||
info = {
|
||||
'local_id': local_id,
|
||||
'create_time': create_time,
|
||||
|
||||
Reference in New Issue
Block a user