docs: architecture, supported-formats, event-protocol, reading-position, ios-integration
This commit is contained in:
parent
086006a252
commit
5f34b871ba
84
docs/architecture.md
Normal file
84
docs/architecture.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Architecture
|
||||
|
||||
## 分层架构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ iOS / Android / 鸿蒙 / macOS / Windows / Web │ ← 宿主 App
|
||||
├──────────────────────────────────────────────┤
|
||||
│ UniFFI C-ABI bridge │ ← zx_document_ffi
|
||||
├──────────────────────────────────────────────┤
|
||||
│ zx_document_core (Rust) │ ← 核心逻辑
|
||||
│ ├─ file_type 文件类型识别 │
|
||||
│ ├─ markdown MD 解析为 DocumentBlock │
|
||||
│ ├─ text TXT 读取 │
|
||||
│ ├─ image_meta 图片 metadata │
|
||||
│ ├─ epub EPUB 结构解析 │
|
||||
│ ├─ pdf PDF 位置模型 │
|
||||
│ ├─ progress ReadingPosition │
|
||||
│ ├─ events ReadingEvent │
|
||||
│ ├─ search 基础搜索 │
|
||||
│ └─ anchors NoteAnchor │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 职责边界
|
||||
|
||||
### Rust Core 负责
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| 文件类型识别 | 根据 magic bytes → MIME → 扩展名判断 MaterialType |
|
||||
| Markdown 解析 | 读取 .md 文件,输出 DocumentBlock 列表 |
|
||||
| TXT 读取 | 读取 .txt 文件,输出段落/行 block |
|
||||
| 图片 metadata | 读取宽高、格式、文件大小 |
|
||||
| 阅读位置 | 提供统一的 ReadingPosition(跨格式) |
|
||||
| 阅读事件 | 生成 MaterialOpened/Closed/PositionChanged/Heartbeat 事件 |
|
||||
| 搜索 | 大小写不敏感,返回 block/snippet |
|
||||
| 笔记锚点 | 从 ReadingPosition 生成 NoteAnchor |
|
||||
| FFI 绑定 | 通过 UniFFI 暴露 API 给 Swift/Kotlin |
|
||||
|
||||
### 宿主 App 负责
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| 网络请求 | COS 下载、API 调用、事件上报 |
|
||||
| Token 管理 | 用户登录、JWT 存储 |
|
||||
| UI 渲染 | SwiftUI/Compose 渲染 DocumentBlock |
|
||||
| 系统预览 | PDF/Office 通过 PDFKit/QuickLook 预览 |
|
||||
| 文件选择 | 系统文件选择器 |
|
||||
| 分享/删除 | 系统分享面板、文件管理 |
|
||||
|
||||
### Rust Core 明确不负责
|
||||
|
||||
- 不做网络请求
|
||||
- 不保存 Token
|
||||
- 不直接访问后端 API
|
||||
- 不做 AI / RAG 解析
|
||||
- 不做向量化
|
||||
- 不做 Office 高保真预览
|
||||
- 不做 OCR
|
||||
- 不做 PDF 标注
|
||||
- 不做富文本编辑器
|
||||
- 不做完整 UI
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
1. App 下载文件到本地(COS → 沙盒)
|
||||
2. App 调用 detect_material_type(file_path)
|
||||
3. 根据 MaterialType 决定预览方式:
|
||||
- NativeReader → 调用 open_document → get_xxx_blocks → 原生渲染
|
||||
- PlatformPreview → iOS QuickLook / Android 系统预览
|
||||
- ExternalOpen → 打开外部 App
|
||||
4. 阅读过程中 App 调用 update_reading_position / heartbeat
|
||||
5. App 定期调用 export_pending_events → 上传到后端
|
||||
```
|
||||
|
||||
## Crate 拆分
|
||||
|
||||
| Crate | 类型 | 用途 |
|
||||
|-------|------|------|
|
||||
| zx_document_core | library | 核心 Rust 逻辑,纯计算 |
|
||||
| zx_document_ffi | library | UniFFI 绑定,类型转换 |
|
||||
| xtask | binary | 构建脚本,生成 binding,打包 artifact |
|
||||
107
docs/event-protocol.md
Normal file
107
docs/event-protocol.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Reading Event Protocol
|
||||
|
||||
## 概述
|
||||
|
||||
Rust Core 生成阅读事件,App 负责收集和上传。这是一个单向数据流:Rust → App → Backend。
|
||||
|
||||
## 事件类型
|
||||
|
||||
```rust
|
||||
enum ReadingEvent {
|
||||
MaterialOpened { material_id, timestamp_ms },
|
||||
MaterialClosed { material_id, timestamp_ms, active_seconds },
|
||||
PositionChanged { material_id, position, timestamp_ms },
|
||||
Heartbeat { material_id, active_seconds, position?, timestamp_ms },
|
||||
MarkedAsRead { material_id, timestamp_ms },
|
||||
}
|
||||
```
|
||||
|
||||
## 事件说明
|
||||
|
||||
### MaterialOpened
|
||||
|
||||
用户打开一份资料时触发。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "material_opened",
|
||||
"material_id": "abc123",
|
||||
"timestamp_ms": 1717100000000
|
||||
}
|
||||
```
|
||||
|
||||
### MaterialClosed
|
||||
|
||||
用户关闭资料时触发,`active_seconds` 为本次打开的累计活跃秒数。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "material_closed",
|
||||
"material_id": "abc123",
|
||||
"timestamp_ms": 1717100300000,
|
||||
"active_seconds": 285
|
||||
}
|
||||
```
|
||||
|
||||
### PositionChanged
|
||||
|
||||
用户滚动/翻页/缩放导致阅读位置变化。频率由 App 控制(建议每 5 秒或停止交互后触发)。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "position_changed",
|
||||
"material_id": "abc123",
|
||||
"timestamp_ms": 1717100100000,
|
||||
"position": {
|
||||
"type": "markdown",
|
||||
"block_id": "heading-3",
|
||||
"scroll_progress": 0.45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Heartbeat
|
||||
|
||||
定时心跳,用于计算阅读时长。即使位置未变化也应周期性触发(建议 10-15 秒)。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "heartbeat",
|
||||
"material_id": "abc123",
|
||||
"timestamp_ms": 1717100150000,
|
||||
"active_seconds": 15,
|
||||
"position": null
|
||||
}
|
||||
```
|
||||
|
||||
### MarkedAsRead
|
||||
|
||||
用户手动标记已读。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "marked_as_read",
|
||||
"material_id": "abc123",
|
||||
"timestamp_ms": 1717100400000
|
||||
}
|
||||
```
|
||||
|
||||
## 事件收集流程
|
||||
|
||||
```text
|
||||
1. App 打开资料 → Rust 生成 MaterialOpened
|
||||
2. App 启动定时器(~15s)
|
||||
3. 每次定时器触发 → Rust 生成 Heartbeat
|
||||
4. 用户交互(滚动/翻页)→ App 调用 update_position → Rust 生成 PositionChanged
|
||||
5. App 关闭资料 → Rust 生成 MaterialClosed(含 active_seconds)
|
||||
6. App 定期调用 export_pending_events() 获取所有未导出事件
|
||||
7. App POST 事件到后端 /reading/events
|
||||
8. App 调用 clear_exported_events() 清空缓冲区
|
||||
```
|
||||
|
||||
## App 侧实现要点
|
||||
|
||||
- 事件应在本地缓存(内存队列),不要一次传一个
|
||||
- 批量上传(每次 5-20 条)或定时上传(每 30s)
|
||||
- 网络失败时保留队列,下次重试
|
||||
- 离线时事件不丢失,恢复网络后继续上传
|
||||
102
docs/ios-integration.md
Normal file
102
docs/ios-integration.md
Normal file
@ -0,0 +1,102 @@
|
||||
# iOS Integration Guide
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述如何将 `zhixi-document-runtime` 集成到 iOS App 中。
|
||||
|
||||
## 构建 Rust 库
|
||||
|
||||
```bash
|
||||
# 安装 Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# 添加 iOS target
|
||||
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
|
||||
|
||||
# 构建
|
||||
cargo build --release --target aarch64-apple-ios
|
||||
cargo build --release --target aarch64-apple-ios-sim
|
||||
```
|
||||
|
||||
## 生成 Swift Binding
|
||||
|
||||
```bash
|
||||
# 通过 xtask 生成
|
||||
cargo run -p xtask -- generate-bindings
|
||||
|
||||
# 或手动通过 UniFFI
|
||||
uniffi-bindgen generate \
|
||||
crates/zx_document_ffi/src/zx_document.udl \
|
||||
--language swift \
|
||||
--out-dir bindings/ios/generated
|
||||
```
|
||||
|
||||
## XCFramework
|
||||
|
||||
```bash
|
||||
# 通过 scripts 构建
|
||||
./scripts/build-ios.sh
|
||||
|
||||
# 输出
|
||||
# bindings/ios/ZxDocumentRuntime.xcframework/
|
||||
```
|
||||
|
||||
## 引入 XCFramework
|
||||
|
||||
1. 将 `ZxDocumentRuntime.xcframework` 拖入 Xcode 项目
|
||||
2. 将生成的 Swift 文件添加到项目
|
||||
3. 确保 `Frameworks, Libraries, and Embedded Content` 中包含 framework
|
||||
|
||||
## Swift 调用示例
|
||||
|
||||
```swift
|
||||
import ZxDocumentRuntime
|
||||
|
||||
// 文件类型识别
|
||||
let materialType = try detectMaterialType(filePath: "/path/to/file.md")
|
||||
print("Type: \(materialType)")
|
||||
|
||||
// 打开文档
|
||||
let doc = try openDocument(filePath: "/path/to/file.md", materialId: "abc123")
|
||||
let info = try getDocumentInfo(handle: doc)
|
||||
print("Title: \(info.title)")
|
||||
|
||||
// Markdown blocks
|
||||
let blocks = try getMarkdownBlocks(handle: doc)
|
||||
for block in blocks {
|
||||
print("[\(block.id)] \(block)")
|
||||
}
|
||||
|
||||
// 搜索
|
||||
let results = try searchDocument(handle: doc, query: "学习")
|
||||
for r in results {
|
||||
print("Found at \(r.blockId): \(r.snippet)")
|
||||
}
|
||||
|
||||
// 更新阅读位置
|
||||
let position = ReadingPosition.markdown(
|
||||
blockId: "heading-3",
|
||||
scrollProgress: 0.45
|
||||
)
|
||||
try updateReadingPosition(materialId: "abc123", position: position)
|
||||
|
||||
// 导出阅读事件
|
||||
let events = try exportPendingEvents()
|
||||
for event in events {
|
||||
print("Event: \(event)")
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 编译错误
|
||||
|
||||
- 确保 Rust target 已安装
|
||||
- 确保 XCFramework 的 `Build Phase` / `Link Binary with Libraries` 已包含
|
||||
- 检查 `Library Search Paths` 包含 framework 路径
|
||||
|
||||
### 运行时崩溃
|
||||
|
||||
- 检查文件路径是否存在
|
||||
- 编码问题:确保文件是 UTF-8(TXT/MD)
|
||||
- 大文件:PDF/EPUB 可能内存占用大,考虑后台线程处理
|
||||
72
docs/reading-position-model.md
Normal file
72
docs/reading-position-model.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Reading Position Model
|
||||
|
||||
## 概述
|
||||
|
||||
统一的阅读位置模型,跨所有支持格式。用于记录用户读到哪里、支持继续阅读。
|
||||
|
||||
## ReadingPosition
|
||||
|
||||
```rust
|
||||
enum ReadingPosition {
|
||||
Markdown { block_id: String, scroll_progress: f32 },
|
||||
Text { line_number: u32, scroll_progress: f32 },
|
||||
Pdf { page_number: u32, page_progress: f32, overall_progress: f32 },
|
||||
Image { zoom_scale: f32, offset_x: f32, offset_y: f32 },
|
||||
Epub { chapter_id: String, chapter_progress: f32, overall_progress: f32 },
|
||||
Unknown,
|
||||
}
|
||||
```
|
||||
|
||||
## 各格式说明
|
||||
|
||||
### Markdown
|
||||
|
||||
- `block_id`:对应的 DocumentBlock ID
|
||||
- `scroll_progress`:0.0 ~ 1.0,在该 block 内的滚动比例
|
||||
|
||||
恢复阅读时,App 应滚动到对应 block 的对应位置。
|
||||
|
||||
### Text
|
||||
|
||||
- `line_number`:当前顶部可见行号(1-based)
|
||||
- `scroll_progress`:0.0 ~ 1.0,全文滚动比例
|
||||
|
||||
### PDF
|
||||
|
||||
- `page_number`:当前页码(1-based)
|
||||
- `page_progress`:0.0 ~ 1.0,该页内滚动比例
|
||||
- `overall_progress`:0.0 ~ 1.0,全文进度
|
||||
|
||||
### Image
|
||||
|
||||
- `zoom_scale`:当前缩放倍数(1.0 = 原始尺寸)
|
||||
- `offset_x` / `offset_y`:视口偏移(像素)
|
||||
|
||||
### Epub
|
||||
|
||||
- `chapter_id`:当前章节 ID(来自 spine)
|
||||
- `chapter_progress`:0.0 ~ 1.0,章节内滚动比例
|
||||
- `overall_progress`:0.0 ~ 1.0,全书进度
|
||||
|
||||
## 序列化
|
||||
|
||||
所有位置信息通过 serde 序列化为 JSON,方便 App 读取和上传。
|
||||
|
||||
```json
|
||||
// Markdown 示例
|
||||
{
|
||||
"type": "markdown",
|
||||
"block_id": "heading-3",
|
||||
"scroll_progress": 0.45
|
||||
}
|
||||
```
|
||||
|
||||
## 继续阅读流程
|
||||
|
||||
```text
|
||||
1. App 请求后端:GET /materials/{id}/last-position
|
||||
2. 后端返回 JSON 格式的 ReadingPosition
|
||||
3. App 反序列化为 ReadingPosition
|
||||
4. App 根据 format 类型恢复到对应位置
|
||||
5. 如果无历史位置,从开头开始
|
||||
```
|
||||
59
docs/supported-formats.md
Normal file
59
docs/supported-formats.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Supported Formats
|
||||
|
||||
## PreviewMode
|
||||
|
||||
每种文件格式对应一个 PreviewMode:
|
||||
|
||||
| Mode | 含义 | 由谁渲染 |
|
||||
|------|------|---------|
|
||||
| `NativeReader` | 知习内置阅读器 | App 原生渲染 + Rust 提供 blocks |
|
||||
| `PlatformPreview` | 平台系统预览 | iOS QuickLook / Android 系统能力 |
|
||||
| `ExternalOpen` | 外部 App 打开 | 用户选择外部应用 |
|
||||
| `Unsupported` | 暂不支持 | 显示提示 |
|
||||
|
||||
## 格式矩阵
|
||||
|
||||
### 第一版(当前优先级)
|
||||
|
||||
| 格式 | 扩展名 | PreviewMode | Rust 职责 | App 职责 |
|
||||
|------|--------|-------------|----------|---------|
|
||||
| Markdown | `.md` | NativeReader | 解析为 DocumentBlock | 原生渲染 block 列表 |
|
||||
| 纯文本 | `.txt` | NativeReader | 读取内容,分段落/行 | 原生文本渲染 |
|
||||
| PDF | `.pdf` | PlatformPreview | 定义阅读位置模型 | iOS PDFKit / Android 系统 |
|
||||
| PNG | `.png` | NativeReader | metadata(宽高/格式) | 原生图片查看 |
|
||||
| JPEG | `.jpg` `.jpeg` | NativeReader | metadata | 原生图片查看 |
|
||||
| WebP | `.webp` | NativeReader | metadata | 原生图片查看 |
|
||||
| GIF | `.gif` | NativeReader | metadata | 原生图片查看 |
|
||||
| Word | `.doc` `.docx` | PlatformPreview | 不解析 | QuickLook / 系统预览 |
|
||||
| Excel | `.xls` `.xlsx` | PlatformPreview | 不解析 | QuickLook / 系统预览 |
|
||||
| PPT | `.ppt` `.pptx` | ExternalOpen | 不解析 | 外部 App 打开 |
|
||||
|
||||
### 后续支持
|
||||
|
||||
| 格式 | 目标 PreviewMode | 备注 |
|
||||
|------|-----------------|------|
|
||||
| EPUB | NativeReader | Rust 解析 OPF/spine/nav,App WebView 渲染章节 |
|
||||
| PDF | NativeReader(增强) | 评估 PDFium,支持文本提取和搜索 |
|
||||
|
||||
### 明确不做
|
||||
|
||||
- OCR:不做图片文字识别
|
||||
- PDF 标注:不做高亮/划线/批注
|
||||
- Office 内置高保真预览:不解析 Word/Excel/PPT 排版
|
||||
- 复杂富文本:不做格式保留
|
||||
- DRM 电子书:不做
|
||||
- Kindle 格式:不做
|
||||
|
||||
## 识别策略
|
||||
|
||||
```text
|
||||
1. magic bytes(文件头,最可靠)
|
||||
2. MIME type(根据扩展名推测)
|
||||
3. 文件扩展名(兜底)
|
||||
```
|
||||
|
||||
使用 `infer` crate 做 magic bytes 检测,`mime_guess` 做 MIME 推测。
|
||||
|
||||
## 未知格式
|
||||
|
||||
无法识别的文件返回 `MaterialType::Unknown` + `PreviewMode::Unsupported`。App 侧应显示友好提示,不崩溃。
|
||||
Loading…
x
Reference in New Issue
Block a user