zhixi-document-runtime/docs/document-runtime-architecture.md
wangdl 22276bd44e feat: DOC-FULL-000 完整架构文档 v2
7 设计决策、V2 核心模型、V1→V2 迁移、API 协议映射、项目结构、验收链路

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 19:23:56 +08:00

266 lines
7.1 KiB
Markdown
Raw Permalink 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 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<ReadingPosition>,
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<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`
容量:最大 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 优先 V2V1 为 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
```