365 lines
38 KiB
Plaintext
365 lines
38 KiB
Plaintext
可以,按你选的 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 API,9001 Console)
|
||
4. minio-init(启动时自动建 bucket)
|
||
|
||
### B. 目录规划(建议)
|
||
|
||
- infra/convex/
|
||
- docker-compose.yml(Convex 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,你确认后再动代码)。
|
||
|
||
———
|
||
|
||
### 阶段 1:Convex + MinIO 跑起来(不改业务代码)
|
||
|
||
目标:本机基础设施 ready。
|
||
|
||
- 在 infra/convex/ 放置 compose,docker compose up -d
|
||
- 生成 admin key:docker 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 query,Next.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:迁移核心数据模型与最小 CRUD(documents/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 表
|
||
- startOcr:mutation 创建任务记录,然后 scheduler.runAfter() 触发 action
|
||
- ocrPipeline:action 模拟 OCR(对齐你现在 wolai-backend/app/workers/tasks.py 的行为:更新进度、回写文档内容、任务
|
||
completed)
|
||
|
||
验收:
|
||
|
||
- 前端调用“OCR/任务”相关 API 能返回 taskId
|
||
- 任务状态能轮询/订阅(先轮询即可)
|
||
- 不再需要 wolai-backend 的 Celery 才能完成基本链路
|
||
|
||
———
|
||
|
||
### 阶段 6:文件存储(MinIO + Convex file storage)
|
||
|
||
目标:后续做媒体/附件时不会再回到 Supabase Storage。
|
||
|
||
- 保持 Convex backend 已配置 S3(MinIO)
|
||
- 在 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、references(RPC)、onlyoffice、
|
||
luckysheet、ai-agent、sidebar、search(recent) 等(目前大概率是“Supabase 分支/兜底分支”)。
|
||
- services/ingest_service/ 仍通过 Supabase REST 做任务/索引/清理(属于后续 RAG/索引链路)。
|
||
- 根目录 src/ 也还有 Supabase 用法(更像历史/备用 Next 工程,若不参与桌面构建可先不动)。
|
||
- 按你“尽量一体化绑死 Convex”的优先级,建议下一步这样排
|
||
1. 鉴权/权限从“固定用户”升级为可扩展的真实方案:否则后面所有“按用户/工作区隔离”都只能靠约定。
|
||
Convex Auth 是官方路线之一,但对 Next.js server 侧支持仍在演进中,需要你接受一定不稳定/适配成
|
||
本。citeturn0search1
|
||
2. 把仍依赖 Supabase Storage 的功能全部切到 Convex Files:你已经验证 Dashboard Files 可见;下一步
|
||
是把 luckysheet/onlyoffice 等涉及上传/签名 URL 的地方也迁掉(或临时 501 下线),保证
|
||
USE_CONVEX=1 时不会再触发 Supabase Storage。citeturn0search5
|
||
3. 把“搜索/索引/推荐”从 Postgres/RPC 思路迁到 Convex:文档搜索走 Convex 全文检索;RAG/embedding 走 Vector Search(注意向量检索需要在 action 里跑)。citeturn0search2turn0search4
|
||
4. 把 services/ingest_service 的“队列/任务状态机”迁出 Supabase:要么先停用该服务;要么改成 Convex
|
||
内部的任务表 + actions + scheduler(这样系统更一体化)。
|
||
5. 做运维闭环(自部署必需):明确 Docker 卷备份/恢复策略(Convex 也在推进更易用的数据备份/恢复能
|
||
力,但你仍应以卷级备份为底线)。citeturn0search3
|
||
- 关于“Convex 组件”与你项目的适配结论
|
||
- Convex “Components”适合把通用能力(比如协作编辑、鉴权、工作流)做成可插拔模块,并且支持隔离/复
|
||
用;你的项目属于“练手快速迭代”,很适合用组件化方式逐块替换 Supabase 逻辑。citeturn1search0
|
||
- Files 这一块:Convex 的 Files 更像“一个大池子/大桶”,不强调文件夹;你要的“按用户/工作区区分、路
|
||
径/目录视图”,仍建议用业务表字段(workspace_id、user_id、path)来实现管理与展示,这是最贴合你“一 体化 + 不引入外部对象存储”的路线。citeturn0search5
|
||
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。
|
||
- 抽了一个路由侧通用 helper:wolai-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 客户端/配置/轮询入库实现)
|
||
- “最近访问”已落库到 Convex:wolai-frontend/convex/schema.ts:1 新增 user_recent_pages;wolai-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]`),并在选中时二次校验。
|
||
- 行为完成后的反馈:
|
||
- Move:toast “已移动到 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`,实现嵌入引用的同步编辑与限制(删除语义/跳转)。
|