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

134 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 聊天导出 JSON 数据格式
`export_chat.py``transcribe_chat.py` 生成的 JSON 文件采用紧凑格式:
默认值与空值会被省略。本文档说明如何加载和解读这类文件。
## 生成文件
```bash
.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>` 后缀,
避免覆盖同名联系人或同名群。
## 顶层结构
```json
{
"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_remark``contact_nick_name``contact_tags``contact_memo` ——
单聊联系人 metadata群聊中省略。
- `is_group` —— **仅**群聊出现且为 `true`1-on-1 聊天时省略。
- `messages` —— 消息数组,跨所有 DB 分片按时间由旧到新排序。
消息条数 = `len(messages)`,没有 `total` 字段。
## 消息对象
每条消息必有三个字段:`local_id``timestamp``sender`
其余字段均为**可选**,当值为默认值或 null 时会被省略。
| 字段 | 类型 | 必填 | 含义 / 缺失时的默认值 |
| --------------- | ------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `local_id` | int | 是 | WeChat 内该聊天的稳定行 ID。用于重跑转录或对比导出时的消息匹配。 |
| `timestamp` | int | 是 | Unix 时间戳(秒级,本地时间已换算为秒)。通过 `datetime.fromtimestamp(ts)` 转换。 |
| `sender` | string | 是 | `"me"` 代表当前登录用户;否则为发送者的显示名 —— 1-on-1 聊天中是联系人名,群聊中是群成员名。对于无法归属的消息(如系统通知)为 `""`。 |
| `type` | string | 否 | 消息类型。**缺失时视为 `"text"`**。已知取值:`text``image``voice``sticker``video``link_or_file``call``system``recall``contact_card``location`。 |
| `content` | string | 否 | 消息的渲染文本。当没有可提取内容时省略(例如部分图片 / 通话 / 系统事件)。 |
| `transcription` | string | 否 | **仅**在 `type: "voice"` 且已完成转录的消息上出现。若 Whisper 未产出文本可能为空串 `""`。 |
## 加载示例
带默认值的遍历:
```python
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}")
```
判断消息是否由自己发出:
```python
from_me = m["sender"] == "me"
```
筛选仍需转录的语音消息:
```python
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_image``decode_voice`)取出。
- **群聊**中的 `sender` 是群成员解析后的显示名;当前登录用户仍为 `"me"`
## Delta JSON 输出
`export_all_chats.py --delta-only --start <time> [--end <time>]` 会写入:
```text
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`