主题
1. 数据来源:Claude Code jsonl
1. 文件存储位置
官方 Claude Code
~/.claude/projects/<项目哈希>/
├── <sessionId>.jsonl # 主会话 transcript
└── <sessionId>/subagents/ # 子代理 transcript
├── agent-<id>.jsonl
└── agent-<id>.meta.jsontrpc-claudecode(本环境常用)
~/.trpc-claudecode/projects/<编码项目路径>/
├── <sessionId>.jsonl
└── <sessionId>/subagents/
├── agent-<id>.jsonl
└── agent-<id>.meta.jsonWatcher 默认监听 ~/.claude/projects,也可通过 --projects-root 指向 ~/.trpc-claudecode/projects。
2. jsonl 行格式
每行一个 JSON 对象,核心字段:
json
{
"type": "user",
"message": {
"role": "user",
"content": "帮我写一个快排函数"
},
"timestamp": "2026-06-05T10:00:01.123Z",
"sessionId": "b0dcb60c-5539-42a7-b71b-80ffc878ec0f"
}常见 type
| type | 含义 | 解析处理 |
|---|---|---|
user | 用户输入 | 真人输入 → 新 Turn;tool_result → 归档 |
assistant | 模型输出 | 追加到当前 Turn |
attachment | Skill 列表等 | 本方案暂不单独上报 |
system | 系统元数据 | 本方案暂不单独上报 |
assistant 行的 content 结构
content 可能是字符串,也可能是数组:
json
{
"type": "assistant",
"message": {
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"id": "msg_01ABC...",
"content": [
{"type": "text", "text": "好的,我来帮你写..."},
{"type": "tool_use", "id": "toolu_01XYZ", "name": "Bash", "input": {"command": "ls"}}
],
"usage": {
"input_tokens": 1200,
"output_tokens": 85
},
"stop_reason": "end_turn"
},
"timestamp": "2026-06-05T10:00:04.567Z"
}tool_result 行
工具执行结果以 user 角色回灌:
json
{
"type": "user",
"message": {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01XYZ",
"content": "file1.py\nfile2.py"
}
]
},
"timestamp": "2026-06-05T10:00:05.000Z"
}3. trpc-claudecode 的「拆行」特性
同一 API 响应可能占多行 jsonl,共享 message.id:
行1: message.id=msg_01, content=[{type:text, text:"好的"}]
行2: message.id=msg_01, content=[{type:tool_use, name:Bash, ...}]
行3: message.id=msg_01, content=[{type:tool_use, name:Read, ...}]解析策略:按 message.id 分组(_group_assistant_calls()),而不是简单按行去重。这样一次模型调用对应一个 Langfuse Generation,而不是把整轮压成一条。
4. 子代理文件
agent-*.jsonl
子代理(Task / Agent 工具)有独立 transcript,结构与主会话相同。
agent-*.meta.json(trpc-claudecode)
json
{
"agentType": "Explore",
"description": "搜索项目中与 langfuse 相关的代码"
}用于 match_subagent() 把主会话的 tool_use 和磁盘上的子代理文件配对。
两种磁盘布局
代码在 _candidate_subagent_dirs() 中兼容:
| 布局 | 路径 |
|---|---|
| A(官方 CC) | <projects>/<hash>/agent-<id>.jsonl |
| B(trpc fork) | <projects>/<hash>/<sessionId>/subagents/agent-<id>.jsonl |
5. Hook 方案的输入来源
Hook 从 stdin 读取 Claude Code 传入的 JSON payload:
python
def read_hook_payload() -> Dict[str, Any]:
data = sys.stdin.read()
return json.loads(data)从中提取:
python
session_id = payload.get("sessionId")
transcript_path = payload.get("transcriptPath")这是 Hook 与 Watcher 的唯一输入差异——后续解析逻辑完全一致。
6. Watcher 如何发现文件
python
def is_main_session_jsonl(path: Path) -> bool:
# 文件名是 UUID 格式:<sessionId>.jsonl
# 排除 agent-*.jsonl(那是子代理,由主会话逻辑嵌套处理)
return _MAIN_JSONL_STEM_RE.match(path.stem) and not path.name.startswith("agent-")发现策略:
--transcript:监听单个文件(不存在时会等待创建)--projects-root:递归扫描目录下所有主 session jsonl- 运行中
rescan()发现新 session
7. 生活类比
jsonl 就像餐厅的手写流水账:
- 每写一行 = 一件发生的事(用户说话、AI 回复、取食材回来)
- 同一个
message.id的多行 = 厨师一次炒菜分好几步记录 subagents/agent-*.jsonl= 外包帮厨自己的小本本- Watcher = 会计每隔 1 秒翻一页新账;Hook = 每做完一单才翻一次
下一章:2_parse_pipeline.md — 如何把流水账切成 Turn。