docs: architecture, supported-formats, event-protocol, reading-position, ios-integration

This commit is contained in:
wangdl 2026-05-30 20:16:51 +08:00
parent 086006a252
commit 5f34b871ba
5 changed files with 424 additions and 0 deletions

84
docs/architecture.md Normal file
View 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
View 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
View 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-8TXT/MD
- 大文件PDF/EPUB 可能内存占用大,考虑后台线程处理

View 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
View 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/navApp 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 侧应显示友好提示,不崩溃。