- Architecture diagram: Swift UI → SessionManager → Collector → Queue → API - Core components: Context, SessionManager, Events, Position, UploadQueue - API protocol: batch upload, progress query, continue-learning, summary - Path resolution, marked-read store, app lifecycle, error handling Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.6 KiB
7.6 KiB
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
打开阅读器时需要提供的上下文:
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
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
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 <USER_JWT>
{
"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 <USER_JWT>
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 <USER_JWT>
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 <USER_JWT>
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/<id>/<filename> |
| 2 | Documents 目录 | Documents/<materialId> |
| 3 | Caches 目录 | Caches/<materialId> |
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 协议(api-server 仓库)
- V2 事件 Pipeline 架构
- Rust FFI 绑定更新流程