From c162a9b92f271cab13b29db76551813e9ae11062 Mon Sep 17 00:00:00 2001 From: Belugary <53219544+Belugary@users.noreply.github.com> Date: Tue, 12 May 2026 16:18:43 +0800 Subject: [PATCH] fix(mcp): trim raw XML payload from namecard (type=42) chat output (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` element, so chat history exports emitted `[名片] `. 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). --- mcp_server.py | 27 +++++++++++++ tests/test_namecard_format.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/test_namecard_format.py diff --git a/mcp_server.py b/mcp_server.py index 619af79..d604271 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -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 '`, dumping the full `` element including +antispamticket, biznamecardinfo and head-image URLs. Those tokens are PII that +should not be piped to downstream LLM / log systems. + +These tests pin the new behaviour: a compact `[名片] : ` line, +without any source-only XML fields. +""" +import unittest + +import mcp_server + + +# Realistic-shape sample with the noisy / sensitive attrs that used to leak. +_REAL_NAMECARD = ( + '' +) + + +class FormatNamecardTextTests(unittest.TestCase): + def test_compact_line_for_real_namecard(self): + out = mcp_server._format_namecard_text(_REAL_NAMECARD) + self.assertEqual(out, "[名片] 李雷: 搬砖工人 / 业余摄影") + + def test_no_pii_or_url_in_output(self): + out = mcp_server._format_namecard_text(_REAL_NAMECARD) + self.assertNotIn("antispamticket", out) + self.assertNotIn("v2_abc123def456", out) + self.assertNotIn("qlogo.cn", out) + self.assertNotIn("brandIconUrl", out) + self.assertNotIn("headimgurl", out) + + def test_official_account_marked(self): + xml = ( + '' + ) + out = mcp_server._format_namecard_text(xml) + self.assertEqual( + out, "[名片] Some Official Account (公众号 gh_some_official): 一个公众号" + ) + + def test_no_certinfo_falls_back_to_head_only(self): + xml = '' + out = mcp_server._format_namecard_text(xml) + self.assertEqual(out, "[名片] 韩梅梅") + + def test_only_username_when_nickname_missing(self): + xml = '' + out = mcp_server._format_namecard_text(xml) + self.assertEqual(out, "[名片] wxid_demo") + + def test_missing_both_identifiers_returns_none(self): + xml = '' + self.assertIsNone(mcp_server._format_namecard_text(xml)) + + def test_broken_xml_returns_none(self): + self.assertIsNone(mcp_server._format_namecard_text("")) + self.assertIsNone(mcp_server._format_namecard_text("