Files
zWorkFlow/docs/chat_export_format.md
phoenixray2000 5e0eaa33fa feat(export): 增加微信聊天增量 JSON 导出 (#122)
新增 export_all_chats --delta-only 按时间窗口导出每个会话的增量 JSON.

- --delta-only 写入 deltas/<run_id>/manifest.json + 有消息的会话文件, 便于后续按批次消费
- 沿用现有 CSV plan 选择逻辑, 可通过 --from-plan-csv 控制范围
- 空时间窗口会话直接跳过, 不再生成 message_count=0 的 delta 文件
- 增量 JSON 保留会话级 metadata, 不改变现有 CSV 导出默认流程
- manifest.json 只记录实际有消息的会话, 无法解析的留在 errors 中便于定位

测试: tests/test_export_all_chats_delta.py 7 个 case (run_id 时间戳形状 / 文件名安全化 / msg_uid 哈希组成 / 写窗口不覆盖全量 JSON / 空窗口跳过 / CLI 参数校验 / manifest 记录文件).

跟 #114 (CSV plan) 互补 — CSV plan 选 "哪些会话" 而 delta 选 "哪个时间窗口".
2026-05-29 13:18:53 +08:00

6.1 KiB
Raw Permalink Blame History

聊天导出 JSON 数据格式

export_chat.pytranscribe_chat.py 生成的 JSON 文件采用紧凑格式: 默认值与空值会被省略。本文档说明如何加载和解读这类文件。

生成文件

.venv/bin/python3 export_chat.py <chat_name> [output.json]
.venv/bin/python3 transcribe_chat.py <input.json> [output.json]

export_chat.py 负责原始导出;transcribe_chat.py 使用 WhisperCPU 为语音消息填充转录文本。transcribe_chat.py 可重复运行 —— 已转录的 消息会被跳过。

export_all_chats.py 批量导出时会在输出目录维护 _export_index.json。 该索引用稳定的 username 记录当前 JSON 文件名;联系人备注或群名变化后, 下一次导出会先把旧文件重命名为新的可读文件名,再继续增量合并。若新的 显示名文件已经属于另一个 username,导出文件会追加 __<username> 后缀, 避免覆盖同名联系人或同名群。

顶层结构

{
  "chat": "<display name>",
  "username": "<wxid 或 @chatroom>",
  "exported_at": "YYYY-MM-DD HH:MM:SS",
  "date_first_msg": "YYYY-MM-DD HH:MM:SS",
  "date_last_msg": "YYYY-MM-DD HH:MM:SS",
  "contact_remark": "<remark>",
  "contact_nick_name": "<nick name>",
  "contact_tags": ["<tag>"],
  "contact_memo": "<memo>",
  "is_group": true,
  "messages": [ ... ]
}
  • chat —— 聊天的显示名(联系人名或群名)。
  • username —— 稳定的 WeChat 用户名1-on-1 聊天为 wxid_*,群聊为 *@chatroom)。 transcribe_chat.py 会优先读取本字段而非基于 chat 再次模糊匹配,避免同名联系人漂移。
  • exported_at —— 本地时间字符串,仅作溯源用途。
  • date_first_msg / date_last_msg —— 本次导出结果中第一条 / 最后一条消息的本地时间。
  • contact_remarkcontact_nick_namecontact_tagscontact_memo —— 单聊联系人 metadata群聊中省略。
  • is_group —— 群聊出现且为 true1-on-1 聊天时省略。
  • messages —— 消息数组,跨所有 DB 分片按时间由旧到新排序。

消息条数 = len(messages),没有 total 字段。

消息对象

每条消息必有三个字段:local_idtimestampsender。 其余字段均为可选,当值为默认值或 null 时会被省略。

字段 类型 必填 含义 / 缺失时的默认值
local_id int WeChat 内该聊天的稳定行 ID。用于重跑转录或对比导出时的消息匹配。
timestamp int Unix 时间戳(秒级,本地时间已换算为秒)。通过 datetime.fromtimestamp(ts) 转换。
sender string "me" 代表当前登录用户;否则为发送者的显示名 —— 1-on-1 聊天中是联系人名,群聊中是群成员名。对于无法归属的消息(如系统通知)为 ""
type string 消息类型。缺失时视为 "text"。已知取值:textimagevoicestickervideolink_or_filecallsystemrecallcontact_cardlocation
content string 消息的渲染文本。当没有可提取内容时省略(例如部分图片 / 通话 / 系统事件)。
transcription string type: "voice" 且已完成转录的消息上出现。若 Whisper 未产出文本可能为空串 ""

加载示例

带默认值的遍历:

import json
from datetime import datetime

with open("chat_export_transcribed.json") as f:
    data = json.load(f)

is_group = data.get("is_group", False)

for m in data["messages"]:
    mtype = m.get("type", "text")
    when = datetime.fromtimestamp(m["timestamp"])
    sender = m["sender"]  # "me" | 联系人/群成员名 | ""
    text = m.get("content", "")
    if mtype == "voice":
        text = m.get("transcription") or "[voice, untranscribed]"
    print(f"[{when:%Y-%m-%d %H:%M}] {sender or '(system)'}: {text}")

判断消息是否由自己发出:

from_me = m["sender"] == "me"

筛选仍需转录的语音消息:

pending = [m for m in data["messages"]
           if m.get("type") == "voice" and not m.get("transcription")]

解读注意事项

  • 系统消息type: "system")的 sender"" —— 不属于任何人。 常见内容:撤回通知("X 撤回了一条消息")、添加好友事件等。
  • 空转录transcription: "")表示 Whisper 已经运行但未产出文本, 通常是极短或静音片段。这与"尚未转录"(字段缺失)是不同的状态。
  • 非文本消息的 content 是渲染摘要:[视频] 12秒[表情] 哈哈[图片] 等。原始媒体仍在 WeChat DB 中,可用 mcp_server.py 中的 辅助函数(decode_imagedecode_voice)取出。
  • 群聊中的 sender 是群成员解析后的显示名;当前登录用户仍为 "me"

Delta JSON 输出

export_all_chats.py --delta-only --start <time> [--end <time>] 会写入:

deltas/<run_id>/manifest.json
deltas/<run_id>/chats/*.delta.json

时间窗口内没有消息的会话会跳过,不生成空的 *.delta.json 文件。

Delta 文件顶层字段与完整聊天 JSON 类似,但包含:

  • schema_version
  • export_kind: "wechat_delta"
  • range.start / range.end
  • message_count
  • messages[].msg_uid

msg_uid 是下游去重用的稳定哈希,不要求调用方只依赖 local_id