# Document Runtime 完整架构 v2 > DOC-FULL-000 | 2026-06-07 ## 1. 职责边界 ### Rust (zx_document_core + zx_document_ffi) 负责 ```text 文件类型识别 → 预览模式 → 文档信息提取 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 不负责 ```text userId / token / knowledgeBaseId readingTargetType (knowledge_source / temporary_file) 后端 API 请求 / COS 下载 / 本地持久化上传队列 iOS UI / Android UI / AI / RAG / 向量化 ``` ### iOS 负责补充 ```text readingTargetType / platform / appVersion / clientTimezoneOffsetMinutes 本地上传队列 / 调用 API batch upload / 离线重试 阅读页 UI / 首页继续学习定位 / 分析展示 ``` ### API 负责 ```text 接收 ReadingEventUploadItem → 去重入库 → 聚合 LearningSession / MaterialReadingProgress / DailyLearningActivity / LearningRecord 提供 continue / summary / trend / progress 查询接口 ``` --- ## 2. 关键设计决策 ### D1:Rust 只保存 materialId ```rust pub struct ReadingMaterialRef { pub material_id: String, } ``` Rust 不知道 `material_id` 是 `KnowledgeSource.id` 还是 `TemporaryReadingMaterial.id`。`readingTargetType` 由 iOS 上传适配层补充。 ### D2:V1 保留 deprecated,V2 新增独立模块 ``` events.rs ← V1, 保留 deprecated events_v2.rs ← V2, 新模块 ``` V1 不删除,iOS 已有接入。新代码走 V2。 ### D3:clientSessionId 由 Rust 生成 UUID 一次阅读页生命周期 = 一个 `clientSessionId`。Rust 生成保证跨平台一致。`sequence` 从 1 递增。 ### D4:iOS 控制 tick 节奏,Rust 计算 delta ``` iOS Timer 每 15s → Rust pushHeartbeatV2 App 后台 → iOS 调用 pause App 前台 → iOS 调用 resume 退出页面 → iOS 调用 close ``` Rust 不创建 timer。`ActiveTimeTracker` 根据 timestamp 计算增量,时间倒退不产生负数。 ### D5:Position JSON 使用 camelCase ```rust #[serde(tag = "type", rename_all = "camelCase")] ``` 输出:`{"type":"pdf","pageNumber":12,"pageProgress":0.4,"overallProgress":0.32}` ### D6:progress clamp 到 0~1 所有 `scrollProgress` / `pageProgress` / `overallProgress` / `chapterProgress` 字段:NaN→0, Infinity→1, 负数→0, >1→1。 ### D7:ack 按 eventId 删除 ``` Rust exportPendingEventsV2 → iOS 写本地队列 → iOS ackEventsV2(eventIds) → Rust 删除 ``` `ack` 按 `eventId` 精确删除,不按数量。重复 ack 不崩溃。Buffer 最大 1000 条。 --- ## 3. 核心 V2 模型 ### 3.1 ReadingMaterialRef ```rust pub struct ReadingMaterialRef { pub material_id: String, } ``` ### 3.2 ReadingSessionV2 ```rust pub struct ReadingSessionV2 { pub client_session_id: String, // UUID, Rust 生成 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, pub status: ReadingSessionStatus, // Active | Paused | Closed } ``` ### 3.3 ReadingEventV2 ```rust pub struct ReadingEventV2 { pub event_id: String, // UUID, Rust 生成 pub client_session_id: String, // 来自 ReadingSessionV2 pub material_id: String, // 来自 ReadingMaterialRef pub event_type: ReadingEventTypeV2, pub position: Option, 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 ```rust pub struct ActiveTimeTracker { pub last_tick_ms: Option, pub is_active: bool, pub accumulated_remainder_ms: i64, } ``` 方法:`start / pause / resume / tick / close` ### 3.5 EventBuffer V2 ```rust pub struct BufferedReadingEventV2 { pub event: ReadingEventV2, pub state: BufferedEventState, // Pending | Exported | Failed pub exported_at_ms: Option, pub retry_count: u32, } ``` 方法:`push_event_v2 / export_pending_events_v2 / ack_events_v2 / mark_events_failed_v2` 容量:最大 1000 条,超出时 Failed→Exported→Pending 顺序清理。 --- ## 4. V1 → V2 迁移策略 ``` 1. 新增 events_v2.rs 模块(V2 核心) 2. V1 events.rs 保留,标记 #[deprecated] 3. V1 FFI 方法保留,不破坏 iOS 编译 4. 新功能走 V2 FFI 5. iOS ReadingRuntimeAdapter 优先 V2,V1 为 fallback ``` ### V1 vs V2 差异 | 字段 | V1 | V2 | |------|----|----| | event_id | 无 | UUID | | client_session_id | 无 | UUID | | active_seconds | 累计值/语义模糊 | active_seconds_delta 增量 | | sequence | 无 | 递增 | | target | material_id 裸字符串 | ReadingMaterialRef | --- ## 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. 最终验收链路 ``` iOS 创建 materialId → Rust startReadingSessionV2 → Rust 生成 clientSessionId → push MaterialOpened / PositionChanged / Heartbeat / MaterialClosed → exportPendingEventsV2 → iOS 写入本地队列 → ackEventsV2 → iOS 补 readingTargetType / platform / appVersion / timezone → API batch upload ```