""" 将单个聊天的全部消息导出为 JSON。 用法: .venv/bin/python3 export_chat.py [output.json] 参数: 联系人显示名、备注名、群名或 wxid。 [output.json] 可选输出路径,默认 "_export.json"。 示例: .venv/bin/python3 export_chat.py .venv/bin/python3 export_chat.py /tmp/out.json 输出 JSON 的紧凑结构: { "chat": "", "username": "", "exported_at": "YYYY-MM-DD HH:MM:SS", "is_group": true, // 仅群聊出现 "messages": [ {"local_id": 1, "timestamp": 1713..., "sender": "me", "content": "..."}, {"local_id": 2, "timestamp": 1713..., "sender": "", "type": "voice"} ] } 默认值/空值会被省略: text 消息省略 "type",无可提取内容时省略 "content", 1-on-1 聊天省略 "is_group"。 语音消息以 type "voice" 导出且不带 transcription 字段;运行 transcribe_chat.py 可用 Whisper 补齐转录。 需先完成 WeChat DB 解密(详见 README)。 完整 schema、字段语义与加载示例: docs/chat_export_format.md """ import json import sqlite3 import sys from contextlib import closing from datetime import datetime import mcp_server MSG_TYPE_MAP = { 1: "text", 3: "image", 34: "voice", 42: "contact_card", 43: "video", 47: "sticker", 48: "location", 49: "link_or_file", 50: "call", 10000: "system", 10002: "recall", } def _msg_type_str(local_type): base, _ = mcp_server._split_msg_type(local_type) return MSG_TYPE_MAP.get(base, f"type_{local_type}") def _resolve_sender(row, ctx, names, id_to_username): """Resolve the sender of a message. Returns "me" for the logged-in user, or the sender's display name otherwise (the contact's name in 1-on-1 chats, the member's name in groups). Empty string for unattributable messages (e.g. system notifications). """ local_id, local_type, create_time, real_sender_id, content, ct = row decoded = mcp_server._decompress_content(content, ct) sender_from_content, _ = mcp_server._format_message_text( local_id, local_type, decoded, ctx["is_group"], ctx["username"], ctx["display_name"], names ) label = mcp_server._resolve_sender_label( real_sender_id, sender_from_content, ctx["is_group"], ctx["username"], ctx["display_name"], names, id_to_username, ) return label or "" def _decode_sticker_desc(b64_desc): """WeChat encodes sticker labels as base64 protobuf: repeated (lang, text) pairs. Returns the 'default' language label (usually Chinese), or None. Limitation: treats the length byte as a single octet rather than a real protobuf varint — labels >127 bytes would be misread. In practice sticker descriptions are short (<30 chars), so this is adequate. Also sensitive to the bytes b"default" appearing inside a preceding value; no such cases observed. """ import base64 try: raw = base64.b64decode(b64_desc) except Exception: return None # Find the 'default' marker; text follows as: \x12 i = raw.find(b"default") if i < 0 or i + 7 >= len(raw) or raw[i + 7] != 0x12: return None try: text_len = raw[i + 8] text_bytes = raw[i + 9 : i + 9 + text_len] return text_bytes.decode("utf-8") or None except (IndexError, UnicodeDecodeError): return None def _format_sticker_message(content): root = mcp_server._parse_xml_root(content) if content else None if root is None: return "[表情]" emoji = root.find(".//emoji") if emoji is None: return "[表情]" desc = emoji.get("desc") or "" label = _decode_sticker_desc(desc) if desc else None return f"[表情] {label}" if label else "[表情]" def _format_system_message(content): if not content: return "[系统消息]" if " [output.json]") sys.exit(1) chat = sys.argv[1] out = sys.argv[2] if len(sys.argv) > 2 else f"{chat}_export.json" export_chat(chat, out)