import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { nowIso } from "./_utils/time"; import type { MutationCtx, QueryCtx } from "./_generated/server"; async function assertWorkspaceMember( ctx: QueryCtx | MutationCtx, userId: string, workspaceId: string, ) { const membership = await ctx.db .query("workspace_members") .withIndex("by_workspace_user", (q) => q.eq("workspace_id", workspaceId).eq("user_id", userId), ) .first(); if (!membership) { throw new Error("无权访问该工作空间"); } return membership; } export const listWorkspacesByUser = query({ args: { userId: v.string() }, handler: async (ctx, args) => { const memberships = await ctx.db .query("workspace_members") .withIndex("by_user_id", (q) => q.eq("user_id", args.userId)) .collect(); const workspaceIds = Array.from(new Set(memberships.map((item) => item.workspace_id))); const result = []; for (const workspaceId of workspaceIds) { const workspace = await ctx.db .query("workspaces") .withIndex("by_workspace_id", (q) => q.eq("id", workspaceId)) .first(); if (!workspace) continue; const memberCount = await ctx.db .query("workspace_members") .withIndex("by_workspace_id", (q) => q.eq("workspace_id", workspaceId)) .collect(); result.push({ id: workspace.id, name: workspace.name, type: workspace.type, iconUrl: workspace.icon_url ?? null, memberCount: memberCount.length, isDefault: memberships.some( (item) => item.workspace_id === workspaceId && item.is_default, ), }); } return result; }, }); export const createPage = mutation({ args: { userId: v.string(), id: v.string(), workspaceId: v.string(), parentId: v.union(v.string(), v.null()), title: v.string(), accessScope: v.union( v.literal("private"), v.literal("shared"), v.literal("public"), ), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const ts = nowIso(); await ctx.db.insert("pages", { id: args.id, workspace_id: args.workspaceId, parent_id: args.parentId, title: args.title, access_scope: args.accessScope, block_ids: [], child_page_ids: [], asset_ids: [], created_by: args.userId, created_at: ts, updated_at: ts, deleted_at: null, }); return { id: args.id, workspaceId: args.workspaceId, parentId: args.parentId, title: args.title, accessScope: args.accessScope, createdBy: args.userId, createdAt: ts, updatedAt: ts, archivedAt: null, sortOrder: null, }; }, }); export const renamePage = mutation({ args: { userId: v.string(), id: v.string(), title: v.string(), }, handler: async (ctx, args) => { const page = await ctx.db .query("pages") .withIndex("by_page_id", (q) => q.eq("id", args.id)) .first(); if (!page) throw new Error("页面不存在"); await assertWorkspaceMember(ctx, args.userId, page.workspace_id); const ts = nowIso(); await ctx.db.patch(page._id, { title: args.title, updated_at: ts, }); return { id: args.id, title: args.title, updatedAt: ts, }; }, }); export const movePage = mutation({ args: { userId: v.string(), id: v.string(), parentId: v.union(v.string(), v.null()), }, handler: async (ctx, args) => { const page = await ctx.db .query("pages") .withIndex("by_page_id", (q) => q.eq("id", args.id)) .first(); if (!page) throw new Error("页面不存在"); await assertWorkspaceMember(ctx, args.userId, page.workspace_id); const ts = nowIso(); await ctx.db.patch(page._id, { parent_id: args.parentId, updated_at: ts, }); return { id: args.id, parentId: args.parentId, sortOrder: null, updatedAt: ts, }; }, }); export const archivePage = mutation({ args: { userId: v.string(), id: v.string(), }, handler: async (ctx, args) => { const page = await ctx.db .query("pages") .withIndex("by_page_id", (q) => q.eq("id", args.id)) .first(); if (!page) throw new Error("页面不存在"); await assertWorkspaceMember(ctx, args.userId, page.workspace_id); const ts = nowIso(); await ctx.db.patch(page._id, { deleted_at: ts, updated_at: ts, }); return { id: args.id, archivedAt: ts, updatedAt: ts, }; }, }); export const listPagesByWorkspace = query({ args: { userId: v.string(), workspaceId: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); return await ctx.db .query("pages") .withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId)) .collect(); }, }); export const appendBlocks = mutation({ args: { userId: v.string(), workspaceId: v.string(), pageId: v.string(), blocks: v.array( v.object({ id: v.string(), parentBlockId: v.union(v.string(), v.null()), type: v.string(), props: v.optional(v.any()), content: v.optional(v.any()), orderKey: v.string(), }), ), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const ts = nowIso(); for (const block of args.blocks) { await ctx.db.insert("blocks", { id: block.id, workspace_id: args.workspaceId, page_id: args.pageId, parent_block_id: block.parentBlockId, type: block.type, props: block.props, content: block.content, child_block_ids: [], created_by: args.userId, created_at: ts, updated_at: ts, }); } return { count: args.blocks.length, pageId: args.pageId, blockIds: args.blocks.map((item) => item.id), }; }, }); export const insertBlockAfter = mutation({ args: { userId: v.string(), workspaceId: v.string(), pageId: v.string(), afterBlockId: v.string(), block: v.object({ id: v.string(), parentBlockId: v.union(v.string(), v.null()), type: v.string(), props: v.optional(v.any()), content: v.optional(v.any()), orderKey: v.string(), }), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const ts = nowIso(); await ctx.db.insert("blocks", { id: args.block.id, workspace_id: args.workspaceId, page_id: args.pageId, parent_block_id: args.block.parentBlockId, type: args.block.type, props: args.block.props, content: args.block.content, child_block_ids: [], created_by: args.userId, created_at: ts, updated_at: ts, }); return { pageId: args.pageId, afterBlockId: args.afterBlockId, blockId: args.block.id, }; }, }); export const replaceBlock = mutation({ args: { userId: v.string(), workspaceId: v.string(), blockId: v.string(), type: v.string(), props: v.optional(v.any()), content: v.optional(v.any()), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const block = await ctx.db .query("blocks") .withIndex("by_block_id", (q) => q.eq("id", args.blockId)) .first(); if (!block) throw new Error("块不存在"); const ts = nowIso(); await ctx.db.patch(block._id, { type: args.type, props: args.props, content: args.content, updated_at: ts, }); return { blockId: args.blockId, updatedAt: ts, }; }, }); export const deleteBlock = mutation({ args: { userId: v.string(), workspaceId: v.string(), blockId: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const block = await ctx.db .query("blocks") .withIndex("by_block_id", (q) => q.eq("id", args.blockId)) .first(); if (!block) throw new Error("块不存在"); await ctx.db.delete(block._id); const ts = nowIso(); return { blockId: args.blockId, deletedAt: ts, updatedAt: ts, }; }, }); export const listBlocksByPage = query({ args: { userId: v.string(), workspaceId: v.string(), pageId: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); return await ctx.db .query("blocks") .withIndex("by_page", (q) => q.eq("page_id", args.pageId)) .collect(); }, }); export const createAgentActionStarted = mutation({ args: { userId: v.string(), workspaceId: v.string(), actionId: v.string(), sessionId: v.string(), actionType: v.string(), actor: v.union(v.literal("human"), v.literal("agent"), v.literal("automation")), input: v.any(), startedAt: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); await ctx.db.insert("agent_actions", { id: args.actionId, workspace_id: args.workspaceId, session_id: args.sessionId, action_type: args.actionType, status: "running", actor: args.actor, input: args.input, output: null, error: null, started_at: args.startedAt, finished_at: null, }); return { ok: true, actionId: args.actionId }; }, }); export const markAgentActionSucceeded = mutation({ args: { userId: v.string(), workspaceId: v.string(), actionId: v.string(), output: v.any(), finishedAt: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const row = await ctx.db .query("agent_actions") .withIndex("by_action_id", (q) => q.eq("id", args.actionId)) .first(); if (!row) throw new Error("action log not found"); await ctx.db.patch(row._id, { status: "succeeded", output: args.output, finished_at: args.finishedAt, }); return { ok: true }; }, }); export const markAgentActionFailed = mutation({ args: { userId: v.string(), workspaceId: v.string(), actionId: v.string(), error: v.string(), finishedAt: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); const row = await ctx.db .query("agent_actions") .withIndex("by_action_id", (q) => q.eq("id", args.actionId)) .first(); if (!row) throw new Error("action log not found"); await ctx.db.patch(row._id, { status: "failed", error: args.error, finished_at: args.finishedAt, }); return { ok: true }; }, }); export const listAgentActionsBySession = query({ args: { userId: v.string(), workspaceId: v.string(), sessionId: v.string(), }, handler: async (ctx, args) => { await assertWorkspaceMember(ctx, args.userId, args.workspaceId); return await ctx.db .query("agent_actions") .withIndex("by_workspace_session", (q) => q.eq("workspace_id", args.workspaceId).eq("session_id", args.sessionId), ) .collect(); }, });