fix(mcp): trim raw XML payload from namecard (type=42) chat output (#83)

When a chat history contains a name-card message (msg_type=42), the
dispatcher in `_format_message_text` had no case for `base_type == 42`,
so it fell through to the generic non-text branch:

    elif base_type != 1:
        type_label = format_msg_type(local_type)
        text = f"[{type_label}] {text}" if text else f"[{type_label}]"

`text` for type=42 is the full raw `<msg ...>` element, so chat history
exports emitted `[名片] <msg username="..." antispamticket="v2_..."
brandIconUrl="https://wx.qlogo.cn/..." bigheadimgurl="..." ... />`.

That payload has two problems:
1. It leaks anti-spam tokens (`antispamticket`) and head-image CDN URLs
   into chat logs that are routinely piped to LLMs and other downstream
   tools.
2. The raw XML drowns out the actual signal — a human or an LLM reading
   the chat just wants to know "X shared Y's contact".

This PR adds `_format_namecard_text(content)` that pulls only the three
useful attributes:

- `nickname` — display name
- `username` — wxid (annotated as "公众号" when prefixed `gh_`)
- `certinfo` — user-authored bio

and wires it into the dispatch chain via a new `elif base_type == 42:`
branch, sitting alongside the existing `49` (app message) handler. It
reuses `_parse_xml_root` and `_collapse_text` — no new helpers
introduced.

Tests: 7 cases in `tests/test_namecard_format.py` covering the realistic
shape (with antispamticket / brand URLs that must NOT appear in output),
official accounts (`gh_*`), missing certinfo, missing nickname, missing
both identifiers, and broken-XML fallthrough.

All 158 tests pass locally (151 baseline + 7 new).
This commit is contained in:
Belugary
2026-05-12 16:18:43 +08:00
committed by GitHub
parent 216f44a99f
commit c162a9b92f
2 changed files with 100 additions and 0 deletions

View File

@@ -666,6 +666,31 @@ def _parse_app_message_outer(content):
return root
def _format_namecard_text(content):
"""Parse type=42 (名片) XML into a compact human-readable line.
Source XML carries dozens of fields (antispamticket, biznamecardinfo,
brand URLs, image MD5s) but the useful signal is just three attrs:
``nickname`` (display name), ``username`` (wxid; ``gh_*`` for 公众号),
and ``certinfo`` (the user-authored bio). Everything else is either
auth tokens that should not be piped to downstream systems, or
rendering metadata that bloats the chat log without helping a human
or an LLM understand the conversation.
"""
root = _parse_xml_root(content)
if root is None:
return None
nickname = (root.get("nickname") or "").strip()
username = (root.get("username") or "").strip()
certinfo = _collapse_text(root.get("certinfo") or "")
if not nickname and not username:
return None
head = nickname or username
if username.startswith("gh_"):
head = f"{head} (公众号 {username})"
return f"[名片] {head}: {certinfo}" if certinfo else f"[名片] {head}"
def _format_app_message_text(content, local_type, is_group, chat_username, chat_display_name, names):
if not content or '<appmsg' not in content:
return None
@@ -865,6 +890,8 @@ def _format_message_text(local_id, local_type, content, is_group, chat_username,
text = "[表情]"
elif base_type == 50:
text = _format_voip_message_text(text) or "[通话]"
elif base_type == 42:
text = _format_namecard_text(text) or "[名片]"
elif base_type == 49:
formatted = _format_app_message_text(
text, local_type, is_group, chat_username, chat_display_name, names