diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..feb8851 --- /dev/null +++ b/docs/architecture.md @@ -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 | diff --git a/docs/event-protocol.md b/docs/event-protocol.md new file mode 100644 index 0000000..f51e467 --- /dev/null +++ b/docs/event-protocol.md @@ -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) +- 网络失败时保留队列,下次重试 +- 离线时事件不丢失,恢复网络后继续上传 diff --git a/docs/ios-integration.md b/docs/ios-integration.md new file mode 100644 index 0000000..98f34cd --- /dev/null +++ b/docs/ios-integration.md @@ -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 可能内存占用大,考虑后台线程处理 diff --git a/docs/reading-position-model.md b/docs/reading-position-model.md new file mode 100644 index 0000000..1aa23e5 --- /dev/null +++ b/docs/reading-position-model.md @@ -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. 如果无历史位置,从开头开始 +``` diff --git a/docs/supported-formats.md b/docs/supported-formats.md new file mode 100644 index 0000000..3d7cbd1 --- /dev/null +++ b/docs/supported-formats.md @@ -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 侧应显示友好提示,不崩溃。