Files
mnote/recycle/design/convex-mir-design
T
2026-04-13 19:21:42 +08:00

365 lines
38 KiB
Plaintext
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.
可以,按你选的 1A/2B/3B,我建议的落地方案是:
- Convex:自托管 Docker Compose(官方)
- 对象存储:MinIO(S3 兼容)
- 鉴权:固定“开发用户”(仅开发态),所有权限校验先按这个用户走
- 后端:不再以 FastAPI/Celery 为主链路,长任务改为 Convex action + scheduler;如后续需要 OCR/RAG 重计算,再让 Convex
action 去调用现有 services/* HTTP 服务
下面是我建议的“可执行迁移流程设计”(按阶段推进,每一阶段都能跑通):
———
## 一、部署方案(Windows 本机)
### A. 运行组件
1. convex-backend(端口:3210 + 3211 actions
2. convex-dashboard(端口:6791
3. minio(端口:9000 S3 API9001 Console
4. minio-init(启动时自动建 bucket
### B. 目录规划(建议)
- infra/convex/
- docker-compose.ymlConvex backend + dashboard + MinIO
- .env(只放本机,不提交;或提交 .env.example
- README.md(写清启动端口与初始化)
### C. 关键环境变量(Convex backend 使用 MinIO 作为 S3 存储)
Convex 自托管支持把导出/导入/模块/用户文件/搜索索引都放到 S3,并允许通过 S3_ENDPOINT_URL 连接 MinIO。你将配置这些
bucket(示例名):
- convex-snapshot-exports
- convex-snapshot-imports
- convex-modules
- convex-user-files
- convex-search-indexes
并在 backend 容器里设置(示例):
- AWS_REGION=us-east-1
- AWS_ACCESS_KEY_ID=<minio key>
- AWS_SECRET_ACCESS_KEY=<minio secret>
- S3_ENDPOINT_URL=http://minio:9000
- S3_STORAGE_EXPORTS_BUCKET=...(以及其余 4 个)
> 这样后续你在 Convex 里用文件能力(上传/存储/取 URL)会自然落到 MinIO,不需要再引入 Supabase Storage 或自写一套 presigned > 上传逻辑。
———
## 二、迁移总体策略(符合“先跑通但都换掉”)
你的项目目前最大的问题不是“后端”,而是数据访问散落在 Next Route Handlers + Supabase。迁移应以 “把 Supabase 数据面替换成
Convex” 为主线,同时把鉴权先简化为固定用户。
我建议采用“门面不变、内核替换”的方式:
- 保留现有 wolai-frontend/src/app/api/**/route.ts 路由不变(前端 UI 不用立刻大改)
- 逐个把这些 route handler 内部从 supabase.* 改为 convex query/mutation/action
- 用一个总开关 USE_CONVEX=1 控制,方便随时切回 Supabase 对照(你没有用户数据,回退成本也接近 0)
———
## 三、阶段化详细流程(每阶段验收点明确)
### 阶段 0:盘点与冻结范围(半天内)
目标:明确“哪些模块先迁、哪些先不动”,避免一次改爆。
- 以 CODE_INDEX.md 为基准,确认实际运行入口是 wolai-frontend/(不是根目录 src/)。
- 先只迁:documents / workspaces / workspace_members / mindmap / background_tasks(足够跑主流程)
- 暂不迁或延后:luckysheet_ws(你现在也没实际测试/用户)
验收:列出一张迁移模块清单(我可以直接在仓库里生成 infra/convex/MIGRATION.md,你确认后再动代码)。
———
### 阶段 1Convex + MinIO 跑起来(不改业务代码)
目标:本机基础设施 ready。
- 在 infra/convex/ 放置 composedocker compose up -d
- 生成 admin keydocker compose exec backend ./generate_admin_key.sh
- 在 wolai-frontend/.env.local 配:
- CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3210
- CONVEX_SELF_HOSTED_ADMIN_KEY=...
- USE_CONVEX=1
验收:
- Dashboard 能打开:http://localhost:6791
- MinIO Console 能打开:http://localhost:9001
- Convex CLI 能连上自托管(下一阶段会做)
———
### 阶段 2:在 wolai-frontend/ 初始化 Convex 项目(最小 demo
目标:让前端工程具备 convex/ 目录与生成类型的能力。
- 安装 convex 依赖
- 初始化 wolai-frontend/convex/schema + demo function
- 跑一次 npx convex dev(自托管模式,指向 CONVEX_SELF_HOSTED_URL
验收:写一个 ping queryNext.js 页面/route 能调用到并返回结果。
———
### 阶段 3:引入“固定开发用户”与权限骨架(替代 Supabase Auth/RLS
目标:所有数据访问都通过同一套“开发用户上下文”注入,避免到处散落假逻辑。
建议实现方式:
- wolai-frontend/src/lib/auth/devUser.ts
- getDevUser() 固定返回 { userId, email, name }(从 .env.local 读取,默认常量)
- Convex functions 不接受“任意 userId 参数”,而是由 route handler 统一注入(现在固定,未来替换为真实 auth)
权限策略(先最小化):
- 所有写操作:要求 workspace_members 存在(你可以先自动把 dev 用户加入默认 workspace
- 所有读操作:同上
- 未来接入真实 auth 时,只需要把 getDevUser() 替换为 getAuthedUser()Convex 侧的权限函数不变
验收:不依赖 Supabase token,也能跑通“创建默认 workspace + 创建文档”。
———
### 阶段 4:迁移核心数据模型与最小 CRUDdocuments/workspaces
目标:主流程跑通(侧边栏、打开文档、保存)。
- 在 Convex schema 中创建对应集合(建议保留你现有 UUID 作为业务 id 字段,并建立唯一索引,避免 URL/引用大改)
- 实现 queries/mutations
- workspaces.getOrCreateDefaultForUser
- documents.listByWorkspace
- documents.getById
- documents.create
- documents.updateContent(含 raw_text/index_status 等你现在写回字段)
然后改 wolai-frontend/src/app/api/documents/**/route.ts 内部实现:
- 先只改:create/content/save/title/options/list 等最常用路径
- 暂时保留 Supabase 版本分支(USE_CONVEX 开关)
验收:你打开应用后能创建/读取/保存文档(数据落在 Convex),且不再访问 Supabase 表。
———
### 阶段 5:迁移 mindmap 与任务(替换 Celery/trigger 的思路)
目标:把“异步/任务/进度”统一到 Convex。
- background_tasks:做成 Convex 表
- startOcrmutation 创建任务记录,然后 scheduler.runAfter() 触发 action
- ocrPipelineaction 模拟 OCR(对齐你现在 wolai-backend/app/workers/tasks.py 的行为:更新进度、回写文档内容、任务
completed
验收:
- 前端调用“OCR/任务”相关 API 能返回 taskId
- 任务状态能轮询/订阅(先轮询即可)
- 不再需要 wolai-backend 的 Celery 才能完成基本链路
———
### 阶段 6:文件存储(MinIO + Convex file storage
目标:后续做媒体/附件时不会再回到 Supabase Storage。
- 保持 Convex backend 已配置 S3MinIO
- 在 Convex 里实现:
- files.generateUploadUrl
- files.saveMeta(把文件与 document/workspace 关联)
- files.getUrl(用于展示/下载)
验收:能上传一个文件到 Convex file storage(底层落 MinIO),并在页面拿到可访问 URL。
———
到目前为止,你已经跑通了“文档/附件 + Convex 自部署”这条主链路;接下来要做的事,本质上是:把
USE_CONVEX=1 时仍在走 Supabase 的那些模块逐步“替换或下线”,直到项目真的只依赖 Convex。
- 先确认“还剩哪些 Supabase 依赖”
- wolai-frontend/src/ 下仍有大量 Supabase 路由/工具在用:mindmap、referencesRPC)、onlyoffice、
luckysheet、ai-agent、sidebar、search(recent) 等(目前大概率是“Supabase 分支/兜底分支”)。
- services/ingest_service/ 仍通过 Supabase REST 做任务/索引/清理(属于后续 RAG/索引链路)。
- 根目录 src/ 也还有 Supabase 用法(更像历史/备用 Next 工程,若不参与桌面构建可先不动)。
- 按你“尽量一体化绑死 Convex”的优先级,建议下一步这样排
1. 鉴权/权限从“固定用户”升级为可扩展的真实方案:否则后面所有“按用户/工作区隔离”都只能靠约定。
Convex Auth 是官方路线之一,但对 Next.js server 侧支持仍在演进中,需要你接受一定不稳定/适配成
本。citeturn0search1
2. 把仍依赖 Supabase Storage 的功能全部切到 Convex Files:你已经验证 Dashboard Files 可见;下一步
是把 luckysheet/onlyoffice 等涉及上传/签名 URL 的地方也迁掉(或临时 501 下线),保证
USE_CONVEX=1 时不会再触发 Supabase Storage。citeturn0search5
3. 把“搜索/索引/推荐”从 Postgres/RPC 思路迁到 Convex:文档搜索走 Convex 全文检索;RAG/embedding 走 Vector Search(注意向量检索需要在 action 里跑)。citeturn0search2turn0search4
4. 把 services/ingest_service 的“队列/任务状态机”迁出 Supabase:要么先停用该服务;要么改成 Convex
内部的任务表 + actions + scheduler(这样系统更一体化)。
5. 做运维闭环(自部署必需):明确 Docker 卷备份/恢复策略(Convex 也在推进更易用的数据备份/恢复能
力,但你仍应以卷级备份为底线)。citeturn0search3
- 关于“Convex 组件”与你项目的适配结论
- Convex “Components”适合把通用能力(比如协作编辑、鉴权、工作流)做成可插拔模块,并且支持隔离/复
用;你的项目属于“练手快速迭代”,很适合用组件化方式逐块替换 Supabase 逻辑。citeturn1search0
- Files 这一块:Convex 的 Files 更像“一个大池子/大桶”,不强调文件夹;你要的“按用户/工作区区分、路
径/目录视图”,仍建议用业务表字段(workspace_id、user_id、path)来实现管理与展示,这是最贴合你“一 体化 + 不引入外部对象存储”的路线。citeturn0search5
checklist:
- M1(已完成):documents/workspaces/media/search 全链路 Convex 化 + 冒烟回归
- M2(已完成):Mindmap 数据与接口迁移(/api/mindmap/**、/api/mindmap-trash/empty),落到 Convex(优先复用
documents.mindmap_data
- M3(已完成):References(页面引用/反链)迁移(/api/references/record、/api/references/backlinks),补齐目前的 占位实现
- M4(已完成):AI Agent/api/ai-agent/run、/api/ai-agent/client-tool-result)去 Supabase 化(鉴权/读写文档/工
具回调)
- M5(待继续):Online Table / Luckysheet/api/tables/**、/api/luckysheet/**)迁移或在 Convex 模式下先禁用(给
出明确 UI 提示)
- M6(待继续):OnlyOffice/api/onlyoffice/*)迁移或在 Convex 模式下先禁用(同上)
- M7(待继续):服务侧(services/ingest_service 等)去 Supabase 化:任务表/状态机迁到 Convex jobs/actions(或先
停用该链路)
- M8(未开始):鉴权从“固定用户”升级为可扩展方案(仍保持 Convex 一体化)
- M9(未开始):运维闭环:Convex 数据/Files 卷备份恢复、日志与健康检查、启动脚本收敛
我刚做完的(你现在的代码状态)
- 补齐 documents 的最后缺口:wolai-frontend/src/app/api/documents/embed/route.ts:1 已支持 Convex。
- 抽了一个路由侧通用 helperwolai-frontend/src/lib/convex/route.ts:1(统一拿 auth+client)。
- M7 进展:已把“自动入库/LightRAG 触发”迁到 Convex jobs/actions 的最小骨架(支持 document/mindmap/media 三类入库任务),并在 Convex 模式下默认关闭 services/ingest_service 的 Supabase 轮询入库(避免继续依赖 Supabase)。
- 触发入库(开发用):POST wolai-frontend/src/app/api/dev/ingest/enqueue/route.ts:1
- 查询任务:GET wolai-frontend/src/app/api/dev/jobs/demo/route.ts:1(传 id
- Convex action 需要环境变量:LIGHTRAG_URL / LIGHTRAG_API_KEY(运行在 Convex 侧)
- 自动触发(Convex 侧):保存页面/导图会 debounce enqueue 入库任务(wolai-frontend/convex/_utils/ingestJobs.ts:1
- 已移除 ingest_service 内与 Supabase 相关代码(不再包含 Supabase 客户端/配置/轮询入库实现)
- “最近访问”已落库到 Convexwolai-frontend/convex/schema.ts:1 新增 user_recent_pageswolai-frontend/ convex/recents.ts:1 + 接入 wolai-frontend/src/app/api/search/recent/route.ts:1、wolai-frontend/src/
app/api/search/documents/route.ts:1。
- 回归脚本已扩展并跑通:pw-tests/scripts/e2e_convex_smoke.py:1;最新产物 pw-tests/artifacts/convex-
smoke-20260116-194415.png:1、pw-tests/artifacts/convex-smoke-console-20260116-194415.log:1。
接下来我建议从 M2(Mindmap)开始做:它目前是 Convex 模式下仍“纯 Supabase 路由”的最大功能块。你希望优先
做 Mindmap,还是先做 AI Agent/OnlyOffice/表格?
———
## 补充规格:编辑器“移动/嵌入到...”按 wolai 机制复刻(同页嵌入先禁止)
### 0. 结论(本次明确的产品语义)
- “嵌入到...” = **块引用(同步编辑)**:目标页面插入“嵌入引用块”,引用源块;源块仍留在原位置。
- “移动到...” = **移动块本体**:源块(含子树)从当前页面移到目标页面;块 ID 保持不变。
- **同页嵌入先禁止**:当目标页面 = 源块所在页面时,禁止“嵌入到...”(避免递归/复杂边界)。
> 说明:当前代码里(CustomSideMenu/sidebar)做的是“把块变成子页面 + 插入 pageReference”,这不等价于 wolai 的块引用/块移动,需要整体重做。
### 1. 术语
- 源块(sourceBlock):用户在编辑器里选中的那个块(BlockNote block),有稳定 `blockId`。
- 源页面(sourceDoc):包含源块的页面(document)。
- 目标页面(targetDoc):用户在弹窗里选择的页面。
- 块引用(blockReference):一种特殊块,指向 `targetBlockId`,显示/编辑代理到源块。
### 2. 行为规格(可直接转成 pw-tests 验收点)
#### 2.1 “移动到...”(Move
- 触发:块左侧拖拽菜单 → “移动/嵌入到...” → 选“移动到” → 选目标页面。
- 结果:
- 源页面:源块(含 children 子树)消失。
- 目标页面:插入源块子树(MVP 先插在末尾)。
- 不变量:
- 源块 `blockId` 不变(未来引用/链接仍指向同一块)。
- 子树结构保持不变(children 仍挂在源块下)。
- 限制:
- 选择目标页面为当前页面:视为 no-op(提示“已在当前页面”或直接关闭)。
- 无权限/不存在:失败提示。
#### 2.2 “嵌入到...”(Embed
- 触发:块左侧拖拽菜单 → “移动/嵌入到...” → 切换“嵌入到” → 选目标页面。
- 结果:
- 源页面:源块保持原位。
- 目标页面:新增一个 `blockReference`display=embed),`targetBlockId = 源块.blockId`MVP 先插在末尾)。
- 同步编辑:
- 在目标页面的嵌入引用中编辑内容,实际修改的是源块(刷新源页面可见变化)。
- 删除语义:
- 删除目标页面里的引用块,只移除“引用”,不删除源块本体。
- 跳转语义(先对齐 wolai 思路,后续可微调):
- 嵌入引用块本体不强制“点击跳转”;但在块菜单提供“跳转到原块”动作。
- 限制:
- **同页嵌入禁止**`targetDocId === sourceDocId` 时直接禁止(UI 禁用 + API 双重校验)。
- 无权限/不存在:失败提示。
### 3. 数据结构设计(MVP,兼容你当前“整页 content JSON”)
#### 3.1 新增块类型:blockReference(区分于 pageReference
- `type: "blockReference"`
- `props`(建议):
- `targetBlockId: string`(必填)
- `display: "inline" | "embed"`MVP 用 embed
- `alias?: string`(行内引用别名,后续再做)
#### 3.2 块索引(block_index)——用于从 blockId 反查所在页面
因为块仍存放在 `documents.content` 内(整页 JSON),为了实现:
- “跳转到原块”
- “嵌入引用渲染/编辑时找到源块”
需要一个索引集合(Convex 表):
- `block_index { blockId, documentId, workspaceId, updatedAt }`
维护方式(MVP):
- 每次保存页面 content 时(documents.save / documents.updateContent),解析 blocks,批量 upsert 索引。
- Move 操作需要同时更新源/目标页的索引(或依赖后续 save 再修正,但建议 move 立刻修正)。
#### 3.3 引用边(可选,但很有用)
- `reference_edges { sourceDocumentId, targetBlockId, createdAt }`
- 用途:反链面板(Backlinks)、统计、权限校验辅助。
### 4. API 设计(建议新增 blocks 维度接口,避免滥用 documents/embed
> 目标:把“块移动/块引用”从“创建子页面 + pageReference”的错误语义中解耦出来。
#### 4.1 `POST /api/blocks/move`
- 入参:`{ sourceDocumentId, blockId, targetDocumentId, position?: "end" }`
- 行为:从 sourceDoc content 移除 block 子树,追加到 targetDoc content;更新索引。
#### 4.2 `POST /api/blocks/embed`
- 入参:`{ sourceDocumentId, blockId, targetDocumentId, position?: "end" }`
- 行为:校验非同页;在 targetDoc content 追加 `blockReference(targetBlockId=blockId, display="embed")`。
#### 4.3 `GET /api/blocks/get?blockId=...`
- 返回:`{ documentId, block, path? }`
- 用途:渲染引用块、跳转到原块、hover 预览(后续)。
#### 4.4 `POST /api/blocks/patch`(嵌入引用的同步编辑)
- 入参:`{ blockId, patch }`MVP 可先做 `replaceBlock`:提交完整 block JSON
- 行为:定位源块所在文档,修改该块内容并保存;广播刷新(后续可用 Convex 订阅优化)。
### 5. UI / 交互设计(与现有 MoveEmbedPickerDialog 的对接)
- 仍复用现有 `MoveEmbedPickerDialog` 做“选页面”能力。
- 在“嵌入到”模式下:
- Picker 直接排除当前页面(`excludeIds=[currentDocumentId]`),并在选中时二次校验。
- 行为完成后的反馈:
- Movetoast “已移动到 XXX”
- Embed:toast “已在目标页面末尾插入引用块”
### 6. pw-tests 验收用例(最小集合,锁定行为不跑偏)
- `move_basic`A 页面块 → Move 到 B;断言 A 不存在、B 存在,且块 `blockId` 未变化。
- `embed_basic`A 页面块 → Embed 到 B;断言 A 仍存在、B 出现 `blockReference(targetBlockId=...)`。
- `embed_edit_sync`:在 B 的嵌入引用中编辑 → 刷新 A → 断言源块同步变化。
- `embed_same_page_forbidden`Embed 目标选择当前页 → UI 提示/不可选,且服务端拒绝。
- `embed_delete_only_reference`:删除 B 的引用块 → A 源块仍存在。
### 7. 迁移实施顺序(避免一次性大爆炸)
1) 先落地 `blockReference` 块类型与只读渲染(不做同步编辑)。
2) 上 `block_index` 并在保存 content 时维护;补齐 `GET /api/blocks/get`。
3) 实现 `POST /api/blocks/embed` + UI 接入(替换旧 documents/embed 的错误语义)。
4) 实现 `POST /api/blocks/move`(跨文档移动块子树)。
5) 最后做 `POST /api/blocks/patch`,实现嵌入引用的同步编辑与限制(删除语义/跳转)。