0.5 缩减重构
This commit is contained in:
+12
@@ -15,8 +15,11 @@ import type * as _utils_ingestJobs from "../_utils/ingestJobs.js";
|
||||
import type * as _utils_lightrag from "../_utils/lightrag.js";
|
||||
import type * as _utils_text from "../_utils/text.js";
|
||||
import type * as _utils_time from "../_utils/time.js";
|
||||
import type * as agentActions from "../agentActions.js";
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as blocks from "../blocks.js";
|
||||
import type * as comments from "../comments.js";
|
||||
import type * as crons from "../crons.js";
|
||||
import type * as documentGroupShares from "../documentGroupShares.js";
|
||||
import type * as documentShares from "../documentShares.js";
|
||||
import type * as documentStars from "../documentStars.js";
|
||||
@@ -26,8 +29,11 @@ import type * as groupMembers from "../groupMembers.js";
|
||||
import type * as groups from "../groups.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as jobs from "../jobs.js";
|
||||
import type * as maintenance from "../maintenance.js";
|
||||
import type * as mediaAssets from "../mediaAssets.js";
|
||||
import type * as mindmaps from "../mindmaps.js";
|
||||
import type * as mnoteNextBridge from "../mnoteNextBridge.js";
|
||||
import type * as pages from "../pages.js";
|
||||
import type * as ping from "../ping.js";
|
||||
import type * as recents from "../recents.js";
|
||||
import type * as references from "../references.js";
|
||||
@@ -49,8 +55,11 @@ declare const fullApi: ApiFromModules<{
|
||||
"_utils/lightrag": typeof _utils_lightrag;
|
||||
"_utils/text": typeof _utils_text;
|
||||
"_utils/time": typeof _utils_time;
|
||||
agentActions: typeof agentActions;
|
||||
auth: typeof auth;
|
||||
blocks: typeof blocks;
|
||||
comments: typeof comments;
|
||||
crons: typeof crons;
|
||||
documentGroupShares: typeof documentGroupShares;
|
||||
documentShares: typeof documentShares;
|
||||
documentStars: typeof documentStars;
|
||||
@@ -60,8 +69,11 @@ declare const fullApi: ApiFromModules<{
|
||||
groups: typeof groups;
|
||||
http: typeof http;
|
||||
jobs: typeof jobs;
|
||||
maintenance: typeof maintenance;
|
||||
mediaAssets: typeof mediaAssets;
|
||||
mindmaps: typeof mindmaps;
|
||||
mnoteNextBridge: typeof mnoteNextBridge;
|
||||
pages: typeof pages;
|
||||
ping: typeof ping;
|
||||
recents: typeof recents;
|
||||
references: typeof references;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 简单的 ID 生成器,用于生成类似 UUID 的字符串
|
||||
export function generateId(): string {
|
||||
// 使用时间戳 + 随机数生成唯一 ID
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 15);
|
||||
return `${timestamp}${random}`;
|
||||
}
|
||||
// 简单的 ID 生成器,用于生成类似 UUID 的字符串
|
||||
export function generateId(): string {
|
||||
// 使用时间戳 + 随机数生成唯一 ID
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 15);
|
||||
return `${timestamp}${random}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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 getById = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
actionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
return await ctx.db
|
||||
.query("agent_actions")
|
||||
.withIndex("by_action_id", (q) => q.eq("id", args.actionId))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
export const listBySession = 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();
|
||||
},
|
||||
});
|
||||
|
||||
export const createStarted = 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 markSucceeded = 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 markFailed = 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 };
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Password } from "@convex-dev/auth/providers/Password";
|
||||
import { convexAuth } from "@convex-dev/auth/server";
|
||||
|
||||
if (!process.env.JWT_PRIVATE_KEY) {
|
||||
process.env.JWT_PRIVATE_KEY = "-----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPcrWDwjh74nu1 gfYo9ywVIRMedfPwbNiDjHnovzHAIeToc1+Lw3ju/io9OME3KLWzIibUHX39LilK sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d /CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU 6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9 /5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1 yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN 3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwQx3gZA39sa8 qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7 GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO 0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8 EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN 2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru 3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh 9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq AT5PRKihrWbg63/rXaSCxw== -----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* Convex Auth 配置
|
||||
*
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
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 getById = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
blockId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
return await ctx.db
|
||||
.query("blocks")
|
||||
.withIndex("by_block_id", (q) => q.eq("id", args.blockId))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
export const listByPage = 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 append = 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 { ok: true, count: args.blocks.length };
|
||||
},
|
||||
});
|
||||
|
||||
export const insertAfter = 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 { ok: true, blockId: args.block.id, afterBlockId: args.afterBlockId };
|
||||
},
|
||||
});
|
||||
|
||||
export const replace = 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("块不存在");
|
||||
await ctx.db.patch(block._id, {
|
||||
type: args.type,
|
||||
props: args.props,
|
||||
content: args.content,
|
||||
updated_at: nowIso(),
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const remove = 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);
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
@@ -34,14 +34,78 @@ async function cleanupByCreationTime(ctx: any, table: string, cutoffMs: number,
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async function cleanupOrphanConvexFiles(
|
||||
ctx: any,
|
||||
args: { cutoffMs: number; maxDeletes: number; dryRun: boolean },
|
||||
): Promise<{ deleted: number; scanned: number; hitLimit: boolean }> {
|
||||
const batchSize = 200;
|
||||
let deleted = 0;
|
||||
let scanned = 0;
|
||||
let lastCreationTime: number | null = null;
|
||||
let lastId: any = null;
|
||||
|
||||
while (deleted < args.maxDeletes) {
|
||||
let q = ctx.db.system.query("_storage").order("asc");
|
||||
if (lastCreationTime != null && lastId != null) {
|
||||
// 说明:Convex 的 `paginate` 每个函数只能调用一次,这里用“游标过滤 + take”模拟分页。
|
||||
// 使用 (_creationTime, _id) 作为游标,避免同一毫秒内多条记录导致遗漏/死循环。
|
||||
q = q.filter((qb: any) =>
|
||||
qb.or(
|
||||
qb.gt(qb.field("_creationTime"), lastCreationTime),
|
||||
qb.and(qb.eq(qb.field("_creationTime"), lastCreationTime), qb.gt(qb.field("_id"), lastId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const rows = await q.take(batchSize);
|
||||
if (!rows.length) break;
|
||||
|
||||
let reachedNewer = false;
|
||||
for (const row of rows) {
|
||||
scanned += 1;
|
||||
const created = typeof row._creationTime === "number" ? row._creationTime : 0;
|
||||
lastCreationTime = created;
|
||||
lastId = row._id;
|
||||
if (created >= args.cutoffMs) {
|
||||
reachedNewer = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 说明:当前仓库里,Convex Files 仅用于 media_assets.storage_id。
|
||||
// 若未来新增其它引用表,需要把引用检查一并补齐,避免误删仍被使用的文件。
|
||||
const ref = await ctx.db
|
||||
.query("media_assets")
|
||||
.withIndex("by_storage_id", (q: any) => q.eq("storage_id", row._id))
|
||||
.first();
|
||||
if (ref) continue;
|
||||
|
||||
if (!args.dryRun) {
|
||||
try {
|
||||
await ctx.storage.delete(row._id);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
deleted += 1;
|
||||
if (deleted >= args.maxDeletes) break;
|
||||
}
|
||||
|
||||
if (reachedNewer) break;
|
||||
}
|
||||
|
||||
return { deleted, scanned, hitLimit: deleted >= args.maxDeletes };
|
||||
}
|
||||
|
||||
export const cleanupWeekly = internalMutation({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
maxDeletes: v.optional(v.number()),
|
||||
maxFileDeletes: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const dryRun = Boolean(args.dryRun);
|
||||
const maxDeletes = Math.max(100, Math.min(50_000, Math.floor(args.maxDeletes ?? 20_000)));
|
||||
const maxFileDeletes = Math.max(100, Math.min(50_000, Math.floor(args.maxFileDeletes ?? 10_000)));
|
||||
|
||||
const cutoffMs = Date.now() - WEEK_MS;
|
||||
|
||||
@@ -60,11 +124,21 @@ export const cleanupWeekly = internalMutation({
|
||||
const deletedAuthSessions = await cleanupByCreationTime(ctx, "authSessions", cutoffMs, perTable, dryRun);
|
||||
const deletedJobs = await cleanupByCreationTime(ctx, "jobs", cutoffMs, maxDeletes - perTable * 2, dryRun);
|
||||
|
||||
// 说明:清理 Convex Files 孤儿数据(_storage 里“7 天前且无引用”的文件)。
|
||||
const files = await cleanupOrphanConvexFiles(ctx, { cutoffMs, maxDeletes: maxFileDeletes, dryRun });
|
||||
|
||||
// 若达到上限,兜底再跑一轮(避免一次删太多导致超时)
|
||||
const hitLimit =
|
||||
deletedAuthRefreshTokens >= perTable || deletedAuthSessions >= perTable || deletedJobs >= maxDeletes - perTable * 2;
|
||||
deletedAuthRefreshTokens >= perTable ||
|
||||
deletedAuthSessions >= perTable ||
|
||||
deletedJobs >= maxDeletes - perTable * 2 ||
|
||||
files.hitLimit;
|
||||
if (!dryRun && hitLimit) {
|
||||
await ctx.scheduler.runAfter(60_000, (internal as any).maintenance.cleanupWeekly, { dryRun: false, maxDeletes });
|
||||
await ctx.scheduler.runAfter(60_000, (internal as any).maintenance.cleanupWeekly, {
|
||||
dryRun: false,
|
||||
maxDeletes,
|
||||
maxFileDeletes,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -75,9 +149,12 @@ export const cleanupWeekly = internalMutation({
|
||||
authRefreshTokens: deletedAuthRefreshTokens,
|
||||
authSessions: deletedAuthSessions,
|
||||
jobs: deletedJobs,
|
||||
convexFiles: files.deleted,
|
||||
},
|
||||
scanned: {
|
||||
convexFiles: files.scanned,
|
||||
},
|
||||
rescheduled: !dryRun && hitLimit,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
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();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
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 getById = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
id: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
return await ctx.db
|
||||
.query("pages")
|
||||
.withIndex("by_page_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
export const listByWorkspace = 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 create = 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 { ok: true, id: args.id };
|
||||
},
|
||||
});
|
||||
|
||||
export const rename = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
id: v.string(),
|
||||
title: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
const page = await ctx.db
|
||||
.query("pages")
|
||||
.withIndex("by_page_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!page) throw new Error("页面不存在");
|
||||
await ctx.db.patch(page._id, {
|
||||
title: args.title,
|
||||
updated_at: nowIso(),
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const move = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
id: v.string(),
|
||||
parentId: v.union(v.string(), v.null()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
const page = await ctx.db
|
||||
.query("pages")
|
||||
.withIndex("by_page_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!page) throw new Error("页面不存在");
|
||||
await ctx.db.patch(page._id, {
|
||||
parent_id: args.parentId,
|
||||
updated_at: nowIso(),
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const archive = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
id: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await assertWorkspaceMember(ctx, args.userId, args.workspaceId);
|
||||
const page = await ctx.db
|
||||
.query("pages")
|
||||
.withIndex("by_page_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
if (!page) throw new Error("页面不存在");
|
||||
const ts = nowIso();
|
||||
await ctx.db.patch(page._id, {
|
||||
deleted_at: ts,
|
||||
updated_at: ts,
|
||||
});
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
@@ -385,4 +385,65 @@ export default defineSchema({
|
||||
.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"]),
|
||||
});
|
||||
|
||||
+221
-221
@@ -14,102 +14,102 @@ async function assertWorkspaceMember(ctx: QueryCtx | MutationCtx, userId: string
|
||||
}
|
||||
return membership;
|
||||
}
|
||||
|
||||
// Query: 获取单个表格
|
||||
export const get = query({
|
||||
args: { userId: v.string(), tableId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
if (table.workspace_id) {
|
||||
// 验证用户是否在工作区中(简化版:假设 userId 有效)
|
||||
// TODO: 添加 workspace_members 验证
|
||||
}
|
||||
return {
|
||||
id: table.id,
|
||||
workspace_id: table.workspace_id,
|
||||
document_id: table.document_id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
view_preferences: table.view_preferences,
|
||||
snapshot: table.snapshot ?? null,
|
||||
is_archived: table.is_archived,
|
||||
last_synced_at: table.last_synced_at ?? null,
|
||||
created_by: table.created_by,
|
||||
updated_by: table.updated_by ?? null,
|
||||
created_at: table.created_at,
|
||||
updated_at: table.updated_at,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 通过 gridKey 获取表格(用于 Luckysheet get-workerbook)
|
||||
export const getByGridKey = query({
|
||||
args: { gridKey: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
|
||||
// 返回 Luckysheet 需要的格式
|
||||
return {
|
||||
title: table.title,
|
||||
gridKey: table.grid_key,
|
||||
lang: "zh",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 通过 gridKey 获取完整表格数据(用于 Luckysheet load)
|
||||
export const getByGridKeyFull = query({
|
||||
args: { gridKey: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
|
||||
// 返回完整的表格数据,包括 schema 和 snapshot
|
||||
return {
|
||||
id: table.id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
snapshot: table.snapshot ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 列出文档的所有表格
|
||||
|
||||
// Query: 获取单个表格
|
||||
export const get = query({
|
||||
args: { userId: v.string(), tableId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
if (table.workspace_id) {
|
||||
// 验证用户是否在工作区中(简化版:假设 userId 有效)
|
||||
// TODO: 添加 workspace_members 验证
|
||||
}
|
||||
return {
|
||||
id: table.id,
|
||||
workspace_id: table.workspace_id,
|
||||
document_id: table.document_id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
view_preferences: table.view_preferences,
|
||||
snapshot: table.snapshot ?? null,
|
||||
is_archived: table.is_archived,
|
||||
last_synced_at: table.last_synced_at ?? null,
|
||||
created_by: table.created_by,
|
||||
updated_by: table.updated_by ?? null,
|
||||
created_at: table.created_at,
|
||||
updated_at: table.updated_at,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 通过 gridKey 获取表格(用于 Luckysheet get-workerbook)
|
||||
export const getByGridKey = query({
|
||||
args: { gridKey: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
|
||||
// 返回 Luckysheet 需要的格式
|
||||
return {
|
||||
title: table.title,
|
||||
gridKey: table.grid_key,
|
||||
lang: "zh",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 通过 gridKey 获取完整表格数据(用于 Luckysheet load)
|
||||
export const getByGridKeyFull = query({
|
||||
args: { gridKey: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_grid_key", (q) => q.eq("grid_key", args.gridKey))
|
||||
.first();
|
||||
if (!table) return null;
|
||||
|
||||
// 返回完整的表格数据,包括 schema 和 snapshot
|
||||
return {
|
||||
id: table.id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
snapshot: table.snapshot ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 列出文档的所有表格
|
||||
export const listByDocument = query({
|
||||
args: { userId: v.string(), documentId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const tables = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_document", (q) => q.eq("document_id", args.documentId))
|
||||
.collect();
|
||||
|
||||
return tables
|
||||
.filter((t) => !t.is_archived)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
document_id: t.document_id,
|
||||
grid_key: t.grid_key,
|
||||
title: t.title,
|
||||
schema: t.schema,
|
||||
view_preferences: t.view_preferences,
|
||||
snapshot: t.snapshot ?? null,
|
||||
is_archived: t.is_archived,
|
||||
last_synced_at: t.last_synced_at ?? null,
|
||||
created_at: t.created_at,
|
||||
updated_at: t.updated_at,
|
||||
const tables = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_document", (q) => q.eq("document_id", args.documentId))
|
||||
.collect();
|
||||
|
||||
return tables
|
||||
.filter((t) => !t.is_archived)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
document_id: t.document_id,
|
||||
grid_key: t.grid_key,
|
||||
title: t.title,
|
||||
schema: t.schema,
|
||||
view_preferences: t.view_preferences,
|
||||
snapshot: t.snapshot ?? null,
|
||||
is_archived: t.is_archived,
|
||||
last_synced_at: t.last_synced_at ?? null,
|
||||
created_at: t.created_at,
|
||||
updated_at: t.updated_at,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -150,22 +150,22 @@ export const listByWorkspaceForSearch = query({
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 创建表格
|
||||
export const create = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
documentId: v.string(),
|
||||
title: v.optional(v.string()),
|
||||
schema: v.optional(v.any()),
|
||||
snapshot: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const now = nowIso();
|
||||
const tableId = generateId();
|
||||
const gridKey = generateId(); // 用于 WebSocket 协同标识
|
||||
|
||||
|
||||
// Mutation: 创建表格
|
||||
export const create = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.string(),
|
||||
documentId: v.string(),
|
||||
title: v.optional(v.string()),
|
||||
schema: v.optional(v.any()),
|
||||
snapshot: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const now = nowIso();
|
||||
const tableId = generateId();
|
||||
const gridKey = generateId(); // 用于 WebSocket 协同标识
|
||||
|
||||
const table = {
|
||||
id: tableId,
|
||||
workspace_id: args.workspaceId,
|
||||
@@ -185,95 +185,95 @@ export const create = mutation({
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
await ctx.db.insert("document_tables", table);
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
snapshot: table.snapshot,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 更新表格
|
||||
export const update = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
tableId: v.string(),
|
||||
title: v.optional(v.string()),
|
||||
schema: v.optional(v.any()),
|
||||
view_preferences: v.optional(v.any()),
|
||||
snapshot: v.optional(v.any()),
|
||||
rows: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
|
||||
.first();
|
||||
if (!table) {
|
||||
throw new Error("Table not found");
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
const updates: any = {
|
||||
updated_at: now,
|
||||
updated_by: args.userId,
|
||||
last_synced_at: now,
|
||||
};
|
||||
|
||||
if (args.title !== undefined) updates.title = args.title;
|
||||
if (args.schema !== undefined) updates.schema = args.schema;
|
||||
if (args.view_preferences !== undefined) updates.view_preferences = args.view_preferences;
|
||||
if (args.snapshot !== undefined) updates.snapshot = args.snapshot;
|
||||
|
||||
await ctx.db.patch(table._id, updates);
|
||||
|
||||
// 如果提供了 rows,更新行数据
|
||||
if (args.rows && Array.isArray(args.rows)) {
|
||||
// 先删除旧行
|
||||
const existingRows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
for (const row of existingRows) {
|
||||
await ctx.db.delete(row._id);
|
||||
}
|
||||
|
||||
// 插入新行
|
||||
for (const [index, rowData] of args.rows.entries()) {
|
||||
await ctx.db.insert("document_table_rows", {
|
||||
id: generateId(),
|
||||
workspace_id: table.workspace_id,
|
||||
document_id: table.document_id,
|
||||
table_id: args.tableId,
|
||||
row_index: index,
|
||||
row_data: rowData,
|
||||
row_hash: JSON.stringify(rowData),
|
||||
is_deleted: false,
|
||||
updated_by: args.userId,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
title: updates.title ?? table.title,
|
||||
schema: updates.schema ?? table.schema,
|
||||
view_preferences: updates.view_preferences ?? table.view_preferences,
|
||||
snapshot: updates.snapshot ?? table.snapshot,
|
||||
updated_at: now,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 删除表格(软删除)
|
||||
|
||||
await ctx.db.insert("document_tables", table);
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
grid_key: table.grid_key,
|
||||
title: table.title,
|
||||
schema: table.schema,
|
||||
snapshot: table.snapshot,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 更新表格
|
||||
export const update = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
tableId: v.string(),
|
||||
title: v.optional(v.string()),
|
||||
schema: v.optional(v.any()),
|
||||
view_preferences: v.optional(v.any()),
|
||||
snapshot: v.optional(v.any()),
|
||||
rows: v.optional(v.any()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const table = await ctx.db
|
||||
.query("document_tables")
|
||||
.withIndex("by_table_id", (q) => q.eq("id", args.tableId))
|
||||
.first();
|
||||
if (!table) {
|
||||
throw new Error("Table not found");
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
const updates: any = {
|
||||
updated_at: now,
|
||||
updated_by: args.userId,
|
||||
last_synced_at: now,
|
||||
};
|
||||
|
||||
if (args.title !== undefined) updates.title = args.title;
|
||||
if (args.schema !== undefined) updates.schema = args.schema;
|
||||
if (args.view_preferences !== undefined) updates.view_preferences = args.view_preferences;
|
||||
if (args.snapshot !== undefined) updates.snapshot = args.snapshot;
|
||||
|
||||
await ctx.db.patch(table._id, updates);
|
||||
|
||||
// 如果提供了 rows,更新行数据
|
||||
if (args.rows && Array.isArray(args.rows)) {
|
||||
// 先删除旧行
|
||||
const existingRows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
for (const row of existingRows) {
|
||||
await ctx.db.delete(row._id);
|
||||
}
|
||||
|
||||
// 插入新行
|
||||
for (const [index, rowData] of args.rows.entries()) {
|
||||
await ctx.db.insert("document_table_rows", {
|
||||
id: generateId(),
|
||||
workspace_id: table.workspace_id,
|
||||
document_id: table.document_id,
|
||||
table_id: args.tableId,
|
||||
row_index: index,
|
||||
row_data: rowData,
|
||||
row_hash: JSON.stringify(rowData),
|
||||
is_deleted: false,
|
||||
updated_by: args.userId,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
title: updates.title ?? table.title,
|
||||
schema: updates.schema ?? table.schema,
|
||||
view_preferences: updates.view_preferences ?? table.view_preferences,
|
||||
snapshot: updates.snapshot ?? table.snapshot,
|
||||
updated_at: now,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 删除表格(软删除)
|
||||
export const remove = mutation({
|
||||
args: { userId: v.string(), tableId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
@@ -324,8 +324,8 @@ export const restore = mutation({
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
// Mutation: 永久删除表格
|
||||
|
||||
// Mutation: 永久删除表格
|
||||
export const purge = mutation({
|
||||
args: { userId: v.string(), tableId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
@@ -336,17 +336,17 @@ export const purge = mutation({
|
||||
if (!table) {
|
||||
throw new Error("Table not found");
|
||||
}
|
||||
|
||||
// 删除关联的行
|
||||
const rows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
for (const row of rows) {
|
||||
await ctx.db.delete(row._id);
|
||||
}
|
||||
|
||||
|
||||
// 删除关联的行
|
||||
const rows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
for (const row of rows) {
|
||||
await ctx.db.delete(row._id);
|
||||
}
|
||||
|
||||
// 删除表格
|
||||
await ctx.db.delete(table._id);
|
||||
|
||||
@@ -392,19 +392,19 @@ export const emptyTrashByWorkspace = mutation({
|
||||
return { ok: true, deleted: targets.length };
|
||||
},
|
||||
});
|
||||
|
||||
// Query: 获取表格行数据
|
||||
|
||||
// Query: 获取表格行数据
|
||||
export const getRows = query({
|
||||
args: { tableId: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const rows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
return rows
|
||||
.filter((r) => !r.is_deleted)
|
||||
.sort((a, b) => a.row_index - b.row_index)
|
||||
const rows = await ctx.db
|
||||
.query("document_table_rows")
|
||||
.withIndex("by_table", (q) => q.eq("table_id", args.tableId))
|
||||
.collect();
|
||||
|
||||
return rows
|
||||
.filter((r) => !r.is_deleted)
|
||||
.sort((a, b) => a.row_index - b.row_index)
|
||||
.map((r) => r.row_data);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user