Files
mnote/design/local-first-desktop-client.md
T
2026-01-15 20:54:21 +08:00

395 lines
20 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.
# 本地优先(Local-first)Win 客户端改造方案(不依赖客户端 Docker)
## 1. 背景与目标
你已明确选择“方案 2”:以 **丝滑体验** 为第一优先级,把主要交互(新建/删除/打开/编辑/附件管理/文件树)尽可能做成 **本地零延迟**;在线能力只承担 **多设备同步、鉴权、RAG/OCR 结果回传** 等“不需要即时展示”的部分。
同时你希望:
- **Windows 客户端可打包分发**(家人/同事无需安装 Docker)
- 后续可扩展:安卓端可以继续使用网页版(或再做安卓壳/客户端)
- 现有服务端栈(Supabase/LightRAG/Redis/MinerU/OnlyOffice)在需要时仍可使用,但不强制落到每个客户端
## 2. 现状盘点(基于当前仓库)
- 当前仓库根目录的 `pnpm run desktop` / `pnpm run desktop:hot` 本质是“一键启动脚本”,会启动:
- `wolai-frontend`Next.js
- `wolai-backend`FastAPI
- `services/ingest_service`(自动入库/清理/触发)
- `services/rag_gateway`RAG 查询网关)
- 这不是可分发的“桌面客户端”:仍依赖 Node/Python 环境、端口服务、配置文件,且前端资源与 API 仍走 HTTP。
结论:要实现你要的“丝滑 + 易安装”,需要把“桌面壳 + 本地数据层 + 后台任务/同步”做成独立产品形态,而不是继续依赖 Cloudflare 反代公网访问。
## 3. 推荐总体架构(分层 + 可演进)
### 3.1 角色划分
**A. 桌面客户端(Windows,主力)**
- UI:本地窗口(WebView/Electron)加载本地页面(不经公网)
- 本地数据层:本地数据库(建议 SQLite)+ 本地文件目录(附件/导入文件)
- 本地任务队列:后台线程/进程异步执行(避免 UI 卡顿)
- 网络:只做“同步/索引请求/鉴权”,且尽量后台化
**B. 家庭/办公室“轻服务端”(可选,但强烈建议)**
你目前已有 Docker 相关服务,更适合放到一台“常开机器”(NAS/迷你主机/家用服务器)上,而不是每个客户端:
- Supabase(鉴权/同步/共享数据/存储桶/pgvector
- Redis(队列/后台任务)
- LightRAG(入库/检索)
- MinerUOCR
- OnlyOffice Document Server(如仍需要网页内编辑 Office)
**C. 公网访问(可选)**
- 安卓端/外网访问时,继续使用网页版(可走 Cloudflare、或 VPN/IPv6
- 但桌面端的“主要交互体验”不再依赖公网链路
### 3.2 本地优先的数据归属
核心原则:**“能本地完成的操作,绝不阻塞在网络请求上。”**
- 文档/思维导图的“当前版本内容”:本地可用(SQLite + 文件)
- 附件文件本体:本地可用(本地目录),需要时再同步/上传
- 远端(Supabase)只承担:
- 多设备同步与共享(最终一致)
- RAG/OCR 的产物存储与检索
- 登录鉴权(如你希望跨设备一致账号体系)
## 4. 技术路线:桌面壳怎么做(不跑 Docker)
你现在的前端是 Next.js(含 Server Components + API Routes),要做桌面客户端通常有两条路线:
### 路线 1(推荐落地优先):桌面壳 + 本地 HTTP(保持现有 Next/FastAPI
- 桌面壳:Tauri 2 或 Electron(两者都能)
- 客户端启动时:
1) 启动本机 `wolai-frontend`(生产模式)到 `127.0.0.1:3000`
2) 启动本机 `wolai-backend`(或逐步把关键 API 收敛到一个本地服务)
3) 桌面壳打开 `http://127.0.0.1:3000`(同源、零公网)
优点:
- 改造小:最大复用当前 Next/FastAPI
- 体验立刻提升:不再经过 Cloudflare/Tunnel
缺点:
- “安装体积/依赖”需要进一步工程化(见第 8 节分阶段)
### 路线 2(长期更优雅):本地壳 + 静态前端 + 本地 API(逐步去 Next Server 依赖)
- 把前端逐步改成更“纯前端”的形态(或迁 Vite/React SPA
- 本地 API 用一个可打包的二进制提供(Rust/Go+ SQLite
优点:
- 最终安装最轻、启动更快、可控性最好
缺点:
- 改造量大,不建议作为第一阶段目标
**建议决策**:先走“路线 1”把体验做顺,再按收益逐步往“路线 2”靠拢。
## 5. “丝滑”的关键:本地任务队列 + 最终一致同步
### 5.1 本地操作必须立刻返回
把下列操作改为“本地先完成,后台再同步”:
- 新建/重命名/移动/删除(进入回收站)
- 打开文档(本地读)
- 编辑器保存(本地落盘 + 去抖)
- 附件导入(本地复制到附件目录,立即可用)
### 5.2 同步模型(建议:Oplog/变更日志)
为避免“每次都全量上传大字段”导致卡顿,建议本地维护一个 `oplog`
- 每个操作写入:`op_id、entity_type、entity_id、op_type、payload、created_at、synced_at`
- 后台同步器按顺序上传到 Supabase(或服务端)
- 冲突策略(第一阶段先简单):
- 同一文档若出现冲突:保留双方版本(生成“冲突副本”),避免覆盖丢数据
### 5.3 建议的本地 SQLite 最小表结构(用于落地与排期)
> 目标:先把“本地秒开 + 不依赖网络”的体验做出来;同步与协作逐步增强。
- `local_documents`
- `id`uuid)、`parent_id``title``content_json`(或文件路径)、`updated_at``deleted_at`
- `local_media_assets`
- `id``document_id``file_path``mime``sha256``size``created_at``deleted_at`
- `local_mindmaps`
- `id``document_id`(可选)、`data_json``updated_at``deleted_at`
- `local_trash_jobs`
- `entity_type``entity_id``purge_after`(时间戳)、`canceled_at`
- `local_oplog`
- `op_id``entity_type``entity_id``op_type``payload_json``created_at``synced_at``retry_count`
- `local_sync_state`
- `remote_cursor`(用于增量拉取)、`last_pull_at``last_push_at`
### 5.4 与当前 Supabase 表的映射(基于 supabase_local 现有表)
你当前 Supabase`public` schema)已经有这些关键表:`documents``media_assets``mindmap_meta`/`mindmap_nodes``rag_index_sources``lightrag_*``document_embeddings` 等。
建议映射策略(第一阶段):
- 本地 `local_documents` <-> 远端 `public.documents`
- 远端作为“同步与共享的汇聚层”,本地为“即时交互层”
- 本地 `local_media_assets` <-> 远端 `public.media_assets` + `storage.objects`
- 远端文件本体尽量走 Storage(签名 URL/直传),避免经 API 中转
- 思维导图:本地 `local_mindmaps` <-> 远端 `mindmap_meta`/`mindmap_nodes`
- 目前“思维导图删除后垃圾桶没有出现”,本质是缺少统一的 `deleted_at` 与回收站视图
- 建议把 mindmap 也纳入统一软删体系(与 documents/media_assets 同标准)
## 6. 回收站 + 延迟删除(解决误删与垃圾堆积)
你提出的需求非常关键:误删后 `Ctrl+Z` 可恢复,所以“删文件”不应立即删 OCR/RAG 资源。
建议统一成两段式删除:
1) **软删(立刻)**:进入回收站
- 本地:移动到 `.trash/`(或打标 `deleted_at`),UI 可见
- 服务端:对应记录打 `deleted_at`(不做物理删除)
2) **延迟清理(例如 10 分钟后)**:后台任务执行硬删除
- 若用户在延迟窗口内恢复:取消清理任务
- 手动“清空垃圾桶”:触发立即清理(仍建议带二次确认)
注意:你现有的报错 `rag_index_sources` 外键约束,说明服务端“删除顺序/级联规则”需要统一:
- 设计层面建议:对 RAG 相关表引入 `ON DELETE CASCADE` 或“先删子表再删父表”的硬规则
- 实现层面建议:垃圾桶清理走同一个 `purge` 流程,确保顺序一致
基于你当前的约束信息:`rag_index_sources_document_id_fkey``ON DELETE``NO ACTION`(也就是不会级联删除),因此“清空垃圾桶”若直接删 `documents` 就必然触发外键错误。这里推荐两条修复路径(二选一):
1) **保留外键 NO ACTION**:清理时严格按顺序删除子表(例如先删 `rag_index_sources`、再删 `documents`
2) **改为 ON DELETE CASCADE**:允许删除 `documents` 时自动清理 `rag_index_sources`(更省心,但要确认不会误删共享/审计数据)
## 7. RAG/OCRMinerU)在本地优先架构中的位置
你的期望是:客户端体验优先,OCR/RAG 可以后台慢慢做。因此推荐:
- **客户端只负责触发与展示结果**(不在客户端跑 Docker)
- **OCR/RAG 在“轻服务端”执行**(可继续用你现有 Docker/服务)
### 7.1 触发策略(建议)
当本地文件发生变化(导入/更新):
- 计算 `content_hash`(用于去重与增量)
- 将任务写入本地队列(或远端队列)
- 后台上传文件到服务端(或 Supabase Storage
- 服务端按文件类型处理:
- PDF/图片型 PDFMinerU OCR -> 结构化文本
- docx/pptx/xlsx:提取文本(必要时转 pdf 再 OCR)
- 图片:OCR + 元数据
- 最终写入:标准化 chunk + embedding + 元数据(Supabase/pgvector
### 7.2 Embedding 建议(中英混合 + 化学语料,尽量免费)
优先推荐“可自托管/本地跑”的 embedding(服务端跑即可):
- `bge-m3`:多语种通用表现稳定,适合中英混合
- `multilingual-e5-large`(或同系):检索向 embedding 生态成熟
- 你现有 `Ollama` 方案里预拉的 `qwen3-embedding:8b`:可作为可用基线(但模型较大,CPU 可能慢,建议服务端有 GPU 或更强 CPU)
第一阶段建议先选 1 个跑通“自动入库闭环”,再用你提供的化学书籍片段做小规模对比评测(Recall@k + 人工判读)。
### 7.3 任务编排建议(复用现有 Supabase 表)
你当前 Supabase 已有两张非常适合做“可观测后台任务”的表:
- `public.rag_index_sources`:有 `status/attempts/last_error/source_updated_at/last_enqueued_at/last_processed_at`
- `public.background_tasks`:有 `task_type/status/progress/message/updated_at`
建议用法:
1) **入库入口统一**:任何“需要入库/重建索引”的源(documents/media_assets/mindmap/ocr_text)都落一条 `rag_index_sources`
2) **执行过程可视化**:真正跑 OCR/切块/embedding/入库时,同步写 `background_tasks`(用于前端显示进度条/日志)
3) **幂等与去重**
-`source_updated_at` + `content_hash`(可在 payload 里存)判断是否需要重跑
- 同一 `source_id` 新任务到来时,若已有 `RUNNING` 则只更新 `last_enqueued_at`,避免并发重复跑
### 7.4 OCR 决策(PDF 有文本 vs 图片型 PDF)
针对你提供的两类测试 PDF(“已 OCR 过含文本”与“图片型 PDF”),建议服务端在入库前做一个轻量判断:
- 先尝试“抽取文本”(例如每页抽样、统计可抽取字符数)
- 若字符数/覆盖率低于阈值,再走 MinerU OCR
- OCR 产物建议保留:
- `raw_text`(纯文本,用于检索)
- `layout`(段落/页码/坐标等,用于引用定位与高亮)
这样可以避免对“本来就有文本层的 PDF”重复 OCR,节省大量时间。
### 7.5 LightRAG 版本对齐(你提到的 v1.4.9.10)
当前仓库内的 `LightRAG` 代码与其自带虚拟环境显示版本为 `1.4.9.9``LightRAG/lightrag/__init__.py``LightRAG/.venv` 一致)。
建议策略:
- **先稳**:在“本地优先客户端”主线落地前,LightRAG 不做大范围升级,避免把变量叠加到体验问题上
- **再对齐**:单独开一个“LightRAG 升级分支计划”(仅改 LightRAG 目录/服务启动脚本/兼容性),升级到你说的 `v1.4.9.10` 后,用你提供的两份测试 PDF 跑一遍入库回归(含 OCR 分支)
## 8. 分阶段落地路线(建议按收益/难度)
### P0(最小可用,立刻变丝滑)
- 明确“桌面端默认走本机”:`127.0.0.1` / 局域网 IP
- 桌面端不经过 CloudflareCloudflare 仅保留给“手机/外网”)
- 这一步不解决“安装依赖”,但先把“体验问题”排除在公网链路之外
### P1(可分发 Win 客户端:无需 Docker
目标:家人/同事拿到安装包即可用(最多只安装一次 VC 运行库/WebView2)。
- 引入桌面壳(Tauri 或 Electron
- 把必要服务以“sidecar”方式随客户端分发并由壳统一拉起/关闭:
- Next 前端(建议 next standalone 输出 + 内置 Node
- 后端 API(优先收敛成一个本地服务;Python 可先保留,后续再二进制化)
- 本地数据层落地:SQLite + 本地文件目录
### P2(减少依赖与体积)
- Python 服务逐步二进制化(PyInstaller/Nuitka 或重写关键路径为 Rust/Go
- 能迁到本地 API 的 Next `app/api/*` 逐步迁移,减少 Next server 负担
### P3(多端)
- 安卓端继续用网页版(外网可用 VPN/IPv6/Cloudflare 其一)
- 桌面端与网页版共享同一套“同步协议/数据模型”,但桌面端以本地为主
## 9. 你需要做的关键取舍(我建议的默认值)
1) **OnlyOffice 的定位**
- 如果你追求“打开速度极致”:Windows 客户端对 docx/xlsx/pptx 优先“用本机 Office/OnlyOffice Desktop 打开”,应用内只做预览/管理/版本
- OnlyOffice Document Server 保留给:手机端/外网网页端
2) **服务端部署位置**
- 若你有一台常开机器:把 Supabase/LightRAG/MinerU/Redis/OnlyOffice 放到 LAN 里,桌面端访问延迟会非常低
- 外网访问建议优先 VPN(你是小范围用户),避免 Cloudflare 造成的不可控绕路
## 10. 下一步我建议你先确认的 5 个问题(确认后我再给出更“可执行”的工程拆分)
1) Windows 客户端你更偏好:`Tauri`(小体积)还是 `Electron`(生态成熟)?
2) 你是否接受“第一阶段仍需要安装 Node/Python”,还是必须“一键安装无运行时依赖”?
3) 你希望本地数据目录放哪:`%USERPROFILE%\\Documents\\MNOTE` 还是跟仓库同级?
4) 多人同时编辑是否重要?(决定是否引入 Yjs 协作服务端/冲突策略)
5) 你的“轻服务端”准备放在哪台机器(家用主机/NAS/云服务器)?
## 11. 你已确认的项目决策(已记录,后续按此推进)
你已明确选择:
1) 桌面壳:`Electron`
2) 依赖:可以安装任何东西,但希望“安装后即可启动客户端”(减少手工配置)
3) 本地数据:放在**安装目录**(用户上传文件先落到该目录,再与远程 Supabase 同步)
4) 协作:暂不做多人同时编辑
5) 轻服务端:当前家用主机(先这样)
### 11.1 关键风险提醒:Windows 的“安装目录”通常不可写
如果你用的是常见的安装方式(MSI/NSIS 默认装到 `C:\\Program Files\\...`),Windows 会对普通用户进程启用 UAC/权限限制,**安装目录默认不可写**,会导致:
- 上传文件/本地数据库写入失败或变成“时好时坏”
- 需要管理员权限运行(体验差、也不安全)
你已进一步明确:**标准安装**,但安装位置必须可选且尽量不在 C 盘(数据会越来越大)。
这里的关键点不在“C 盘 vs 非 C 盘”,而在“是否安装在受保护目录(如 Program Files)”。即使装到 `D:\\Program Files\\...`,也可能同样不可写。
为满足“标准安装 + 数据放安装目录 + 不在 C 盘”,我建议按下面规则实现(作为默认方案):
- **安装路径强约束**:安装器允许用户自选安装目录,但**禁止选择 C 盘**,并且默认推荐路径形如 `D:\\Apps\\MNOTE`(而不是 `D:\\Program Files\\...`)。
- **数据目录固定在安装目录内**:例如 `D:\\Apps\\MNOTE\\data`(满足“数据跟安装目录走”)。
- **安装时设置 data 目录 ACL**:安装器在安装阶段为 `data` 目录授予当前用户(或 Users 组)可写权限,避免运行时写入失败。
> 如果你希望“强制必须装到非 C 盘”,这是可做的;但如果同事机器没有 D 盘,也需要允许选择 E/F 等其它盘符。
结论:你不需要做“绿色版”,也不需要把数据放到 `userData`;但必须在安装器层面把“安装目录可写”这件事做成默认成立。
## 12. Electron 客户端落地方案(不让客户端跑 Docker)
### 12.1 本地优先的最小形态(推荐作为第一阶段实现)
Electron 只做三件事:
- 提供 UI(窗口内渲染网页)
- 提供本地数据目录与 SQLite(零延迟读写)
- 后台做“同步/上传/触发服务端入库”(不阻塞 UI)
客户端**不运行 Docker**,也不强制在客户端跑 LightRAG / MinerU / Supabase。
### 12.2 与现有代码的衔接方式(尽量少改,先跑通)
第一阶段建议继续复用你现有的 HTTP 形态,但把链路从“公网 Cloudflare”改为“本机/局域网”:
- Electron 启动时拉起本机服务(作为 sidecar):
- `wolai-frontend`Next production(本机 `127.0.0.1:3000`
- `wolai-backend`FastAPI(本机 `127.0.0.1:8000`
- Electron 窗口直接打开 `http://127.0.0.1:3000`
这样 UI 完全不走公网,页面切换/打开文档会立刻变“本地速度”。
> 说明:你仓库根目录的 `pnpm run desktop` 目前更像“开发者一键启动”;后续我们会新增专用的“Electron sidecar 启动器”,把端口探测、日志、崩溃重启、退出清理做成稳定产品级行为。
### 12.3 依赖与打包策略(让“安装后可启动”)
你允许安装任何东西,但希望安装后可启动。我建议用“两阶段”逐步降低门槛:
**阶段 1(最快落地)**
- 安装前置:Node、Python(以及必要的 VC 运行库、WebView2
- Electron installer 在首次启动时做自检(缺什么就引导安装)
**阶段 2(更像产品)**
- 把后端打成单文件/少量文件的可执行程序(PyInstaller/Nuitka
- 把前端打成 Next standalone(或更进一步变成静态 + 本地 API)
- Electron 安装包内置所有运行时(用户无需额外安装 Node/Python
## 13. 轻服务端(家用主机)承担什么
鉴于你暂时不做多人实时协作,且优先“本地丝滑”,建议家用主机只承担:
- Supabase:鉴权 + 多端同步汇聚层 + Storage + pgvector
- ingest_service:监听/轮询需要入库的源(`rag_index_sources`),做切块/embedding/入库
- MinerUOCR 工作负载(图片/图片型 PDF)
- LightRAG:统一入库/检索(或作为 ingest_service 内部依赖)
- Redis:任务队列/限流(可选)
- OnlyOffice Document Server:主要给“网页/手机端”使用;Windows 客户端可优先走本机 Office/OnlyOffice Desktop 提升打开速度
## 14. 下一步里程碑(可直接开工的任务清单)
### M0:确定安装与数据目录规则(必须先定)
- 标准安装(NSIS/MSI),允许用户选择安装路径
- 默认路径推荐 `D:\\Apps\\MNOTE`
- 禁止安装到 C 盘(可配置为硬限制或安装器强提示)
- 数据目录固定为 `$INSTDIR\\data` 并在安装时设置写权限
### M1Electron 外壳 + 本机服务启动(体验立竿见影)
- 新增 `electron-app/`(主进程 + 渲染进程)
- 主进程实现:
- 启动前端(Next production)到 `127.0.0.1:3000`
- 启动后端(FastAPI)到 `127.0.0.1:8000`
- 进程托管:崩溃重启、退出清理、日志落盘(UTF-8)
- 渲染进程直接加载 `http://127.0.0.1:3000`
### M2:本地数据目录与 SQLite(真正 local-first
- 选定数据根目录(绿色版:`./data`;标准版:`userData`
- 引入 SQLite(文档/附件索引/oplog/回收站任务)
- 把“创建/删除/打开”的关键路径改为本地优先(网络失败也能操作)
### M3:同步(最终一致)
- 本地 `local_oplog` 推送到 Supabase
- 从 Supabase 拉取增量变更,更新本地 SQLite
- 冲突先采用“生成冲突副本”的保守策略
### M4:入库/OCR 后台化(不阻塞 UI)
- 客户端只负责:上传文件 + 写 `rag_index_sources`(或触发 API
- 家用主机跑 ingest_service/MinerU/LightRAG 完成入库
- 客户端订阅/轮询 `background_tasks` 显示进度(可选)