# iOS 阅读信息采集接入文档 ## 1. 概述 AIStudyApp 通过 Rust FFI(zx_document_core)和自有 Swift 层采集用户阅读行为数据,上传至 API Server,为 AI 分析和个性化推荐提供数据基础。 数据流:Swift UI → ReadingRuntimeSessionManager → ReadingEventCollector → ReadingEventUploadQueue → API Server --- ## 2. 架构 ``` ┌─────────────────────────────────────────────────────┐ │ SwiftUI Layer │ │ MaterialReaderView → open/close/heartbeat/position │ └──────────────────┬──────────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────────┐ │ ReadingRuntimeSessionManager (Singleton) │ │ 职责:生命周期管理、状态机、适配器路由 │ │ 状态:idle → opened → reading → closed │ └──────────────────┬──────────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────────┐ │ ReadingEventCollector (Singleton) │ │ 职责:事件缓冲、去重、序列化 │ │ 事件类型:opened / heartbeat / position_changed / │ │ closed / marked_as_read / failed │ └──────────────────┬──────────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────────┐ │ ReadingEventUploadQueue (Singleton) │ │ 职责:批量上传、重试、ACK 确认 │ │ 上传间隔:30s(默认) │ │ 最大批次:50 条 │ └──────────────────┬──────────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────────┐ │ ReadingAPIService (APIClient) │ │ 端点:POST /api/reading-events/batch │ │ GET /api/reading/progress/:id │ └─────────────────────────────────────────────────────┘ ``` --- ## 3. 核心组件 ### 3.1 ReadingMaterialContext 打开阅读器时需要提供的上下文: ```swift struct ReadingMaterialContext { let readingTargetType: ReadingTargetType // .material, .knowledgeSource let materialId: String let knowledgeBaseId: String? let userId: String? } ``` ### 3.2 ReadingRuntimeSessionManager | 方法 | 说明 | |------|------| | `openMaterial(_ ctx:)` | 打开阅读会话,触发 `opened` 事件 | | `closeMaterial()` | 关闭会话,触发 `closed` 事件 | | `updatePosition(_ pos:)` | 更新阅读位置,触发 `position_changed` 事件 | | `sendHeartbeat()` | 心跳,更新活跃时间 | | `markAsRead()` | 标记已读 | | `adapter` | V1/V2 适配器切换 | ### 3.3 ReadingEvent ```swift enum ReadingEvent { case opened(materialId: String, timestamp: UInt64) case heartbeat(materialId: String, position: ReadingPosition?, timestamp: UInt64) case positionChanged(materialId: String, position: ReadingPosition, timestamp: UInt64) case closed(materialId: String, totalActiveMs: UInt64, timestamp: UInt64) case markedAsRead(materialId: String, timestamp: UInt64) case failed(materialId: String, errorCode: String, timestamp: UInt64) } ``` ### 3.4 ReadingPosition ```swift enum ReadingPosition { case markdown(blockId: String, scrollProgress: Float) case text(lineNumber: UInt32, scrollProgress: Float) case pdf(pageNumber: UInt32, pageProgress: Float, overallProgress: Float) case image(zoomScale: Float, offsetX: Float, offsetY: Float) case epub(chapterId: String, chapterProgress: Float, overallProgress: Float) case unknown } ``` --- ## 4. 上传协议 ### 4.1 批量上传 ``` POST /api/reading-events/batch Authorization: Bearer { "events": [ { "eventType": "position_changed", "materialId": "mat-001", "position": { "type": "Markdown", "blockId": "b1", "scrollProgress": 0.5 }, "timestampMs": 1700000000123, "sessionId": "sess-abc" } ] } ``` ### 4.2 阅读进度查询 ``` GET /api/reading/progress/:materialId?targetType=knowledge_source Authorization: Bearer Response 200: { "materialId": "mat-001", "targetType": "knowledge_source", "status": "in_progress", "totalActiveSeconds": 300, "isMarkedRead": false, "lastReadAt": "2026-06-18T12:00:00Z", "lastPosition": { "type": "Markdown", "blockId": "b1", "scrollProgress": 0.5 } } ``` --- ## 5. 位置恢复 ### 5.1 继续学习 ``` GET /api/reading/continue-learning Authorization: Bearer Response 200: { "type": "knowledge_source", "materialId": "mat-001", "title": "Machine Learning Basics", "lastProgress": 0.5, "totalActiveSeconds": 600, "lastReadAt": "2026-06-18T10:00:00Z", "lastPosition": { "type": "Markdown", "blockId": "b1", "scrollProgress": 0.5 } } ``` ### 5.2 学习摘要 ``` GET /api/reading/learning-summary Authorization: Bearer Response 200: { "todaySeconds": 3600, "weekSeconds": 7200, "totalSeconds": 50000, "activeDays": 5, "sessionsCount": 12, "materialsReadCount": 3, "markedReadCount": 1, "dailyAverageSeconds": 1200 } ``` --- ## 6. 文件路径解析 `MaterialPathResolver` 负责将 API 返回的 materialId 解析为本地文件路径: | 优先级 | 策略 | 路径 | |:--:|------|------| | 1 | Knowledge Source | `Documents/knowledge_sources//` | | 2 | Documents 目录 | `Documents/` | | 3 | Caches 目录 | `Caches/` | --- ## 7. 已读标记 `MarkedReadStore` 提供离线已读标记(UserDefaults 持久化): - `mark(materialId)` — 标记已读 - `unmark(materialId)` — 取消标记 - `contains(materialId)` — 查询是否已读 --- ## 8. App 生命周期 | 事件 | 处理 | |------|------| | `sceneDidEnterBackground` | UploadScheduler.flush() 强制上传 | | `sceneWillEnterForeground` | RuntimeRecoveryService 恢复未完成会话 | | `applicationWillTerminate` | 关闭所有活跃 Session + flush | --- ## 9. 错误处理 | 场景 | 策略 | |------|------| | 网络不可用 | 事件缓冲在本地队列,最多保留 1000 条 | | 上传失败 | 指数退避重试(1s→2s→4s→8s→30s) | | ACK 丢失 | 未 ACK 事件下次上传时重发 | | Rust FFI 崩溃 | V1 fallback 适配器(NoopReadingRuntimeAdapter) | --- ## 10. 相关文档 - [阅读事件 API 协议](./reading-event-api-protocol.md)(api-server 仓库) - [V2 事件 Pipeline 架构](./v2-event-pipeline.md) - [Rust FFI 绑定更新流程](./rust-ffi-update.md)