0.3.1 UI修复

This commit is contained in:
liaibo
2026-01-18 19:01:31 +08:00
parent 90a38b48cf
commit 8a5b915002
86 changed files with 1160 additions and 2867 deletions
+116 -35
View File
@@ -1,11 +1,54 @@
import { mutation, query } from "./_generated/server";
import { internalQuery, mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import { nowIso } from "./_utils/time";
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
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;
}
export const getMeta = query({
args: { id: v.string() },
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();
if (!doc) return null;
if (doc.user_id !== userId) return null;
return {
id: doc.id,
user_id: doc.user_id,
workspace_id: doc.workspace_id,
access_scope: doc.access_scope,
title: doc.title ?? null,
parent_id: doc.parent_id ?? null,
created_at: doc.created_at,
updated_at: doc.updated_at ?? null,
wide_layout: doc.wide_layout ?? null,
use_small_text: doc.use_small_text ?? null,
show_heading_numbers: doc.show_heading_numbers ?? null,
show_toc: doc.show_toc ?? null,
show_structure: doc.show_structure ?? null,
protect_editing: doc.protect_editing ?? null,
show_word_count: doc.show_word_count ?? null,
word_count: doc.word_count ?? null,
character_count: doc.character_count ?? null,
block_count: doc.block_count ?? null,
};
},
});
export const getMetaForIngest = internalQuery({
args: { userId: v.string(), id: v.string() },
handler: async (ctx, args) => {
const doc = await ctx.db
@@ -38,6 +81,21 @@ export const getMeta = query({
});
export const getContent = query({
args: { id: v.string() },
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();
if (!doc) return null;
if (doc.user_id !== userId) return null;
return { content: doc.content ?? null };
},
});
export const getContentForIngest = internalQuery({
args: { userId: v.string(), id: v.string() },
handler: async (ctx, args) => {
const doc = await ctx.db
@@ -51,8 +109,10 @@ export const getContent = query({
});
export const listByWorkspace = query({
args: { userId: v.string(), workspaceId: v.string() },
args: { workspaceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const docs = await ctx.db
.query("documents")
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
@@ -60,7 +120,7 @@ export const listByWorkspace = query({
// 说明:阶段 4 先不做垃圾桶(deleted_at != null),因此这里直接过滤。
return docs
.filter((d) => d.user_id === args.userId)
.filter((d) => d.user_id === userId)
.filter((d) => d.deleted_at == null)
.map((d) => ({
access_scope: d.access_scope,
@@ -78,15 +138,17 @@ export const listByWorkspace = query({
});
export const listTrashedByWorkspace = query({
args: { userId: v.string(), workspaceId: v.string() },
args: { workspaceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const docs = await ctx.db
.query("documents")
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
.collect();
return docs
.filter((d) => d.user_id === args.userId)
.filter((d) => d.user_id === userId)
.filter((d) => d.deleted_at != null)
.sort((a, b) => (b.deleted_at ?? "").localeCompare(a.deleted_at ?? ""))
.slice(0, 100)
@@ -102,7 +164,6 @@ export const listTrashedByWorkspace = query({
export const create = mutation({
args: {
userId: v.string(),
id: v.string(),
workspaceId: v.string(),
parentId: v.union(v.string(), v.null()),
@@ -111,6 +172,8 @@ export const create = mutation({
content: v.optional(v.any()),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const siblings = await ctx.db
.query("documents")
.withIndex("by_workspace_parent", (q) => q.eq("workspace_id", args.workspaceId).eq("parent_id", args.parentId))
@@ -124,7 +187,7 @@ export const create = mutation({
await ctx.db.insert("documents", {
id: args.id,
user_id: args.userId,
user_id: userId,
workspace_id: args.workspaceId,
parent_id: args.parentId,
title,
@@ -166,33 +229,37 @@ export const create = mutation({
});
export const updateContent = mutation({
args: { userId: v.string(), id: v.string(), content: v.any() },
args: { id: v.string(), content: v.any() },
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, { content: args.content, updated_at: ts });
// 说明:在 Convex 模式下,把自动入库/LightRAG 触发迁到 Convex jobs/actions。
// 说明:在 Convex 模式下,把"自动入库/LightRAG 触发"迁到 Convex jobs/actions。
// 采用 debounce,避免频繁保存时触发过多任务。
await enqueueIngestDocumentJob(ctx, { userId: args.userId, documentId: args.id, debounceMs: 1500 });
await enqueueIngestDocumentJob(ctx, { userId, documentId: args.id, debounceMs: 1500 });
return { ok: true, updated_at: ts };
},
});
export const updateTitle = mutation({
args: { userId: v.string(), id: v.string(), title: v.union(v.string(), v.null()) },
args: { id: v.string(), title: v.union(v.string(), v.null()) },
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, { title: args.title, updated_at: ts });
return { ok: true, updated_at: ts };
@@ -201,18 +268,19 @@ export const updateTitle = mutation({
export const move = mutation({
args: {
userId: v.string(),
id: v.string(),
parentId: v.union(v.string(), v.null()),
sortOrder: v.number(),
},
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, {
parent_id: args.parentId,
@@ -224,29 +292,33 @@ export const move = mutation({
});
export const softDelete = mutation({
args: { userId: v.string(), id: v.string() },
args: { id: v.string() },
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, { deleted_at: ts, deleted_by: args.userId, updated_at: ts });
await ctx.db.patch(doc._id, { deleted_at: ts, deleted_by: userId, updated_at: ts });
return { ok: true };
},
});
export const restore = mutation({
args: { userId: v.string(), id: v.string() },
args: { id: v.string() },
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, {
deleted_at: null,
@@ -260,26 +332,30 @@ export const restore = mutation({
});
export const purge = mutation({
args: { userId: v.string(), id: v.string() },
args: { id: v.string() },
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
await ctx.db.delete(doc._id);
return { ok: true };
},
});
export const emptyTrashByWorkspace = mutation({
args: { userId: v.string(), workspaceId: v.string() },
args: { workspaceId: v.string() },
handler: async (ctx, args) => {
// 说明:阶段 4/5 先用“membership 存在即可”的规则,避免引入复杂权限模型。
const userId = await requireUserId(ctx);
// 说明:阶段 4/5 先用"membership 存在即可"的规则,避免引入复杂权限模型。
const membership = await ctx.db
.query("workspace_members")
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", args.userId))
.withIndex("by_workspace_user", (q) => q.eq("workspace_id", args.workspaceId).eq("user_id", userId))
.first();
if (!membership) {
@@ -291,7 +367,7 @@ export const emptyTrashByWorkspace = mutation({
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
.collect();
const toDelete = docs.filter((d) => d.user_id === args.userId && d.deleted_at != null);
const toDelete = docs.filter((d) => d.user_id === userId && d.deleted_at != null);
for (const d of toDelete) {
await ctx.db.delete(d._id);
}
@@ -302,7 +378,6 @@ export const emptyTrashByWorkspace = mutation({
export const updateOptions = mutation({
args: {
userId: v.string(),
id: v.string(),
options: v.object({
wideLayout: v.optional(v.boolean()),
@@ -315,12 +390,14 @@ 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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const patch: Record<string, unknown> = {};
if (typeof args.options.wideLayout === "boolean") patch.wide_layout = args.options.wideLayout;
@@ -344,19 +421,20 @@ export const updateOptions = mutation({
export const updateStats = mutation({
args: {
userId: v.string(),
id: v.string(),
wordCount: v.number(),
characterCount: v.number(),
blockCount: v.number(),
},
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();
if (!doc) throw new Error("页面不存在");
if (doc.user_id !== args.userId) throw new Error("无权限");
if (doc.user_id !== userId) throw new Error("无权限");
const ts = nowIso();
await ctx.db.patch(doc._id, {
word_count: args.wordCount,
@@ -370,18 +448,19 @@ export const updateStats = mutation({
export const duplicate = mutation({
args: {
userId: v.string(),
sourceId: v.string(),
newId: v.string(),
title: v.optional(v.union(v.string(), v.null())),
},
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();
if (!source) throw new Error("页面不存在或无权限访问");
if (source.user_id !== args.userId) throw new Error("页面不存在或无权限访问");
if (source.user_id !== userId) throw new Error("页面不存在或无权限访问");
const siblings = await ctx.db
.query("documents")
@@ -397,7 +476,7 @@ export const duplicate = mutation({
await ctx.db.insert("documents", {
id: args.newId,
user_id: args.userId,
user_id: userId,
workspace_id: source.workspace_id,
parent_id: source.parent_id,
title,
@@ -438,15 +517,17 @@ export const duplicate = mutation({
});
export const listAllForCopy = query({
args: { userId: v.string(), workspaceId: v.string() },
args: { workspaceId: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const docs = await ctx.db
.query("documents")
.withIndex("by_workspace", (q) => q.eq("workspace_id", args.workspaceId))
.collect();
return docs
.filter((d) => d.user_id === args.userId)
.filter((d) => d.user_id === userId)
.filter((d) => d.deleted_at == null)
.map((d) => ({
id: d.id,