439 lines
11 KiB
TypeScript
439 lines
11 KiB
TypeScript
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();
|
||
|
|
},
|
||
|
|
});
|