zhixi-document-runtime/docs/reading-event-protocol.md
2026-06-09 19:58:07 +08:00

161 lines
4.8 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.

# ReadingEvent V2 协议
## 概述
V2 阅读事件协议定义了一套完整的学习行为采集框架:**阅读会话 → 事件生成 → buffer 暂存 → export 导出 → ack 确认**。
与 V1 的区别:
- V1无 session无 ack无 id 追踪
- V2session 管理 + 全局事件 buffer + export/ack 确认 + crash recovery
## 核心概念
### ReadingSessionV2
一次阅读会话。iOS App 一次打开资料即创建一个 session。
```rust
pub struct ReadingSessionV2 {
pub client_session_id: String, // UUID v4会话标识
pub material: ReadingMaterialRef, // 被阅读的资料
pub started_at_ms: i64, // 会话开始时间
pub last_event_at_ms: i64, // 最后一次事件时间
pub next_sequence: u64, // 下一个事件序号(从 1 开始)
pub total_active_seconds: u32, // 累计活跃阅读秒数
pub last_position: Option<ReadingPosition>, // 最后位置
pub status: ReadingSessionStatus, // Active/Paused/Closed
}
```
#### 生命周期
```
start → Active → (pause → Paused → resume → Active)* → close → Closed
```
- **Active**:可推送任意类型事件
- **Paused**:仅可推送 delta=0 事件PositionChanged/MarkedAsRead
- **Closed**:不可推送事件,不可重新打开
### ReadingMaterialRef
```rust
pub struct ReadingMaterialRef {
pub material_id: String,
}
```
Rust 不存储 `readingTargetType`(如 knowledge/material/course/note由 iOS 上传时补充。
### ReadingEventV2
```rust
pub struct ReadingEventV2 {
pub event_id: String, // UUID v4全局唯一事件 ID
pub client_session_id: String, // 关联的会话 ID
pub material_id: String, // 资料 ID
pub event_type: ReadingEventTypeV2, // 事件类型
pub position: Option<ReadingPosition>, // 阅读位置camelCase JSON
pub active_seconds_delta: u32, // 距上次事件的活跃秒数
pub timestamp_ms: i64, // 客户端时间戳
pub sequence: u64, // session 内递增序号1-based
}
```
#### eventType 取值
| Rust | API JSON |
|------|----------|
| `MaterialOpened` | `material_opened` |
| `MaterialClosed` | `material_closed` |
| `PositionChanged` | `position_changed` |
| `Heartbeat` | `heartbeat` |
| `MarkedAsRead` | `marked_as_read` |
#### activeSecondsDelta 规则
| 事件 | delta |
|------|-------|
| MaterialOpened | 0 |
| PositionChanged | 0 |
| MarkedAsRead | 0 |
| Heartbeat | ActiveTimeTracker.tick() 返回值 |
| MaterialClosed | ActiveTimeTracker.close() 返回值 |
### ActiveTimeTracker
iOS 控制 tick 节奏Rust 计算 delta
```
start(ts) → tick(ts+15s) → tick(ts+30s) → close(ts+43s)
delta=15 delta=15 delta=13
```
- 暂停时不累计时间
- 时间倒退返回 0
- 余数毫秒累积到下次 tick
### EventBuffer 状态机
```
push
┌─ Pending ──┐
│ │
export() reload_stale()
│ │
▼ │
Exported ───────┘
┌─────┴─────┐
│ │
ack() mark_failed()
│ │
▼ ▼
(removed) Failed
export() ──→ 重试
```
- **Pending**:新事件,等待导出
- **Exported**:已导出,等待 ack。crash 后 `reload_stale_events()` 恢复为 Pending
- **Failed**:上传失败,下次 export 重试
- **Ack 后删除**:从 buffer 移除
### 溢出驱逐
Buffer 容量1000 条。满时驱逐顺序:
1. Failed最早失败的
2. Exported最早导出的
3. 最早 Pending
## API 上传字段映射
| Rust ReadingEventV2 | API ReadingEventUploadItem | 来源 |
|---------------------|---------------------------|------|
| event_id | eventId | Rust UUID |
| client_session_id | clientSessionId | Rust UUID |
| material_id | materialId | Rust 保存 |
| event_type | eventType | Rust→snake_case |
| position | position | RustcamelCase JSONclamped |
| active_seconds_delta | activeSecondsDelta | ActiveTimeTracker |
| timestamp_ms | clientTimestampMs | Rust |
| sequence | sequence | session 内递增 |
| — | readingTargetType | **iOS 补充** |
| — | platform | **iOS 补充** |
| — | appVersion | **iOS 补充** |
| — | clientTimezoneOffsetMinutes | **iOS 补充** |
## iOS 上传流程
```
1. Rust export_pending_events_v2(limit, timestamp) → Vec<ReadingEventV2>
2. iOS 遍历事件,补充 readingTargetType/platform/appVersion/timezone
3. iOS 构造 API 请求体
4. POST /reading/events
5. 成功 → Rust ack_events_v2(eventIds)
6. 失败 → Rust mark_events_failed_v2(eventIds)
7. App 启动 → Rust reload_stale_events_v2()
```