Files
agent-base/docs/superpowers/specs/2026-04-05-openclaw-session-archival-design.md
weishen 1d2531a71f 完善spec:本地解析推送结构化数据 & Django Admin每日导出
- 架构调整:三节点本地解析JSONL,推送结构化JSON而非原始文件
- API改为bulk_upsert接口,接收sessions/messages/tool_calls数组
- 推送脚本改用纯标准库,由OpenClaw cron job每天凌晨触发
- 新增Admin按时间范围查询对话和Daily Markdown导出功能
- Markdown导出精简thinking,保留user/assistant对话和tool调用
- 非技术决策:原始JSONL保留在各节点本地

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 13:32:24 +08:00

506 lines
16 KiB
Markdown
Raw 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.
# OpenClaw Session 解析与归档系统设计
## 1. 需求概述
### 1.1 背景
OpenClaw 多 Agent 系统运行于 Mac Mini + Ubuntu1 + Ubuntu2 三节点,每日产生大量 session JSONL 会话文件,散落于各服务器,缺乏统一查询和分析能力。
### 1.2 目标
- 各节点定时推送 JSONL 到 Mac Mini
- 解析后存入 PostgreSQL + TimescaleDB三表关联sessions / messages / tool_calls
- Django Admin 提供可查询、可筛选的管理界面
### 1.3 核心分析维度
- Token 消耗趋势(按 Agent / 时间 / 模型)
- Tool 调用分析(最常用工具、失败率、平均耗时)
- 模型行为分析thinking level 分布、模型切换频率、stopReason 分布)
## 2. 架构总览
```
┌─────────────────┐ parse push ┌──────────────────┐
│ Ubuntu1 │ ──────────► │ Mac Mini │
└─────────────────┘ structured data │ │
┌─────────────────┐ (JSON) push │ Django + DRF │
│ Ubuntu2 │ ──────────► │ │
└─────────────────┘ structured data │ ├─ API接收 │
┌─────────────────┐ (JSON) push │ ├─ 数据写入 │
│ Mac Mini (本地) │ ──────────────────► │ ├─ Django Admin │
└─────────────────┘ structured data └────┬─────────────┘
(JSON) │
┌─────┴─────────────┐
│ PostgreSQL + │
│ TimescaleDB (NAS) │
└───────────────────┘
```
各节点解析脚本负责:读取 JSONL → 本地解析为结构化数据sessions / messages / tool_calls→ POST JSON 到 Django API → 服务端幂等写入数据库。Django 服务端不再做 JSONL 解析。
## 3. 数据源
### 3.1 JSONL 文件结构
每个 `.jsonl` 文件是一个 session每行是一个 JSON 事件,事件类型:
| 类型 | 说明 |
|------|------|
| `session` | session 元信息id、timestamp、cwd |
| `model_change` | 模型切换 |
| `thinking_level_change` | 思考级别变化 |
| `custom` | 自定义事件(如 model-snapshot |
| `message` | 对话消息user / assistant / toolResult |
message 的 content 是数组,每个 content 可能是:
- `text` — 纯文本
- `thinking` — 推理过程
- `toolCall` — 工具调用
### 3.2 文件命名约定
- 普通 session: `{uuid}.jsonl`
- 带 topic: `{uuid}-topic-{timestamp}.jsonl`
- 被重置过的: `{uuid}.jsonl.reset.{timestamp}`
- 被删除的: `{uuid}.jsonl.deleted.{timestamp}`
解析时需跳过 `.deleted.` 文件。
### 3.3 Agent 识别
session 文件位于 `agents/{agent_name}/sessions/` 路径下,从路径提取 agent_name。
## 4. 数据模型
所有表结构通过 Django Model + Django Migration 进行定义和映射,不直接执行 DDL。TimescaleDB 的 hypertable 创建通过 `RunSQL` 操作嵌入 Django migration 中。
### 4.1 Session 表
每个 JSONL 文件一条记录。
```python
class Session(models.Model):
session_id = models.UUIDField() # session UUID
agent_name = models.CharField() # 如 xingyao
source_node = models.CharField() # 来源节点: macmini / ubuntu1 / ubuntu2
session_version = models.IntegerField() # version from JSONL
model_provider = models.CharField() # 最后一个 model_change 的 provider
model_id = models.CharField() # 最后一个 model_change 的 modelId
thinking_level = models.CharField() # 最后一个 thinking_level_change
start_time = models.DateTimeField() # 第一个事件的 timestamp
end_time = models.DateTimeField() # 最后一个事件的 timestamp
cwd = models.CharField() # 工作目录
total_tokens = models.IntegerField() # 所有 assistant message 的 usage.totalTokens 之和
total_cost = models.FloatField() # 所有 assistant message 的 cost.total 之和
message_count = models.IntegerField() # message 数量
tool_call_count = models.IntegerField() # toolCall 数量
error_count = models.IntegerField() # isError=True 的 toolResult 数量
raw_file_path = models.TextField() # 原始 JSONL 在 Mac Mini 的本地路径
pushed_at = models.DateTimeField() # 推送/入库时间
status = models.CharField() # active / deleted
metadata = models.JSONField() # 原始 session 和 custom 事件的原始 JSON
```
### 4.2 Message 表
每个 `type: message` 的事件一条记录,外键关联 Session。
```python
class Message(models.Model):
session = models.ForeignKey(Session)
message_id = models.CharField() # 事件 id
parent_id = models.CharField() # parentId
seq = models.IntegerField() # 在 session 中的顺序
role = models.CharField() # user / assistant / toolResult
content_text = models.TextField() # 提取的纯文本(去 JSON 结构,用于全文搜索)
raw_content = models.JSONField() # message.content 原始 JSON
raw_message = models.JSONField() # 整条 message 原始 JSON
timestamp = models.DateTimeField()
# assistant 专用
model = models.CharField()
provider = models.CharField()
stop_reason = models.CharField()
tokens_input = models.IntegerField()
tokens_output = models.IntegerField()
tokens_cache_read = models.IntegerField()
tokens_cache_write = models.IntegerField()
tokens_total = models.IntegerField()
cost_total = models.FloatField()
# toolResult 专用
tool_call_id = models.CharField() # 对应哪个 toolCall
tool_name = models.CharField()
is_error = models.BooleanField()
exit_code = models.IntegerField()
duration_ms = models.IntegerField()
```
### 4.3 ToolCall 表
每个 `type: toolCall` 的 content 块一条记录。
```python
class ToolCall(models.Model):
session = models.ForeignKey(Session)
message = models.ForeignKey(Message)
tool_call_id = models.CharField() # call_function_xxx_N
tool_name = models.CharField() # 如 exec
arguments = models.JSONField() # 调用参数原始 JSON
# 执行结果(从对应的 toolResult 关联回来)
result_text = models.TextField() # toolResult 的提取文本
is_error = models.BooleanField()
exit_code = models.IntegerField()
duration_ms = models.IntegerField()
seq = models.IntegerField() # 在该 message 中的顺序
```
### 4.4 TimescaleDB 时序优化
- 在 Django 的最后一张数据表 migration 后,新增 migration 使用 `RunSQL` 创建 hypertables
- `Session.start_time` 设为 hypertable 的分区列(按天分区)
- `Message.timestamp` 设为 hypertable 的分区列(按天分区)
- `ToolCall` 表使用关联 `Message.timestamp` 进行分区(不额外添加字段,通过消息时间推导)
## 5. 推送机制
### 5.1 解析与推送脚本
Python 脚本部署在所有三个节点Mac Mini / Ubuntu1 / Ubuntu2
```
Usage: python sync_sessions.py --remote-url http://macmini:8000/api/sessions/bulk_upsert/
```
**职责**
1. 扫描本地 `agents/{agent_name}/sessions/` 目录
2. 读取 `.sync_state` 文件,找出新增/变更的 `.jsonl` 文件(排除 `.deleted.`
3. 本地解析每个 JSONL逐行 JSON → 提取 session 元信息 / messages / tool_calls
4. POST 结构化 JSON 到 Django API`POST /api/sessions/bulk_upsert/`
5. 上传成功后更新 `.sync_state`
**脚本特点**
- 纯 Python 标准库,无外部依赖
- 本地完成解析,服务端只负责结构化写入
- 幂等:服务端根据 `session_id + agent_name` 判断重复,重复推送跳过
**请求体示例**
```json
{
"agent_name": "xingyao",
"source_node": "macmini",
"sessions": [
{
"session_id": "xxx-xxx",
"session_version": 1,
"model_provider": "anthropic",
"model_id": "claude-sonnet-4-6",
...
}
],
"messages": [
{
"session_id": "xxx-xxx",
"message_id": "yyy",
"role": "user",
...
}
],
"tool_calls": [
{
"session_id": "xxx-xxx",
"message_id": "yyy",
"tool_call_id": "call_function_xxx_0",
"tool_name": "exec",
...
}
]
}
```
### 5.2 定时任务
各节点由 OpenClaw cron job 每天凌晨 2:00 触发一次:
```
0 2 * * * cd /path/to/script && python sync_sessions.py
```
## 6. API 设计
### POST /api/sessions/bulk_upsert/
接收结构化 JSON 批量写入。
```
Content-Type: application/json
Body:
{
"agent_name": "xingyao",
"source_node": "macmini",
"sessions": [...],
"messages": [...],
"tool_calls": [...]
}
```
响应:
```json
{
"status": "ok",
"sessions_upserted": 3,
"messages_upserted": 42,
"tool_calls_upserted": 15
}
```
幂等:根据 `session_id + agent_name` 组合判断重复,已存在则跳过。整个请求在一个事务中写入,任一失败则全部回滚。
## 7. Django Admin
### 7.1 Session 列表页
- 可排序列时间、Agent、模型、token 总量、message 数量
- 侧边栏筛选agent_name、source_node、model_id、日期范围
- 搜索框session_id、cwd
- 查看完整对话原文(渲染为人类可读 HTML
### 7.2 Session 详情页Inline
- Message 按 seq 排序的内联列表
- 每条 message 可折叠展开
- 区分 user / assistant / toolResult 的视觉样式
- assistant 显示 thinking、text、toolCall
- toolResult 显示执行结果和错误状态
- ToolCall 独立 tab可排序
### 7.3 Message 列表页
- 按角色、时间范围、model_id、tool_name 筛选
- 搜索 content_text 全文
### 7.4 ToolCall 列表页
- 按 tool_name、is_error、exit_code 筛选
- 排序duration_ms、token 消耗
### 7.5 按时间范围查询对话
- Admin 增加自定义视图:`/admin/openclaw/daily/`
- 支持选择日期范围 + agent_name
- 按 session 分组,按时间顺序展示完整对话内容
- 每条消息区分 user / assistant / toolResult去掉 thinking 部分
- 人类可读 HTML支持折叠/展开
### 7.6 Daily 复盘导出Markdown
- Admin Session 列表页增加自定义 Action选中 sessions → 导出 Markdown
- 按天为单位生成 `daily-report-{YYYY-MM-DD}.md` 文件并下载
- 精简规则:
- 去掉所有 `thinking` 内容块
- 保留 agent 纯文本回复
- 保留 user 原始输入
- 保留 tool 调用(工具名 + 参数摘要 + 执行结果文本)
- 保留 token 消耗、模型信息等摘要统计
- Markdown 格式示例:
```markdown
# Daily Report: 2026-04-05
## Session: abc12345 (Agent: xingyao)
**模型**: claude-sonnet-4-6 | **Token**: 45,230
### 10:23 User
帮我看看这个 bug...
### 10:23 Assistant
问题出在第 45 行...
### 10:24 Assistant → [Tool: exec]
`grep -n "bug" file.py`
**结果**: 匹配到 3 行
```
## 8. 解析引擎
### 8.1 客户端解析sync_sessions.py
运行在各节点,负责读取 JSONL 并提取结构化数据:
```
逐行读取 JSONL →
提取 session 事件id、version、cwd
跟踪 model_change / thinking_level_change 状态 →
遍历 message 事件 →
提取 assistant usagetokens、cost
提取 content 数组中的 text / toolCall 块 →
遍历 toolResult 事件 → 关联到 toolCall
构建 JSON 批量提交
```
### 8.2 服务端写入Django API
接收客户端推送的结构化 JSON
```
验证 agent_name + source_node →
批量 upsert Session幂等session_id + agent_name 去重)→
批量写入 Message外键关联 Session
批量写入 ToolCall外键关联 Message
更新 Session 聚合字段token 总数、cost、message count 等)→
返回写入结果
```
整个写入在一个事务中完成,任一失败则全部回滚。
## 9. 技术栈
| 组件 | 版本 |
|------|------|
| Python | 3.12+ |
| Django | 5.x |
| Django REST Framework | 3.15+ |
| PostgreSQL | 16+ |
| TimescaleDB | 2.14+ |
| psycopg | 3.x |
| 数据库分区 | 按天分区,保留 90 天热数据 |
## 10. Docker Compose 部署
### 10.1 架构决策
- Docker Compose 仅管理 Django 应用服务(`web`),数据库连接远程 NAS 上的 PostgreSQL + TimescaleDB
- Django 服务器与 NAS 处于同一局域网,通过内网 IP 直连
- 所有可变配置通过 `.env` 文件注入,该文件加入 `.gitignore`
### 10.2 项目结构
```
agent-base/
├── docker-compose.yml # 编排配置
├── Dockerfile # Django 镜像构建
├── nginx/
│ └── nginx.conf.placeholder # Nginx 反代占位配置(预留)
├── .env # 环境变量(不提交)
├── .env.example # 环境变量模板(提交)
├── .dockerignore
├── manage.py
├── src/ # Django 项目源码
│ ├── openclaw/ # app
│ └── config/ # 项目配置
└── docs/ # 文档
```
### 10.3 `.env` 配置
```bash
# .env.example提交到仓库
# Django
DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_PORT=8000
DJANGO_ALLOWED_HOSTS=*
# Database (NAS)
DB_HOST=192.168.x.x
DB_PORT=5432
DB_NAME=openclaw_archive
DB_USER=openclaw
DB_PASSWORD=CHANGE_ME
```
`.env` 文件被加入 `.gitignore`,仅保留 `.env.example` 作为模板。
### 10.4 Dockerfile
```dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE ${DJANGO_PORT:-8000}
CMD ["gunicorn", "--bind", "0.0.0.0:${DJANGO_PORT:-8000}", \
"--workers", "4", "--timeout", "120", \
"config.wsgi:application"]
```
Gunicorn 4 个 worker120 秒超时(解析 JSONL 可能耗时的场景)。
### 10.5 docker-compose.yml
```yaml
services:
web:
build:
context: .
dockerfile: Dockerfile
container_name: openclaw-archive
env_file:
- .env
ports:
- "${DJANGO_PORT:-8000}:${DJANGO_PORT:-8000}"
volumes:
- static_volume:/app/staticfiles
- jsonl_archive:/app/archive
restart: unless-stopped
# nginx 占位(预留)
# nginx:
# image: nginx:alpine
# container_name: openclaw-nginx
# ports:
# - "80:80"
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf
# - static_volume:/app/staticfiles:ro
# depends_on:
# - web
volumes:
static_volume:
jsonl_archive: # 原始 JSONL 归档存储
```
### 10.6 部署与运维命令
```bash
# 首次部署
cp .env.example .env
vim .env # 填写真实配置
docker compose build
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py createsuperuser
docker compose up -d
# 日常运维
docker compose ps # 查看状态
docker compose logs -f web # 查看日志
docker compose down # 停止
docker compose up -d --build # 重建并重启
# 数据库迁移
docker compose run --rm web python manage.py makemigrations
docker compose run --rm web python manage.py migrate
```
### 10.7 Nginx 反代(预留)
当前阶段不使用 Nginx。`docker-compose.yml` 中已预留 Nginx 配置(注释状态),生产环境需要 HTTPS、静态文件优化时取消注释即可启用。
启用前需将 `nginx/nginx.conf.placeholder` 重命名为 `nginx.conf` 并根据实际域名和证书路径修改配置。
## 12. 非技术决策
- 不接收 `.deleted.` 文件
- 幂等推送:同一 session 重复推送时跳过(基于 session_id + agent_name 组合键)
- Mac Mini 本地 session 同样通过推送脚本入库(保证统一流程,不走特殊路径)
- 原始 JSONL 保留在各节点本地,不推送到 Django 服务端