zhixi-document-runtime/docs/document-runtime-architecture.md
wangdl ce0315d9ae fix: architecture doc - ReadingMaterialRef → ReadingMaterialRefV2
Review finding #1: section 3.3 still referenced old V1 type name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 21:31:20 +08:00

8.7 KiB
Raw Permalink Blame History

Document Runtime 完整架构 v2

DOC-FULL-000 | 2026-06-07

1. 职责边界

Rust (zx_document_core + zx_document_ffi) 负责

文件类型识别 → 预览模式 → 文档信息提取
Markdown / Text / Image / PDF / EPUB / Office 能力模型
ReadingPosition → ReadingSessionV2 → ReadingEventV2
ActiveTimeTracker → activeSecondsDelta 计算
EventBuffer → export / ack / failed / clear
SearchResult → NoteAnchor → 阅读位置恢复
UniFFI DTO → Swift/Kotlin binding → XCFramework 构建

Rust 不负责

userId / token / knowledgeBaseId
readingTargetType (knowledge_source / temporary_file)
后端 API 请求 / COS 下载 / 本地持久化上传队列
iOS UI / Android UI / AI / RAG / 向量化

iOS 负责补充

readingTargetType / platform / appVersion / clientTimezoneOffsetMinutes
本地上传队列 / 调用 API batch upload / 离线重试
阅读页 UI / 首页继续学习定位 / 分析展示

API 负责

接收 ReadingEventUploadItem → 去重入库 → 聚合
LearningSession / MaterialReadingProgress / DailyLearningActivity / LearningRecord
提供 continue / summary / trend / progress 查询接口

2. 关键设计决策

D1Rust 只保存 materialId

pub struct ReadingMaterialRef {
    pub material_id: String,
}

Rust 不知道 material_idKnowledgeSource.id 还是 TemporaryReadingMaterial.idreadingTargetType 由 iOS 上传适配层补充。

D2V1 保留 deprecatedV2 新增独立模块

events.rs       ← V1, 保留 deprecated
events_v2.rs    ← V2, 新模块

V1 不删除iOS 已有接入。新代码走 V2。

D3clientSessionId 由 Rust 生成 UUID

一次阅读页生命周期 = 一个 clientSessionId。Rust 生成保证跨平台一致。sequence 从 1 递增。

D4iOS 控制 tick 节奏Rust 计算 delta

iOS Timer 每 15s → Rust pushHeartbeatV2
App 后台 → iOS 调用 pause
App 前台 → iOS 调用 resume
退出页面 → iOS 调用 close

Rust 不创建 timer。ActiveTimeTracker 根据 timestamp 计算增量,时间倒退不产生负数。

D5Position JSON 使用 camelCase

#[serde(tag = "type", rename_all = "camelCase")]

输出:{"type":"pdf","pageNumber":12,"pageProgress":0.4,"overallProgress":0.32}

D6progress clamp 到 0~1

所有 scrollProgress / pageProgress / overallProgress / chapterProgress 字段NaN→0, Infinity→1, 负数→0, >1→1。

D7ack 按 eventId 删除

Rust exportPendingEventsV2 → iOS 写本地队列 → iOS ackEventsV2(eventIds) → Rust 删除

ackeventId 精确删除,不按数量。重复 ack 不崩溃。Buffer 最大 1000 条。


3. 核心 V2 模型

3.1 ReadingMaterialRefV2

pub struct ReadingMaterialRefV2 {
    pub material_id: String,         // 业务 ID
    pub file_path: String,           // 本地文件路径
    pub file_name: String,           // 文件名
    pub file_type: String,           // markdown / pdf / epub / text / image / office
    pub mime_type: Option<String>,   // MIME 类型(可选)
    pub file_size: Option<u64>,      // 文件大小字节(可选)
    pub content_hash: Option<String>,// 内容哈希(可选)
    pub created_at_ms: Option<i64>,  // 创建时间毫秒(可选)
}

不包含:readingTargetType, userId, knowledgeBaseId, platform, appVersion

V1 ReadingMaterialRef(仅 material_id)标记 #[deprecated],保留不删除。

3.2 ReadingSessionV2

pub struct ReadingSessionV2 {
    pub client_session_id: String,   // UUID, Rust 生成
    pub material: ReadingMaterialRefV2,
    pub started_at_ms: i64,
    pub closed_at_ms: Option<i64>,   // 关闭时间(由 iOS 传入)
    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 | Interrupted | Failed
}

新增方法:mark_session_interrupted_v2, mark_session_failed_v2, get_active_session_v2, set_session_closed_at_ms

3.3 ReadingEventV2

