0.6 rust重构01

This commit is contained in:
lix-2026
2026-04-14 13:22:29 +08:00
parent 71fb1aee7e
commit 84a8454fa9
401 changed files with 5841 additions and 1512 deletions
+6
View File
@@ -9,6 +9,7 @@
*/
import type * as _utils_attachmentExtract from "../_utils/attachmentExtract.js";
import type * as _utils_auth from "../_utils/auth.js";
import type * as _utils_documentTree from "../_utils/documentTree.js";
import type * as _utils_id from "../_utils/id.js";
import type * as _utils_ingestJobs from "../_utils/ingestJobs.js";
@@ -16,8 +17,10 @@ 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 audit from "../audit.js";
import type * as auth from "../auth.js";
import type * as blocks from "../blocks.js";
import type * as bridgeLogs from "../bridgeLogs.js";
import type * as comments from "../comments.js";
import type * as crons from "../crons.js";
import type * as documentGroupShares from "../documentGroupShares.js";
@@ -49,6 +52,7 @@ import type {
declare const fullApi: ApiFromModules<{
"_utils/attachmentExtract": typeof _utils_attachmentExtract;
"_utils/auth": typeof _utils_auth;
"_utils/documentTree": typeof _utils_documentTree;
"_utils/id": typeof _utils_id;
"_utils/ingestJobs": typeof _utils_ingestJobs;
@@ -56,8 +60,10 @@ declare const fullApi: ApiFromModules<{
"_utils/text": typeof _utils_text;
"_utils/time": typeof _utils_time;
agentActions: typeof agentActions;
audit: typeof audit;
auth: typeof auth;
blocks: typeof blocks;
bridgeLogs: typeof bridgeLogs;
comments: typeof comments;
crons: typeof crons;
documentGroupShares: typeof documentGroupShares;
+25
View File
@@ -0,0 +1,25 @@
import { getAuthUserId } from "@convex-dev/auth/server";
function readEnv(key: string): string | undefined {
const raw = process.env[key];
if (!raw) return undefined;
const trimmed = raw.trim();
return trimmed ? trimmed : undefined;
}
function isDevAuthEnabled(): boolean {
return readEnv("MNOTE_DEV_AUTH") === "1" || readEnv("NEXT_PUBLIC_MNOTE_DEV_AUTH") === "1";
}
export async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
if (userId !== null) {
return String(userId);
}
if (isDevAuthEnabled()) {
return readEnv("DEV_USER_ID") ?? "dev-user";
}
throw new Error("未登录");
}
@@ -0,0 +1,73 @@
type DocumentRecordLike = {
_id: unknown;
id?: string | null;
user_id?: string | null;
parent_id?: string | null;
deleted_at?: string | null;
created_at?: string | null;
updated_at?: string | null;
};
export function compareDocumentCanonicalOrder(a: DocumentRecordLike, b: DocumentRecordLike): number {
const aDeleted = a?.deleted_at != null;
const bDeleted = b?.deleted_at != null;
if (aDeleted !== bDeleted) {
return aDeleted ? 1 : -1;
}
const updatedA = String(a?.updated_at ?? "");
const updatedB = String(b?.updated_at ?? "");
if (updatedA !== updatedB) {
return updatedB.localeCompare(updatedA);
}
const createdA = String(a?.created_at ?? "");
const createdB = String(b?.created_at ?? "");
if (createdA !== createdB) {
return createdB.localeCompare(createdA);
}
return String(a?._id ?? "").localeCompare(String(b?._id ?? ""));
}
export function pickCanonicalDocumentRecord<T extends DocumentRecordLike>(records: T[]): T | null {
if (records.length === 0) return null;
return [...records].sort(compareDocumentCanonicalOrder)[0] ?? null;
}
export async function getCanonicalDocumentByBusinessId<T extends DocumentRecordLike>(
ctx: any,
documentId: string,
): Promise<T | null> {
const records = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", documentId))
.collect();
return pickCanonicalDocumentRecord(records) as T | null;
}
export async function getCanonicalParentDocumentId(
ctx: any,
documentId: string | null | undefined,
): Promise<string | null> {
const normalizedId = String(documentId ?? "").trim();
if (!normalizedId) return null;
const doc = await getCanonicalDocumentByBusinessId<DocumentRecordLike>(ctx, normalizedId);
return doc?.parent_id ?? null;
}
export async function requireCanonicalOwnedDocument<T extends DocumentRecordLike & { user_id?: string | null }>(
ctx: any,
documentId: string,
userId: string,
): Promise<T> {
const doc = await getCanonicalDocumentByBusinessId<T>(ctx, documentId);
if (!doc) {
throw new Error("页面不存在");
}
if (String(doc.user_id ?? "") !== String(userId)) {
throw new Error("无权限");
}
return doc;
}
+63
View File
@@ -0,0 +1,63 @@
import { query } from "./_generated/server";
import { v } from "convex/values";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
async function requireWorkspaceMember(ctx: any, workspaceId: string, userId: string) {
const member = await ctx.db
.query("workspace_members")
.withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", workspaceId).eq("user_id", userId))
.first();
if (!member) {
throw new Error("无权限");
}
return member;
}
export const listByTrace = query({
args: { workspaceId: v.string(), traceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const commandLogs = await ctx.db
.query("command_logs")
.withIndex("by_workspace_trace", (q) => q.eq("workspace_id", args.workspaceId).eq("trace_id", args.traceId))
.collect();
const domainEvents = await ctx.db
.query("domain_events")
.withIndex("by_workspace_trace", (q) => q.eq("workspace_id", args.workspaceId).eq("trace_id", args.traceId))
.collect();
return {
trace_id: args.traceId,
command_logs: commandLogs,
domain_events: domainEvents,
generated_at: nowIso(),
};
},
});
export const listByRequest = query({
args: { workspaceId: v.string(), requestId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const commandLogs = await ctx.db
.query("command_logs")
.withIndex("by_workspace_request", (q) => q.eq("workspace_id", args.workspaceId).eq("request_id", args.requestId))
.collect();
const domainEvents = await ctx.db
.query("domain_events")
.withIndex("by_workspace_request", (q) => q.eq("workspace_id", args.workspaceId).eq("request_id", args.requestId))
.collect();
return {
request_id: args.requestId,
command_logs: commandLogs,
domain_events: domainEvents,
generated_at: nowIso(),
};
},
});
+173
View File
@@ -0,0 +1,173 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
async function requireWorkspaceMember(ctx: any, workspaceId: string, userId: string) {
const member = await ctx.db
.query("workspace_members")
.withIndex("by_workspace_user", (q: any) => q.eq("workspace_id", workspaceId).eq("user_id", userId))
.first();
if (!member) {
throw new Error("无权限");
}
return member;
}
export const recordCommandLog = mutation({
args: {
workspaceId: v.string(),
id: v.string(),
requestId: v.string(),
traceId: v.string(),
commandId: v.string(),
commandName: v.string(),
actorId: v.string(),
actorType: v.string(),
sourceChannel: v.string(),
sourceClient: v.string(),
status: v.union(v.literal("pending"), v.literal("succeeded"), v.literal("failed"), v.literal("rolled_back")),
targetPageId: v.optional(v.union(v.string(), v.null())),
targetBlockId: v.optional(v.union(v.string(), v.null())),
payload: v.any(),
payloadSummary: v.string(),
refs: v.array(v.string()),
idempotencyKey: v.optional(v.union(v.string(), v.null())),
error: v.optional(v.union(v.string(), v.null())),
createdAt: v.string(),
finishedAt: v.optional(v.union(v.string(), v.null())),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const existing = await ctx.db
.query("command_logs")
.withIndex("by_command_log_id", (q) => q.eq("id", args.id))
.first();
if (existing) {
return { ok: true, id: existing.id, duplicated: true };
}
await ctx.db.insert("command_logs", {
id: args.id,
workspace_id: args.workspaceId,
request_id: args.requestId,
trace_id: args.traceId,
command_id: args.commandId,
command_name: args.commandName,
actor_id: args.actorId,
actor_type: args.actorType,
source_channel: args.sourceChannel,
source_client: args.sourceClient,
status: args.status,
target_page_id: args.targetPageId ?? null,
target_block_id: args.targetBlockId ?? null,
payload: args.payload,
payload_summary: args.payloadSummary,
refs: args.refs,
idempotency_key: args.idempotencyKey ?? null,
error: args.error ?? null,
created_at: args.createdAt,
finished_at: args.finishedAt ?? null,
});
return { ok: true, id: args.id, duplicated: false };
},
});
export const recordDomainEvent = mutation({
args: {
workspaceId: v.string(),
id: v.string(),
requestId: v.string(),
traceId: v.string(),
commandId: v.string(),
commandLogId: v.string(),
eventType: v.string(),
aggregateType: v.string(),
aggregateId: v.string(),
eventVersion: v.number(),
status: v.union(v.literal("pending"), v.literal("committed"), v.literal("rejected"), v.literal("failed")),
actorType: v.string(),
payload: v.any(),
createdAt: v.string(),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const existing = await ctx.db
.query("domain_events")
.withIndex("by_domain_event_id", (q) => q.eq("id", args.id))
.first();
if (existing) {
return { ok: true, id: existing.id, duplicated: true };
}
await ctx.db.insert("domain_events", {
id: args.id,
workspace_id: args.workspaceId,
request_id: args.requestId,
trace_id: args.traceId,
command_id: args.commandId,
command_log_id: args.commandLogId,
event_type: args.eventType,
aggregate_type: args.aggregateType,
aggregate_id: args.aggregateId,
event_version: args.eventVersion,
status: args.status,
actor_type: args.actorType,
payload: args.payload,
created_at: args.createdAt,
});
return { ok: true, id: args.id, duplicated: false };
},
});
export const listByTrace = query({
args: { workspaceId: v.string(), traceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const commandLogs = await ctx.db
.query("command_logs")
.withIndex("by_workspace_trace", (q) => q.eq("workspace_id", args.workspaceId).eq("trace_id", args.traceId))
.collect();
const domainEvents = await ctx.db
.query("domain_events")
.withIndex("by_workspace_trace", (q) => q.eq("workspace_id", args.workspaceId).eq("trace_id", args.traceId))
.collect();
return {
trace_id: args.traceId,
command_logs: commandLogs,
domain_events: domainEvents,
generated_at: nowIso(),
};
},
});
export const listByRequest = query({
args: { workspaceId: v.string(), requestId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
await requireWorkspaceMember(ctx, args.workspaceId, userId);
const commandLogs = await ctx.db
.query("command_logs")
.withIndex("by_workspace_request", (q) => q.eq("workspace_id", args.workspaceId).eq("request_id", args.requestId))
.collect();
const domainEvents = await ctx.db
.query("domain_events")
.withIndex("by_workspace_request", (q) => q.eq("workspace_id", args.workspaceId).eq("request_id", args.requestId))
.collect();
return {
request_id: args.requestId,
command_logs: commandLogs,
domain_events: domainEvents,
generated_at: nowIso(),
};
},
});
+10 -38
View File
@@ -2,6 +2,7 @@ import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
import { getCanonicalDocumentByBusinessId, getCanonicalParentDocumentId } from "./_utils/documentRecord";
async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
@@ -43,11 +44,7 @@ async function resolveSharePermission(ctx: any, doc: any, userId: string): Promi
return parentShare.permission as SharePermission;
}
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
return null;
@@ -83,11 +80,7 @@ async function resolveGroupSharePermission(ctx: any, doc: any, userId: string):
return "read";
}
}
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
return null;
@@ -132,10 +125,7 @@ export const listThreadsByDocument = query({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", args.documentId))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.documentId);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireCanViewDocument(ctx, doc, userId);
@@ -190,10 +180,7 @@ export const listMessagesByThread = query({
.first();
if (!thread) throw new Error("评论线程不存在");
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", thread.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, String(thread.document_id));
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireCanViewDocument(ctx, doc, userId);
@@ -241,10 +228,7 @@ export const createThread = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", args.documentId))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.documentId);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireCanViewDocument(ctx, doc, userId);
@@ -305,10 +289,7 @@ export const reply = mutation({
.first();
if (!thread) throw new Error("评论线程不存在");
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", thread.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, String(thread.document_id));
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireCanViewDocument(ctx, doc, userId);
@@ -354,10 +335,7 @@ export const setResolved = mutation({
.first();
if (!thread) throw new Error("评论线程不存在");
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", thread.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, String(thread.document_id));
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
@@ -393,10 +371,7 @@ export const editMessage = mutation({
.first();
if (!thread) throw new Error("评论线程不存在");
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", message.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, String(message.document_id));
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireCanViewDocument(ctx, doc, userId);
@@ -444,10 +419,7 @@ export const deleteMessage = mutation({
.first();
if (!thread) throw new Error("评论线程不存在");
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", message.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, String(message.document_id));
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
+4 -13
View File
@@ -2,6 +2,7 @@ import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
import { requireCanonicalOwnedDocument } from "./_utils/documentRecord";
type Permission = "read" | "edit";
@@ -11,16 +12,6 @@ async function requireUserId(ctx: any): Promise<string> {
return String(userId);
}
async function requireOwnedDocument(ctx: any, documentId: string, userId: string) {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", documentId))
.first();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
return doc;
}
async function requireGroup(ctx: any, groupId: string) {
const group = await ctx.db
.query("groups")
@@ -43,7 +34,7 @@ export const listByDocument = query({
args: { documentId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const shares = await ctx.db
.query("document_group_shares")
@@ -94,7 +85,7 @@ export const upsert = mutation({
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const group = await requireGroup(ctx, args.groupId);
if (group.workspace_id !== doc.workspace_id) throw new Error("群组不属于当前工作空间");
await requireGroupMember(ctx, args.groupId, userId);
@@ -178,7 +169,7 @@ export const remove = mutation({
args: { documentId: v.string(), groupId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const existingShare = await ctx.db
.query("document_group_shares")
+5 -17
View File
@@ -3,6 +3,7 @@ import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
import type { Id } from "./_generated/dataModel";
import { requireCanonicalOwnedDocument, getCanonicalDocumentByBusinessId } from "./_utils/documentRecord";
const permission = v.union(v.literal("read"), v.literal("edit"));
@@ -20,16 +21,6 @@ function normalizeUsername(raw: string): string {
return username;
}
async function requireOwnedDocument(ctx: any, documentId: string, userId: string) {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", documentId))
.first();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
return doc;
}
async function resolveUserIdByUsername(ctx: any, rawUsername: string): Promise<{ userId: string; username: string }> {
const username = normalizeUsername(rawUsername);
@@ -47,7 +38,7 @@ export const listByDocument = query({
args: { documentId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const shares = await ctx.db
.query("document_shares")
@@ -84,7 +75,7 @@ export const upsert = mutation({
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const { userId: sharedWithUserId } = await resolveUserIdByUsername(ctx, args.username);
if (sharedWithUserId === userId) throw new Error("不能共享给自己");
@@ -149,7 +140,7 @@ export const remove = mutation({
args: { documentId: v.string(), sharedWithUserId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await requireOwnedDocument(ctx, args.documentId, userId);
const doc = await requireCanonicalOwnedDocument(ctx, args.documentId, userId);
const existingShare = await ctx.db
.query("document_shares")
@@ -294,10 +285,7 @@ export const listMyShareRoots = query({
if (documentCache.has(documentId)) {
return cached ?? { exists: false, deleted: true, title: null };
}
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", documentId))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, documentId);
const info = doc
? { exists: true, deleted: doc.deleted_at != null, title: (doc.title ?? null) as string | null }
: { exists: false, deleted: true, title: null };
+5 -20
View File
@@ -2,6 +2,7 @@ import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
import { getCanonicalDocumentByBusinessId, getCanonicalParentDocumentId } from "./_utils/documentRecord";
type SharePermission = "read" | "edit";
@@ -34,12 +35,7 @@ async function resolveSharePermission(ctx: any, doc: any, userId: string): Promi
.withIndex("by_doc_user", (q: any) => q.eq("document_id", parentId).eq("shared_with_user_id", userId))
.first();
if (parentShare && parentShare.include_descendants) return parentShare.permission as SharePermission;
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
return null;
}
@@ -89,12 +85,7 @@ async function resolveGroupSharePermission(ctx: any, doc: any, userId: string):
let parentId: string | null = doc.parent_id ?? null;
for (let depth = 0; depth < 60 && parentId; depth += 1) {
if (await checkDocId(parentId, true)) return "edit";
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
return best;
@@ -109,10 +100,7 @@ export const isStarred = query({
if (authUserId === null) return false;
const userId = String(authUserId);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", args.documentId))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.documentId);
if (!doc) return false;
if (doc.deleted_at != null) return false;
@@ -137,10 +125,7 @@ export const toggle = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", args.documentId))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.documentId);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
+28 -79
View File
@@ -1,21 +1,14 @@
import { internalQuery, mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
import { collectSubtree } from "./_utils/documentTree";
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
import { extractTextFromDocumentContent } from "./_utils/text";
import { getCanonicalDocumentByBusinessId, getCanonicalParentDocumentId } from "./_utils/documentRecord";
const accessScope = v.union(v.literal("private"), v.literal("shared"), v.literal("public"));
async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
if (userId === null) {
throw new Error("未登录");
}
return userId;
}
type SharePermission = "read" | "edit";
type SharePolicy = { permission: SharePermission; disableDownload: boolean; disableCopy: boolean };
@@ -58,11 +51,7 @@ async function resolveSharePolicy(ctx: any, doc: any, userId: string): Promise<S
};
}
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
return null;
@@ -127,11 +116,7 @@ async function resolveGroupSharePolicy(ctx: any, doc: any, userId: string): Prom
for (let depth = 0; depth < 60 && parentId; depth += 1) {
await checkDocId(parentId, true);
const parent = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", parentId))
.first();
parentId = parent?.parent_id ?? null;
parentId = await getCanonicalParentDocumentId(ctx, parentId);
}
if (!best) return null;
@@ -331,10 +316,7 @@ export const getMeta = query({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.deleted_at != null) return null;
try {
@@ -394,10 +376,7 @@ export const getMeta = query({
export const getPermissionForUser = query({
args: { userId: v.string(), id: v.string() },
handler: async (ctx, args) => {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.deleted_at != null) return null;
@@ -425,10 +404,7 @@ export const getPermissionForUser = query({
export const getMetaForIngest = internalQuery({
args: { userId: v.string(), id: v.string() },
handler: async (ctx, args) => {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.user_id !== args.userId) return null;
return {
@@ -459,10 +435,7 @@ export const getContent = query({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.deleted_at != null) return null;
try {
@@ -486,10 +459,7 @@ export const getContent = query({
export const getContentForIngest = internalQuery({
args: { userId: v.string(), id: v.string() },
handler: async (ctx, args) => {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) return null;
if (doc.user_id !== args.userId) return null;
return { content: doc.content ?? null };
@@ -797,6 +767,10 @@ export const create = mutation({
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const existing = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (existing && existing.deleted_at == null) {
throw new Error("页面已存在");
}
const siblings = await ctx.db
.query("documents")
@@ -867,10 +841,7 @@ export const updateContent = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
@@ -896,10 +867,7 @@ export const updateTitle = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
@@ -920,10 +888,7 @@ export const setTemplate = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
@@ -950,10 +915,7 @@ export const move = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
@@ -1040,10 +1002,7 @@ export const softDelete = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
@@ -1075,10 +1034,7 @@ export const restore = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
@@ -1115,10 +1071,7 @@ export const purge = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== userId) throw new Error("无权限");
@@ -1194,10 +1147,7 @@ export const updateOptions = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
@@ -1249,10 +1199,7 @@ export const updateStats = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
@@ -1284,13 +1231,15 @@ export const duplicate = mutation({
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const source = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.sourceId))
.first();
const source = await getCanonicalDocumentByBusinessId(ctx, args.sourceId);
if (!source) throw new Error("页面不存在或无权限访问");
if (source.user_id !== userId) throw new Error("页面不存在或无权限访问");
const existingTarget = await getCanonicalDocumentByBusinessId(ctx, args.newId);
if (existingTarget && existingTarget.deleted_at == null) {
throw new Error("目标页面已存在");
}
const siblings = await ctx.db
.query("documents")
.withIndex("by_workspace_parent", (q) =>
+2 -4
View File
@@ -4,6 +4,7 @@ import { nowIso } from "./_utils/time";
import { enqueueExtractMediaAssetTextJob, enqueueIngestMediaAssetJob } from "./_utils/ingestJobs";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { internal } from "./_generated/api";
import { getCanonicalDocumentByBusinessId } from "./_utils/documentRecord";
async function assertWorkspaceMember(ctx: QueryCtx | MutationCtx, userId: string, workspaceId: string) {
const membership = await ctx.db
@@ -302,10 +303,7 @@ export const createWithStorage = mutation({
handler: async (ctx, args) => {
await assertWorkspaceMember(ctx, args.userId, args.asset.workspace_id);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.asset.document_id))
.first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, args.asset.document_id);
if (!doc || doc.workspace_id !== args.asset.workspace_id) {
throw new Error("目标页面不存在或不属于该工作空间");
+3 -20
View File
@@ -1,9 +1,10 @@
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
import { enqueueIngestMindmapJob } from "./_utils/ingestJobs";
import { internal } from "./_generated/api";
import { requireCanonicalOwnedDocument } from "./_utils/documentRecord";
const defaultMindmapData = {
data: { text: "中心主题" },
@@ -19,14 +20,6 @@ function resolveGraceSeconds(): number {
return Math.floor(parsed);
}
async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
if (userId === null) {
throw new Error("未登录");
}
return userId;
}
function normalizeMindmapId(docId: string, mindmapId: string): string {
const raw = String(mindmapId ?? "").trim();
if (raw) return raw;
@@ -35,17 +28,7 @@ function normalizeMindmapId(docId: string, mindmapId: string): string {
}
async function requireOwnedDocument(ctx: any, userId: string, docId: string) {
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q: any) => q.eq("id", docId))
.first();
if (!doc) {
throw new Error("页面不存在");
}
if (doc.user_id !== userId) {
throw new Error("无权限");
}
return doc;
return await requireCanonicalOwnedDocument<any>(ctx, docId, userId);
}
export const get = query({
+2 -1
View File
@@ -2,6 +2,7 @@ import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { nowIso } from "./_utils/time";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { getCanonicalDocumentByBusinessId } from "./_utils/documentRecord";
async function assertWorkspaceMember(ctx: QueryCtx | MutationCtx, userId: string, workspaceId: string) {
const membership = await ctx.db
@@ -120,7 +121,7 @@ export const listBacklinks = query({
const sourceTitleById = new Map<string, string | null>();
for (const sid of sourceIds) {
const doc = await ctx.db.query("documents").withIndex("by_document_id", (q) => q.eq("id", sid)).first();
const doc = await getCanonicalDocumentByBusinessId<any>(ctx, sid);
sourceTitleById.set(sid, doc ? (doc.title ?? null) : null);
}
+48
View File
@@ -446,4 +446,52 @@ export default defineSchema({
.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"]),
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"]),
});
+1 -9
View File
@@ -1,6 +1,6 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { requireUserId } from "./_utils/auth";
import { nowIso } from "./_utils/time";
import type { MutationCtx, QueryCtx } from "./_generated/server";
@@ -13,14 +13,6 @@ type WorkspaceSummary = {
isDefault: boolean;
};
async function requireUserId(ctx: any): Promise<string> {
const userId = await getAuthUserId(ctx);
if (userId === null) {
throw new Error("未登录");
}
return userId;
}
async function findWorkspaceById(ctx: QueryCtx | MutationCtx, workspaceId: string) {
return await ctx.db
.query("workspaces")