## Problem
In `--web` mode, messages longer than ~90 chars get truncated in the
SSE feed. `SessionMonitor.check_updates()` pushes `SessionTable.summary`
to clients, but `summary` is WeChat's own ~80-char preview kept for the
client-side chat list — not the full message body.
## Fix
`_lookup_latest_local_id` already hits `Msg_<md5(username)>` for the
row at `(username, create_time)` to obtain `local_id` for #79's
dedup. Extend the same query to also return `message_content` and
`WCDB_CT_message_content`, and use it to replace `summary` when the
DB body is longer:
SELECT local_id, message_content, WCDB_CT_message_content
FROM [Msg_<md5>]
WHERE create_time = ?
ORDER BY local_id DESC LIMIT 1
Same row, same query — zero additional IO vs. the prior `MAX(local_id)`.
Renamed to `_lookup_latest_message` to reflect the new
`(local_id, content)` return shape. zstd handling and `wxid_xxx:\n`
group-prefix stripping mirror the existing `SessionTable.summary`
logic in `check_updates`, so the SSE `content` field stays in the same
format clients already render.
Replacement is conservative — only swaps in `full_content` when
strictly longer than `summary`. This never shortens existing behavior
and degrades cleanly if the message-DB write hasn't landed yet (the
SessionTable-vs-message_N.db timing race that #79 already documented).
## Scope
- `monitor_web.py`: one helper extended + one call site adjusted. No
schema change, no new dependency, no client/UI change.
- `_check_hidden_messages` cold path is untouched — its same-second
multi-message coverage still runs as before.
Add structured parsing for transfer messages so they no longer fall
through to the generic `[链接/文件]` fallback in chat history exports.
Mirrors the dispatch + helper pattern PR #65 (merged-forward type=19)
established for `base_type=49` appmsg sub-types.
## What is added
**Helpers (mcp_server.py):**
- `_TRANSFER_PAYSUBTYPE_LABEL` — maps the 6 community-consensus paysubtypes
(1 发起 / 3 已收款 / 4 已退还 / 5 过期已退还 / 7 待领取 / 8 已领取);
unknown values degrade to `未知(paysubtype=N)` so a new variant in a
future WeChat build is visible rather than silently dropped.
- `_extract_transfer_info(appmsg)` — pulls fields out of `<wcpayinfo>`,
with snake/camelCase fallback (`feedesc`/`feeDesc`, `pay_memo`/`paymemo`)
observed across WeChat versions.
- `_format_transfer_message_text(appmsg, title)` — one-line render
for chat history: `[转账·已收款] ¥100.00 备注: lunch`.
**Dispatch (mcp_server.py):**
- `_format_app_message_text` gains an `app_type == 2000` branch that
routes to `_format_transfer_message_text`. `get_chat_history`,
`export_chat`, `export_all_chats` and `monitor_web` all inherit
automatically.
**New MCP tool (mcp_server.py):**
- `decode_transfer(chat_name, local_id, create_time=0)` — full
structured view: direction, amount, memo, payer/receiver wxid,
transfer id, transcation id, begin/invalid timestamps. Uses the
same multi-shard scan + ambiguity-by-create_time pattern as
`decode_file_message` / `decode_record_item`.
**CLI wrapper:**
- `decode_transfer.py` at the repo root — argparse wrapper that prints
the same text as the MCP tool, returning non-zero exit when the
message can't be decoded (script-friendly).
**JSON export (chat_export_helpers.py + export_chat.py + export_all_chats.py):**
- `_extract_content` now returns `(rendered, extras)`. `extras` carries
structured fields when a message type has more signal than the
human-readable string (currently: transfers → `type:"transfer" +
transfer:{direction, fee_desc, pay_memo, ...}`). The channel is
forward-compatible — future additions (video号 metadata, expanded
merged-forward, etc.) flow through the same shape without changing
the caller signature. JSON consumers that only read `content` are
unaffected; the change is additive.
**monitor_web (monitor_web.py):**
- Backend dispatch branch + orange-yellow `.msg-transfer` card CSS +
`renderRich` JS handler.
## Tests
12 new cases in `tests/test_record_decoders.py`:
- `TransferPaysubTypeLabelTests` — locks the 6-value label table.
- `ExtractTransferInfoTests` (6 cases) — full field round-trip, missing
`<wcpayinfo>` fallback, snake/camelCase variants, unknown paysubtype
degradation, empty paysubtype handling.
- `FormatTransferMessageTextTests` (4 cases) — initiate / received-with-memo /
missing-wcpayinfo / missing-fee-desc.
- `AppMessageDispatchTransferTests` — `_format_app_message_text` routes
type=2000 correctly so `get_chat_history` / `export_chat` both pick
it up.
All fixtures use synthetic placeholder values (`wxid_payer_synth`,
`¥100.00`, `1` + 27×`0`); no real PII or transaction IDs.
## Scope
7 files, +546 / -15 (additions only — no behavior change for existing
message types). All 180 tests pass locally (168 baseline + 12 new).
- Add Linux memory scanner (`find_all_keys_linux.py`) using `/proc/<pid>/mem`,
same approach as Windows/macOS — no GDB, no function offsets, no restart needed
- Extract Windows-specific code to `find_all_keys_windows.py`
- Make `find_all_keys.py` a platform dispatcher (Windows / Linux)
- Add `key_utils.py` for cross-platform path matching (`/` vs `\` in all_keys.json)
- Update `config.py` with Linux auto-detection of db_storage paths
- Update all consumers (decrypt_db, monitor, monitor_web, mcp_server) to use
`get_key_info()` for platform-agnostic key lookup
Tested on remote Linux container: 15/15 DBs scanned, decrypted, and verified.
Support all three .dat encryption formats:
- Old XOR format: single-byte XOR, auto-detect key from magic bytes
- V1 format: AES-ECB with fixed key (md5("0")[:16]) + XOR tail
- V2 format (2025-08+): AES-128-ECB + raw middle + XOR tail
New files:
- decode_image.py: unified image decryption module (XOR/V1/V2)
- find_image_key.py: extract AES key from WeChat process memory
- find_image_key_monitor.py: continuous monitoring version for key capture
monitor_web.py changes:
- Inline image preview in Web UI with async decryption
- MonitorDBCache for mtime-based DB decryption caching
- username-to-DB mapping for image resolution chain
- /img/ endpoint for serving decoded images
- SSE image_update events for real-time preview updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>