Files
mnote/wolai-frontend/convex/schema.ts
T
lix-2026 96e03645f7 chore: 收口 review 执行清单与 runtime 验证
- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录

- 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目

- 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑

- 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
2026-05-14 05:52:08 +08:00

513 lines
19 KiB
TypeScript
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.
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 AuthauthTables 提供用户认证所需的数据表
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 Filesstorage)定位信息
// 说明:早期阶段 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/<docId>/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 RPCrecord_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"])
.index("by_workspace_created_at", ["workspace_id", "created_at", "id"])
.index("by_workspace_status_created_at", ["workspace_id", "status", "created_at", "id"])
.index("by_workspace_target_page_created_at", ["workspace_id", "target_page_id", "created_at", "id"])
.index("by_workspace_target_block_created_at", ["workspace_id", "target_block_id", "created_at", "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"])
.index("by_workspace_created_at", ["workspace_id", "created_at", "id"])
.index("by_workspace_status_created_at", ["workspace_id", "status", "created_at", "id"])
.index("by_workspace_aggregate_created_at", [
"workspace_id",
"aggregate_type",
"aggregate_id",
"created_at",
"id",
]),
});