Files
mnote/design/convex-mir-design
T

365 lines
37 KiB
Plaintext
Raw Normal View History

2026-01-17 10:12:53 +08:00
可以,按你选的 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 化 + 冒烟回归
2026-01-18 05:13:53 +08:00
- M2(已完成):Mindmap 数据与接口迁移(/api/mindmap/**、/api/mindmap-trash/empty),落到 Convex(优先复用
2026-01-17 10:12:53 +08:00
documents.mindmap_data
2026-01-18 05:13:53 +08:00
- M3(已完成):References(页面引用/反链)迁移(/api/references/record、/api/references/backlinks),补齐目前的 占位实现
- M4(已完成):AI Agent/api/ai-agent/run、/api/ai-agent/client-tool-result)去 Supabase 化(鉴权/读写文档/工
2026-01-17 10:12:53 +08:00
具回调)
2026-01-18 05:13:53 +08:00
- M5(待继续):Online Table / Luckysheet/api/tables/**、/api/luckysheet/**)迁移或在 Convex 模式下先禁用(给
2026-01-17 10:12:53 +08:00
出明确 UI 提示)
2026-01-18 05:13:53 +08:00
- M6(待继续):OnlyOffice/api/onlyoffice/*)迁移或在 Convex 模式下先禁用(同上)
- M7(待继续):服务侧(services/ingest_service 等)去 Supabase 化:任务表/状态机迁到 Convex jobs/actions(或先
停用该链路)
- M8(未开始):鉴权从“固定用户”升级为可扩展方案(仍保持 Convex 一体化)
- M9(未开始):运维闭环:Convex 数据/Files 卷备份恢复、日志与健康检查、启动脚本收敛
2026-01-17 10:12:53 +08:00
我刚做完的(你现在的代码状态)
- 补齐 documents 的最后缺口:wolai-frontend/src/app/api/documents/embed/route.ts:1 已支持 Convex。
- 抽了一个路由侧通用 helperwolai-frontend/src/lib/convex/route.ts:1(统一拿 auth+client)。
2026-01-18 05:13:53 +08:00
- 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 客户端/配置/轮询入库实现)
2026-01-17 10:12:53 +08:00
- “最近访问”已落库到 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`,实现嵌入引用的同步编辑与限制(删除语义/跳转)。