import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; // 说明: // - 这里先按"兼容现有 Next API 返回结构"的思路设计字段:id/workspace_id/user_id 等命名保持与 Supabase 一致。 // - Convex 自带的 _id 仍然存在,但我们暂时不把它暴露给上层业务,便于逐步迁移与回退。 // - M8: 集成 Convex Auth,authTables 提供用户认证所需的数据表 export default defineSchema({ // Convex Auth 表(用户、账户、会话等) ...authTables, workspaces: defineTable({ id: v.string(), name: v.string(), type: v.union(v.literal("personal"), v.literal("team")), icon_url: v.union(v.string(), v.null()), created_by: v.string(), created_at: v.string(), }) .index("by_workspace_id", ["id"]) .index("by_created_by", ["created_by"]), workspace_members: defineTable({ workspace_id: v.string(), user_id: v.string(), role: v.string(), is_default: v.boolean(), created_at: v.string(), }) .index("by_user_id", ["user_id"]) .index("by_workspace_user", ["workspace_id", "user_id"]) .index("by_workspace_id", ["workspace_id"]), documents: defineTable({ id: v.string(), user_id: v.string(), workspace_id: v.string(), title: v.union(v.string(), v.null()), parent_id: v.union(v.string(), v.null()), sort_order: v.union(v.number(), v.null()), is_starred: v.union(v.boolean(), v.null()), is_template: v.boolean(), access_scope: v.union(v.literal("private"), v.literal("shared"), v.literal("public")), // 页面选项(对应 Supabase documents 上的 UI 配置列) wide_layout: v.union(v.boolean(), v.null()), use_small_text: v.union(v.boolean(), v.null()), show_heading_numbers: v.union(v.boolean(), v.null()), show_toc: v.union(v.boolean(), v.null()), show_structure: v.union(v.boolean(), v.null()), protect_editing: v.union(v.boolean(), v.null()), show_word_count: v.union(v.boolean(), v.null()), collapse_backlinks: v.optional(v.union(v.boolean(), v.null())), page_font: v.optional(v.union(v.literal("default"), v.literal("song"), v.literal("kai"), v.null())), layout_density: v.optional(v.union(v.literal("compact"), v.literal("normal"), v.literal("spacious"), v.null())), hide_child_pages: v.optional(v.union(v.boolean(), v.null())), show_block_ref_count: v.optional(v.union(v.boolean(), v.null())), embed_default_block_id: v.optional(v.union(v.string(), v.null())), // 统计信息(由客户端编辑器计算后回写) word_count: v.union(v.number(), v.null()), character_count: v.union(v.number(), v.null()), block_count: v.union(v.number(), v.null()), todo_total_count: v.optional(v.union(v.number(), v.null())), todo_done_count: v.optional(v.union(v.number(), v.null())), // 说明:当前文档内容结构还在演进,先用 any 承接(与 Supabase Json 一致的宽松形态)。 content: v.any(), content_revision: v.optional(v.number()), content_conflict_key: v.optional(v.union(v.string(), v.null())), // 说明:后续可用于搜索/索引(目前先留空,不强制写入)。 raw_text: v.optional(v.union(v.string(), v.null())), // 时间戳统一使用 ISO 字符串,便于直接复用前端现有排序逻辑。 created_at: v.string(), updated_at: v.union(v.string(), v.null()), // 软删除(阶段 4 先不实现垃圾桶逻辑,但字段先留好,便于后续迁移)。 deleted_at: v.union(v.string(), v.null()), deleted_by: v.union(v.string(), v.null()), // 兼容旧逻辑:Supabase documents.mindmap_data。 mindmap_data: v.optional(v.any()), }) .index("by_document_id", ["id"]) .index("by_user", ["user_id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_parent", ["workspace_id", "parent_id"]), // 最近访问(替代 Supabase user_recent_pages) user_recent_pages: defineTable({ user_id: v.string(), workspace_id: v.string(), document_id: v.string(), last_accessed_at: v.string(), }) .index("by_user_workspace", ["user_id", "workspace_id"]) .index("by_user_document", ["user_id", "document_id"]), // 阶段 5:异步任务/队列表(最小骨架,后续可扩展为通用作业系统) jobs: defineTable({ id: v.string(), user_id: v.string(), type: v.string(), status: v.union(v.literal("queued"), v.literal("running"), v.literal("succeeded"), v.literal("failed")), payload: v.any(), result: v.union(v.any(), v.null()), error: v.union(v.string(), v.null()), created_at: v.string(), updated_at: v.string(), started_at: v.union(v.string(), v.null()), finished_at: v.union(v.string(), v.null()), }) .index("by_job_id", ["id"]) .index("by_user", ["user_id"]) .index("by_status", ["status"]), // 阶段 6:媒体/附件(替代 Supabase Storage + media_assets 表) media_assets: defineTable({ id: v.string(), workspace_id: v.string(), document_id: v.string(), asset_type: v.string(), file_url: v.union(v.string(), v.null()), thumbnail_url: v.union(v.string(), v.null()), // Convex Files(storage)定位信息 // 说明:早期阶段 6(MinIO 直存)写入的记录可能没有该字段;允许缺失以便平滑迁移。 storage_id: v.optional(v.union(v.id("_storage"), v.null())), // S3/MinIO 定位信息 bucket: v.union(v.string(), v.null()), storage_path: v.union(v.string(), v.null()), file_name: v.union(v.string(), v.null()), file_size: v.union(v.number(), v.null()), mime_type: v.union(v.string(), v.null()), // OCR 相关(后续再接入) ocr_text: v.union(v.string(), v.null()), ocr_status: v.union(v.string(), v.null()), ocr_payload: v.optional(v.any()), ocr_strategy: v.optional(v.union(v.string(), v.null())), // 回收站/清理 deleted_at: v.union(v.string(), v.null()), deleted_by: v.union(v.string(), v.null()), purged_at: v.union(v.string(), v.null()), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_asset_id", ["id"]) .index("by_storage_id", ["storage_id"]) .index("by_workspace", ["workspace_id"]) .index("by_document", ["document_id"]) .index("by_workspace_deleted", ["workspace_id", "deleted_at"]), // M2:思维导图(替代本地文件 public/documents//mindmap*.json + Supabase documents.mindmap_data) // 说明: // - 业务侧依然以 (document_id, mindmap_id) 作为“定位键”,避免跨页面冲突。 // - id 是一个便于排查的全局唯一串(例如 `${docId}:${mindmapId}`),但不要求前端依赖它。 mindmaps: defineTable({ id: v.string(), user_id: v.string(), workspace_id: v.string(), document_id: v.string(), mindmap_id: v.string(), data: v.any(), created_at: v.string(), updated_at: v.string(), deleted_at: v.union(v.string(), v.null()), deleted_by: v.union(v.string(), v.null()), }) .index("by_mindmap_id", ["id"]) .index("by_doc_mindmap", ["document_id", "mindmap_id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_deleted", ["workspace_id", "deleted_at"]) .index("by_user", ["user_id"]), // M3:页面引用/反链(替代 Supabase RPC:record_page_ref、list_backlinks) page_references: defineTable({ id: v.string(), workspace_id: v.string(), source_page_id: v.string(), source_block_id: v.union(v.string(), v.null()), target_page_id: v.string(), alias: v.union(v.string(), v.null()), display_mode: v.union(v.literal("inline"), v.literal("embed")), is_previewable: v.boolean(), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_reference_id", ["id"]) .index("by_workspace_target", ["workspace_id", "target_page_id"]) .index("by_workspace_source", ["workspace_id", "source_page_id"]) .index("by_unique", ["workspace_id", "source_page_id", "source_block_id", "target_page_id", "display_mode"]), // M5:在线表格(替代 Supabase document_tables) document_tables: defineTable({ id: v.string(), workspace_id: v.string(), document_id: v.string(), grid_key: v.string(), title: v.string(), schema: v.any(), // TableSchema: { columns, frozenRowCount, frozenColCount } view_preferences: v.any(), snapshot: v.optional(v.any()), // DocumentTableSnapshot: { rows?, luckysheet? } is_archived: v.boolean(), // 垃圾桶字段:与附件/思维导图的删除语义保持一致(允许历史数据缺字段) deleted_at: v.optional(v.union(v.string(), v.null())), deleted_by: v.optional(v.union(v.string(), v.null())), purged_at: v.optional(v.union(v.string(), v.null())), last_synced_at: v.optional(v.string()), created_by: v.string(), updated_by: v.optional(v.string()), created_at: v.string(), updated_at: v.string(), }) .index("by_table_id", ["id"]) .index("by_grid_key", ["grid_key"]) .index("by_document", ["document_id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_archived", ["workspace_id", "is_archived"]), // M5:在线表格行数据(替代 Supabase document_table_rows) document_table_rows: defineTable({ id: v.string(), workspace_id: v.string(), document_id: v.string(), table_id: v.string(), row_index: v.number(), row_data: v.any(), row_hash: v.optional(v.string()), is_deleted: v.boolean(), updated_by: v.optional(v.string()), created_at: v.string(), updated_at: v.string(), }) .index("by_row_id", ["id"]) .index("by_table", ["table_id"]) .index("by_table_row", ["table_id", "row_index"]) .index("by_workspace", ["workspace_id"]) .searchIndex("by_table_row_full", { searchField: "row_hash", filterFields: ["table_id", "is_deleted"], }), document_shares: defineTable({ workspace_id: v.string(), document_id: v.string(), shared_with_user_id: v.string(), permission: v.union(v.literal("read"), v.literal("edit")), include_descendants: v.boolean(), disable_download: v.optional(v.boolean()), disable_copy: v.optional(v.boolean()), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_shared_with_user", ["shared_with_user_id"]) .index("by_created_by", ["created_by"]) .index("by_document", ["document_id"]) .index("by_doc_user", ["document_id", "shared_with_user_id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_shared_with", ["workspace_id", "shared_with_user_id"]) .index("by_workspace_created_by", ["workspace_id", "created_by"]), // M9:评论(页面/块) comment_threads: defineTable({ id: v.string(), workspace_id: v.string(), document_id: v.string(), block_id: v.union(v.string(), v.null()), resolved_at: v.union(v.string(), v.null()), resolved_by: v.union(v.string(), v.null()), comment_count: v.number(), last_activity_at: v.string(), last_comment_preview: v.union(v.string(), v.null()), last_comment_by: v.union(v.string(), v.null()), created_by: v.string(), created_at: v.string(), }) .index("by_thread_id", ["id"]) .index("by_document", ["document_id"]) .index("by_document_block", ["document_id", "block_id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_document_activity", ["workspace_id", "document_id", "last_activity_at"]), comment_messages: defineTable({ id: v.string(), thread_id: v.string(), workspace_id: v.string(), document_id: v.string(), block_id: v.union(v.string(), v.null()), parent_id: v.union(v.string(), v.null()), body: v.string(), created_by: v.string(), created_at: v.string(), deleted_at: v.union(v.string(), v.null()), deleted_by: v.union(v.string(), v.null()), }) .index("by_message_id", ["id"]) .index("by_thread", ["thread_id"]) .index("by_document", ["document_id"]) .index("by_document_block", ["document_id", "block_id"]) .index("by_workspace", ["workspace_id"]), groups: defineTable({ id: v.string(), workspace_id: v.string(), name: v.string(), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_group_id", ["id"]) .index("by_workspace", ["workspace_id"]), group_members: defineTable({ workspace_id: v.string(), group_id: v.string(), user_id: v.string(), role: v.union(v.literal("owner"), v.literal("member")), created_at: v.string(), }) .index("by_group", ["group_id"]) .index("by_group_user", ["group_id", "user_id"]) .index("by_workspace_user", ["workspace_id", "user_id"]), group_invitations: defineTable({ workspace_id: v.string(), group_id: v.string(), invited_user_id: v.string(), invited_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_invited_user", ["invited_user_id"]) .index("by_group_user", ["group_id", "invited_user_id"]) .index("by_workspace_invited_user", ["workspace_id", "invited_user_id"]) .index("by_workspace_group", ["workspace_id", "group_id"]) .index("by_workspace_invited_by", ["workspace_id", "invited_by"]), document_group_shares: defineTable({ workspace_id: v.string(), group_id: v.string(), document_id: v.string(), include_descendants: v.boolean(), disable_download: v.optional(v.boolean()), disable_copy: v.optional(v.boolean()), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_document", ["document_id"]) .index("by_doc_group", ["document_id", "group_id"]) .index("by_workspace_group", ["workspace_id", "group_id"]) .index("by_workspace_created_by", ["workspace_id", "created_by"]), document_group_user_permissions: defineTable({ workspace_id: v.string(), group_id: v.string(), document_id: v.string(), user_id: v.string(), permission: v.union(v.literal("read"), v.literal("edit")), created_at: v.string(), updated_at: v.string(), }) .index("by_doc_group_user", ["document_id", "group_id", "user_id"]) .index("by_workspace_user", ["workspace_id", "user_id"]) .index("by_workspace_group", ["workspace_id", "group_id"]), document_stars: defineTable({ workspace_id: v.string(), document_id: v.string(), user_id: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_workspace_user", ["workspace_id", "user_id"]) .index("by_user_document", ["user_id", "document_id"]) .index("by_workspace_document_user", ["workspace_id", "document_id", "user_id"]), // === 新领域模型表(Action Execution System)=== // Pages 表 - 对应域模型 Page pages: defineTable({ id: v.string(), workspace_id: v.string(), parent_id: v.union(v.string(), v.null()), title: v.string(), access_scope: v.union(v.literal("private"), v.literal("shared"), v.literal("public")), block_ids: v.array(v.string()), child_page_ids: v.array(v.string()), asset_ids: v.array(v.string()), created_by: v.string(), created_at: v.string(), updated_at: v.string(), deleted_at: v.union(v.string(), v.null()), }) .index("by_page_id", ["id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_parent", ["workspace_id", "parent_id"]) .index("by_workspace_deleted", ["workspace_id", "deleted_at"]), // Blocks 表 - 对应域模型 Block blocks: defineTable({ id: v.string(), workspace_id: v.string(), page_id: v.string(), parent_block_id: v.union(v.string(), v.null()), type: v.string(), props: v.optional(v.any()), content: v.optional(v.any()), child_block_ids: v.array(v.string()), created_by: v.string(), created_at: v.string(), updated_at: v.string(), }) .index("by_block_id", ["id"]) .index("by_workspace", ["workspace_id"]) .index("by_page", ["page_id"]) .index("by_page_parent", ["page_id", "parent_block_id"]), // Agent Actions 表 - 对应域模型 AgentAction(持久化日志) agent_actions: defineTable({ id: v.string(), workspace_id: v.string(), session_id: v.string(), action_type: v.string(), status: v.union(v.literal("planned"), v.literal("running"), v.literal("succeeded"), v.literal("failed"), v.literal("cancelled")), actor: v.union(v.literal("human"), v.literal("agent"), v.literal("automation")), input: v.any(), output: v.optional(v.any()), error: v.optional(v.string()), started_at: v.string(), finished_at: v.optional(v.string()), }) .index("by_action_id", ["id"]) .index("by_workspace", ["workspace_id"]) .index("by_workspace_session", ["workspace_id", "session_id"]) .index("by_session", ["session_id"]) .index("by_workspace_status", ["workspace_id", "status"]), command_logs: defineTable({ id: v.string(), workspace_id: v.string(), request_id: v.string(), trace_id: v.string(), command_id: v.string(), command_name: v.string(), actor_id: v.string(), actor_type: v.string(), source_channel: v.string(), source_client: v.string(), status: v.union(v.literal("pending"), v.literal("succeeded"), v.literal("failed"), v.literal("rolled_back")), target_page_id: v.optional(v.union(v.string(), v.null())), target_block_id: v.optional(v.union(v.string(), v.null())), payload: v.any(), payload_summary: v.string(), refs: v.array(v.string()), idempotency_key: v.optional(v.union(v.string(), v.null())), error: v.optional(v.union(v.string(), v.null())), created_at: v.string(), finished_at: v.optional(v.union(v.string(), v.null())), }) .index("by_command_log_id", ["id"]) .index("by_workspace_request", ["workspace_id", "request_id"]) .index("by_workspace_trace", ["workspace_id", "trace_id"]) .index("by_workspace_command", ["workspace_id", "command_id"]), domain_events: defineTable({ id: v.string(), workspace_id: v.string(), request_id: v.string(), trace_id: v.string(), command_id: v.string(), command_log_id: v.string(), event_type: v.string(), aggregate_type: v.string(), aggregate_id: v.string(), event_version: v.number(), status: v.union(v.literal("pending"), v.literal("committed"), v.literal("rejected"), v.literal("failed")), actor_type: v.string(), payload: v.any(), created_at: v.string(), }) .index("by_domain_event_id", ["id"]) .index("by_workspace_request", ["workspace_id", "request_id"]) .index("by_workspace_trace", ["workspace_id", "trace_id"]) .index("by_workspace_command", ["workspace_id", "command_id"]), });