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

4.8 KiB
Raw Permalink Blame History

ReadingEvent V2 协议

概述

V2 阅读事件协议定义了一套完整的学习行为采集框架:阅读会话 → 事件生成 → buffer 暂存 → export 导出 → ack 确认

与 V1 的区别:

  • V1无 session无 ack无 id 追踪
  • V2session 管理 + 全局事件 buffer + export/ack 确认 + crash recovery

核心概念

ReadingSessionV2

一次阅读会话。iOS App 一次打开资料即创建一个 session。

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

pub struct ReadingMaterialRef {
    pub material_id: String,
}

Rust 不存储 readingTargetType(如 knowledge/material/course/note由 iOS 上传时补充。

ReadingEventV2

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()