* 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>
`v2_decrypt_file` previously wrote files to disk even when the keys were
wrong, producing garbage output with no way for the caller to detect the
failure:
1. Wrong AES key -> `detect_image_format` returns 'bin' (magic does not
match any known format) -> a `.bin` file of random bytes was written.
2. Wrong XOR key with correct AES key -> file header looks like a valid
jpg/png (the AES segment decrypts correctly) but the trailing XOR
segment is scrambled -> callers get a half-valid image file that
image viewers either render as truncated or fail to open.
Both cases now return `(None, None)`:
- `fmt == 'bin'` -> fail fast, no file written.
- `xor_size >= 2` -> validate trailer magic by format:
* jpg must end with FF D9 (EOI marker)
* png must contain IEND chunk in the last 12 bytes
Other formats (gif/bmp/tif/webp/hevc/wxgf) lack a mandatory trailer
signature, so they skip the check to avoid false rejection.
Also fixes a latent bug in `test_decode_image_v1_no_aes_key_uses_fixed_key`:
the test built the synthetic .dat with `TEST_XOR_KEY=0x37` but constructed
`ImageResolver` without `xor_key=`, defaulting to `0x88`. The XOR segment
was always scrambled — the test passed because the AES segment alone was
enough for `detect_image_format` to return 'png' from the header, and no
trailer validation existed to catch the corruption. The new trailer check
surfaces this, so the test now passes `xor_key=TEST_XOR_KEY` explicitly.
Tests: 5 new cases (wrong AES key / wrong XOR for jpg / wrong XOR for
png / xor_size=0 bypass / wxgf bypass). All 156 existing tests still pass.