feat: 解析微信引用回复消息 (appmsg type=57) + 新增 decode_refer MCP 工具
> 高价值改动 rationale (override 路径)
>
> 引用回复 (appmsg type=57) 是聊天里第 3 高频的消息类型 (仅次于纯文本和
> 图片)。当前 _format_app_message_text 的 type=57 分支直接把 refermsg/
> content 按 [:160] 截断当摘要,对内层 type=3 (图片) / 34 (语音) /
> 43 (视频) / 47 (动画表情) / 49 (嵌套卡片) 这些"二进制"被引用消息,
> 会把 cdnurl / aeskey / md5 / cdnthumb / voiceurl / externurl 一坨乱码
> 渲染到 LLM 可见的 chat history,严重污染上下文。issue #44 #45 重复反馈
> 一个月无人接 —— 这是个明确的用户痛点,fork 实测覆盖 5 种内层 type 的真
> 实数据,渲染长度从原本几千字降到 21-58 字。改动较大但 review 风险低:
> 替换的就是 19 行 inline 截断逻辑,新加的 helpers / decode_refer 都是
> 纯加,不动现有 API。
\`_format_app_message_text\` 当前 type=57 分支用 19 行 inline 逻辑直接
\`refer.findtext('content')[:160]\` 当摘要。这对 type=1 (文本) 工作正常,
但对其他内层 type 是个隐藏的 bug:
- type=3 图片: 渲染 \`<msg><img cdnthumburl="…" aeskey="…" md5="…" cdnurl="…" />\` 截断
- type=34 语音: 渲染 \`<voicemsg voicelength="…" voiceurl="…" />\` 截断
- type=43 视频: 渲染 \`<videomsg cdnvideourl="…" cdnthumburl="…" />\` 截断
- type=47 动画表情: 渲染 \`<emoji md5="…" externurl="…" />\` 截断
- type=49 嵌套卡片: 渲染外层 escape 后的 XML 字符串截断
后果: cdnurl / aeskey / md5 / voiceurl / externurl 等二进制元数据泄漏到 LLM
可见的聊天历史,污染上下文且无信息量。引用回复是 type=57 是高频消息,影响面大。
按 refer_type 分发 schema-aware 摘要:
1. **新增三组 helpers (mcp_server.py +135 行,纯加)**:
- \`_REFER_INNER_TYPE_LABEL\`: 内层 type → 中文标签 (1 文本 / 3 图片 / 34 语音 / ...)
- \`_INNER_APPMSG_TYPE_LABEL\`: refer_type=49 时嵌套 appmsg/type → 标签 (5 链接 / 6 文件 / 19 聊天记录 / ...)
- \`_extract_refer_info(appmsg)\`: 提取 refermsg 全字段返回 dict
- \`_summarize_refer_content(refer_type, content)\`: 按 type 分支
- type=1: 取原文,截断到 max_len
- type=3/34/43/47/...: 给标签,**不**展开 cdnurl/aeskey/md5
- type=49: 走 \`_parse_xml_root\` (经 \`_XML_UNSAFE_RE\` 过滤 DOCTYPE/ENTITY 防 XXE) 解一层 inner appmsg, 给 \`[链接] xxx\`
- 未识别 type: 给 \`[type=N]\` 兜底
- \`_format_refer_message_text(appmsg, ...)\`: 渲染两行格式
\`<回复正文>\n ↳ 回复 <对方>: <摘要>\`
2. **\`_format_app_message_text\` 的 type=57 分支简化**: 19 行 inline → 3 行 dispatch 到 helper。
3. **新增 MCP 工具 \`decode_refer(chat_name, local_id, create_time=0)\`**: 输出结构化多行文本 (回复正文 / 被引用发送者 / 类型 / 摘要 / svrid / createtime), 错误文案分别指引 \`decode_file_message\` (type=6) / \`decode_record_item\` (type=19) / \`decode_transfer\` (type=2000), 不让用户在 4 个工具间盲猜。
新文件 \`tests/test_refer_message.py\`, 20 个新测试:
- \`ReferInnerTypeLabelTests\` (2): 标签映射 spot-check
- \`ExtractReferInfoTests\` (2): 全字段提取 / refermsg 缺失返回 None
- \`SummarizeReferContentTests\` (11): 5 种 refer_type 标签 / type=1 文本截断 / type=49 嵌套链接卡 / type=49 聊天记录卡 / type=49 invalid XML 退化 / unknown type 兜底 / 空 content / XXE payload 拒绝
- \`FormatReferMessageTextTests\` (4): 1v1 文本引用渲染 / 图片引用不泄漏 PII (cdnurl/aeskey/md5) / refermsg 缺失退回 title / 空 reply 用占位符
- \`AppMessageDispatchReferTests\` (1): dispatcher 走新 helper 不走旧截断
合成 fixture (wxid_synth_a/b, 12345@chatroom, Sender A/B, svrid 1+0\*18), 无真实 PII。
基线 183 → 203 通过 (+20 新增), 0 回归。
- \`mcp_server.py\`: 替换 19 行 type=57 inline → 3 行 dispatch (净 -16 行); 新增 6 个 helpers + 1 个 MCP 工具 \`decode_refer\` (+275 行); 不改任何现有公开 API。
- \`tests/test_refer_message.py\`: 新增 (20 测试, 合成 fixture, 不依赖真实加密素材)。
- **本 PR 不包含 fork 里的 CLI 入口 (\`wxdec.cli.decode_refer\`) 和 \`export_chat\` / \`monitor_web\` 的对应改动** —— 那几处依赖 fork 私有的包结构 (\`wxdec/cli/\`), 不属于上游 scope。后续如有需要可单独提。
issue #44 #45 (引用回复渲染乱码)