feat(mcp): render voice messages with duration in chat history (#97)
## Problem
Voice messages in `_format_message_text` previously rendered as a bare
`[语音] (local_id=N, ts=T)` because msg_type=34 fell through to the generic
non-text branch with no schema-aware summarizer. LLMs reading chat history
had no way to judge whether a voice clip was worth calling `decode_voice`
on without first inspecting it.
## Fix
New helper `_format_voice_text(content)` parses the embedded
`<voicemsg voicelength="…">` and renders `[语音 N.Ns]` (duration to one
decimal, milliseconds → seconds). Type=34 dispatches through it, then
appends the existing `_id_suffix()` so the local_id annotation is
preserved end-to-end:
[语音 3.3s] (local_id=72481, ts=1700000000)
Falls back to `[语音]` (still with `_id_suffix()`) when content is empty,
`<voicemsg>` is absent, XML parse fails, or `voicelength` is missing /
zero / non-numeric.
XML parsing routes through the existing `_parse_xml_root` so the
`_XML_UNSAFE_RE` DOCTYPE/ENTITY filter and 200KB size cap are reused —
no new XXE surface.
## Tests
12 new cases in `tests/test_voice_format.py`: happy path, subsecond,
multi-second, missing / zero / non-numeric voicelength, empty / None
content, missing `<voicemsg>` tag, malformed XML, XXE payload, and two
end-to-end cases through `_format_message_text` (with and without
voicelength) to pin the full rendered output including `_id_suffix()`.
Baseline 183 → 195 passing, 0 regressions.
## Scope
- `mcp_server.py`: adds `_format_voice_text` helper and one branch in
`_format_message_text` (base_type == 34). No public surface change —
this only affects formatting of messages that previously rendered as
the bare `[语音]` fallback.
- `tests/test_voice_format.py`: new file, synthetic fixtures only (no
real PII).
This commit is contained in:
@@ -973,6 +973,21 @@ def _format_voip_message_text(content):
|
||||
return f"[通话] {status_map.get(raw_text, raw_text)}"
|
||||
|
||||
|
||||
def _format_voice_text(content):
|
||||
if not content or '<voicemsg' not in content:
|
||||
return "[语音]"
|
||||
root = _parse_xml_root(content)
|
||||
if root is None:
|
||||
return "[语音]"
|
||||
voice = root.find('.//voicemsg')
|
||||
if voice is None:
|
||||
return "[语音]"
|
||||
length_ms = _parse_int(voice.get('voicelength'), 0)
|
||||
if length_ms <= 0:
|
||||
return "[语音]"
|
||||
return f"[语音 {length_ms / 1000:.1f}s]"
|
||||
|
||||
|
||||
def _format_message_text(local_id, local_type, content, is_group, chat_username, chat_display_name, names, create_time=0):
|
||||
sender_from_content, text = _parse_message_content(content, local_type, is_group)
|
||||
base_type, _ = _split_msg_type(local_type)
|
||||
@@ -985,6 +1000,8 @@ def _format_message_text(local_id, local_type, content, is_group, chat_username,
|
||||
|
||||
if base_type == 3:
|
||||
text = f"[图片] {_id_suffix()}"
|
||||
elif base_type == 34:
|
||||
text = f"{_format_voice_text(text)} {_id_suffix()}"
|
||||
elif base_type == 47:
|
||||
text = "[表情]"
|
||||
elif base_type == 50:
|
||||
|
||||
Reference in New Issue
Block a user