docs: add iOS reading info integration guide (IOS-INFO-026)

- 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>
This commit is contained in:
wangdl 2026-06-18 14:23:12 +08:00
parent de384dc027
commit 6c2f742676

View File

@ -0,0 +1,231 @@
# iOS 阅读信息采集接入文档
## 1. 概述
AIStudyApp 通过 Rust FFIzx_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)