- 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>
232 lines
7.6 KiB
Markdown
232 lines
7.6 KiB
Markdown
# 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 <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 协议](./reading-event-api-protocol.md)(api-server 仓库)
|
||
- [V2 事件 Pipeline 架构](./v2-event-pipeline.md)
|
||
- [Rust FFI 绑定更新流程](./rust-ffi-update.md)
|