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