ios-projects/docs/reading-info-integration.md
wangdl 6c2f742676 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>
2026-06-18 14:23:12 +08:00

232 lines
7.6 KiB
Markdown
Raw 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.

# 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)