pub struct ReadingEventV2 {
    pub event_id: String,             // UUID, Rust 生成
    pub client_session_id: String,    // 来自 ReadingSessionV2
    pub material_id: String,          // 来自 ReadingMaterialRefV2
    pub event_type: ReadingEventTypeV2,
    pub position: Option<ReadingPosition>,
    pub active_seconds_delta: u32,    // 增量,非累计
    pub timestamp_ms: i64,
    pub sequence: u64,
}

delta 规则:

事件 delta
MaterialOpened 0
PositionChanged 0
MarkedAsRead 0
Heartbeat tick 产生的增量
MaterialClosed close 残余增量

3.4 ActiveTimeTracker

pub struct ActiveTimeTracker {
    pub last_tick_ms: Option<i64>,
    pub is_active: bool,
    pub accumulated_remainder_ms: i64,
}

方法:start / pause / resume / tick / close

3.5 EventBuffer V2

pub struct BufferedReadingEventV2 {
    pub event: ReadingEventV2,
    pub state: BufferedEventState,  // Pending | Exported | Failed
    pub exported_at_ms: Option<i64>,
    pub retry_count: u32,
}

方法:push_event_v2 / export_pending_events_v2 / ack_events_v2 / mark_events_failed_v2 / reload_stale_events_v2 / buffer_size_v2

容量:最大 1000 条,超出时 Failed→Exported→Pending 顺序清理。

reload_stale_events_v2:启动时将 Exported 但未 ack 的事件重置为 Pending用于 App crash 后的恢复。iOS 启动时必须调用。


4. V1 → V2 迁移策略

1. 新增 events_v2.rs 模块V2 核心)
2. V1 events.rs 保留,标记 #[deprecated]
3. V1 FFI 方法保留,不破坏 iOS 编译
4. 新功能走 V2 FFI
5. iOS ReadingRuntimeAdapter 优先 V2V1 为 fallback

V1 vs V2 差异

字段 V1 V2
event_id UUID
client_session_id UUID
active_seconds 累计值/语义模糊 active_seconds_delta 增量
sequence 递增
target material_id 裸字符串 ReadingMaterialRefV2含 filePath/fileName/fileType 等)
session status - Active/Paused/Closed/Interrupted/Failed
session recovery - reload_stale_events_v2 / mark_session_interrupted_v2

5. 与 API 协议映射

Rust ReadingEventV2 API ReadingEventUploadItem 来源
event_id eventId Rust
client_session_id clientSessionId Rust
material_id materialId Rust
event_type eventType Rust→iOS 转 snake_case
position position Rust (camelCase JSON)
active_seconds_delta activeSecondsDelta Rust
timestamp_ms clientTimestampMs Rust
sequence sequence Rust
readingTargetType iOS 补充
platform iOS 补充
appVersion iOS 补充
clientTimezoneOffsetMinutes iOS 补充

6. 项目结构

zhixi-document-runtime
├── zx_document_core/src
│   ├── events.rs          ← V1 (deprecated)
│   ├── events_v2.rs       ← V2 (新增)
│   ├── session_v2.rs      ← ReadingSessionV2 (新增)
│   ├── time_tracker.rs    ← ActiveTimeTracker (新增)
│   ├── progress.rs        ← ReadingPosition (改 camelCase+clamp)
│   ├── material_type.rs   ← ✅ 完成
│   ├── markdown.rs        ← ✅ 完成
│   ├── text.rs            ← ✅ 完成
│   ├── image_meta.rs      ← ✅ 完成
│   ├── search.rs          ← ⚠️ 扩展 PDF/EPUB
│   ├── anchors.rs         ← ⚠️ 补 from_search_result
│   ├── document.rs        ← ⚠️ 扩展 DocumentInfo
│   ├── pdf.rs             ← ❌ stub
│   ├── epub.rs            ← ❌ stub
│   └── blocks.rs          ← ✅ 完成
├── zx_document_ffi/src
│   └── lib.rs             ← 新增 V2 exports
├── docs/
├── fixtures/
├── bindings/ios/
└── scripts/

7. 最终验收链路

App 启动
→ reload_stale_events_v2重置未 ack 的 Exported 事件为 Pending
→ cleanup_stale_sessions_v2清理超时旧 session

iOS 创建 ReadingMaterialRefV2只传 materialId + 文件信息)
→ Rust startReadingSessionV2生成 clientSessionId
→ push MaterialOpeneddelta=0
→ heartbeat tick 每 15s → push Heartbeatdelta=增量秒数)
→ push PositionChangeddelta=0
→ push MarkedAsReaddelta=0
→ push MaterialCloseddelta=残余秒数)
→ exportPendingEventsV2
→ iOS 写入本地 ReadingEventUploadQueue
→ ackEventsV2按 eventId 精确删除)
→ iOS 补 readingTargetType / platform / appVersion / timezone
→ API batch upload