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

291 lines
8.7 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.

# 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. 关键设计决策
### D1Rust 只保存 materialId
```rust
pub struct ReadingMaterialRef {
pub material_id: String,
}
```
Rust 不知道 `material_id``KnowledgeSource.id` 还是 `TemporaryReadingMaterial.id``readingTargetType` 由 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
```rust
#[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 删除
```
`ack``eventId` 精确删除,不按数量。重复 ack 不崩溃。Buffer 最大 1000 条。
---
## 3. 核心 V2 模型
### 3.1 ReadingMaterialRefV2
```rust
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
```rust
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
```rust
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
```rust
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
```rust
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
```