- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录 - 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目 - 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑 - 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
513 lines
19 KiB
TypeScript
513 lines
19 KiB
TypeScript
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/<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 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"])
|
||
.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",
|
||
]),
|
||
});
|