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
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import {
@@ -36,12 +35,7 @@ export async function POST(request: Request) {
const resolvedUserId = async () => {
if (userId) return userId;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) return null;
return session.user.id;
return null;
};
const finalUserId = await resolvedUserId();
@@ -4,7 +4,6 @@ import { loadLocalAiConfig } from "@/lib/ai/localAiConfig";
import { createToolRegistry, resolveAllowedToolIds } from "@/lib/ai-agent/tools/registry";
import { builtinTools, builtinToolSets } from "@/lib/ai-agent/tools/builtins/registryBuiltins";
import { runAiAgent } from "@/lib/ai-agent/runtime/runAgent";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { searchSearxng } from "@/lib/ai-agent/tools/builtins/searchWeb";
import { createMindmapServerTools, type MindmapSupabaseClient } from "@/lib/ai-agent/tools/builtins/mindmap/mindmapServerTools";
@@ -80,12 +79,7 @@ export async function POST(request: Request) {
const { auth, client } = await getAuthedConvexClient();
return { userId: auth.userId, supabase: null as any, convexClient: client };
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) return { userId: "", supabase: null as any, convexClient: null as any };
return { userId: session.user.id, supabase, convexClient: null as any };
return { userId: "", supabase: null as any, convexClient: null as any };
})();
if (!userId) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
@@ -1,18 +1,8 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
export async function POST(request: NextRequest) {
const supabase = await createSupabaseRouteClient();
const { event, session } = await request.json();
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
await supabase.auth.setSession(session);
}
if (event === "SIGNED_OUT") {
await supabase.auth.signOut();
}
// 说明:历史兼容接口(Supabase Auth callback)。当前项目迁移到 Convex 后,这里保持空实现。
await request.json().catch(() => null);
return NextResponse.json({ success: true });
}
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { isDevAuthEnabled } from "@/lib/auth/devUser";
interface SignInRequest {
email: string;
@@ -19,15 +20,14 @@ export async function POST(request: Request) {
// Convex 模式
if (isConvexEnabled()) {
try {
const body = await request.json() as SignInRequest;
const { email, password, name, flow } = body;
await request.json().catch(() => null);
// TODO: 调用 Convex Auth mutation 处理登录/注册
// 当前暂时返回未实现错误
return NextResponse.json(
{ error: "Convex Auth API 路由暂未实现,请稍后实现" },
{ status: 501 }
);
// 说明:当前项目默认使用开发用户鉴权(USE_CONVEX=1),无需走登录/注册
if (isDevAuthEnabled()) {
return NextResponse.json({ ok: true, mode: "dev_user" });
}
return NextResponse.json({ error: "Convex Auth 登录/注册尚未接入" }, { status: 501 });
} catch (error) {
console.error("Convex Auth error:", error);
return NextResponse.json(
@@ -37,74 +37,5 @@ export async function POST(request: Request) {
}
}
// Supabase 模式(保留兼容)
try {
const { createSupabaseRouteClient } = await import("@/lib/supabase/server");
const supabase = await createSupabaseRouteClient();
const body = await request.json() as SignInRequest;
const { email, password, name, flow } = body;
if (flow === "signUp") {
// 注册
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name: name ?? "",
},
},
});
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 400 }
);
}
// 注册成功后自动登录
const { error: signInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (signInError) {
return NextResponse.json(
{ error: signInError.message },
{ status: 401 }
);
}
return NextResponse.json(
{ message: "注册并登录成功" },
{ status: 200 }
);
} else {
// 登录
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return NextResponse.json(
{ error: error.message },
{ status: 401 }
);
}
return NextResponse.json(
{ message: "登录成功" },
{ status: 200 }
);
}
} catch (error) {
console.error("Supabase Auth error:", error);
return NextResponse.json(
{ error: "服务器错误" },
{ status: 500 }
);
}
return NextResponse.json({ error: "仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,9 +1,7 @@
import { randomUUID } from "crypto";
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { getBlocksFromDocumentContent, findBlockInTree, withBlocksWrittenBack } from "@/lib/blocks";
@@ -26,16 +24,15 @@ export async function POST(request: Request) {
}
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
const source = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
const source = await client.query(api.documents.getContent, { id: sourceDocumentId });
if (!source) return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 });
const sourceBlocks = getBlocksFromDocumentContent(source.content);
const hit = findBlockInTree(sourceBlocks, blockId);
if (!hit) return NextResponse.json({ error: "源块不存在或无权限" }, { status: 404 });
const target = await client.query(api.documents.getContent, { userId: auth.userId, id: targetDocumentId });
const target = await client.query(api.documents.getContent, { id: targetDocumentId });
if (!target) return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
const targetBlocks = getBlocksFromDocumentContent(target.content);
@@ -53,11 +50,14 @@ export async function POST(request: Request) {
const nextBlocks = [...targetBlocks, referenceBlock];
const payload = withBlocksWrittenBack(target.content, nextBlocks);
await client.mutation(api.documents.updateContent, { userId: auth.userId, id: targetDocumentId, content: payload });
await client.mutation(api.documents.updateContent, { id: targetDocumentId, content: payload });
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -112,4 +112,5 @@ export async function POST(request: Request) {
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ ok: true });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { getBlocksFromDocumentContent, findBlockInTree } from "@/lib/blocks";
@@ -16,9 +14,8 @@ export async function GET(request: Request) {
}
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const doc = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
const { client } = await getAuthedConvexClient();
const doc = await client.query(api.documents.getContent, { id: sourceDocumentId });
if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 });
const blocks = getBlocksFromDocumentContent(doc.content);
const hit = findBlockInTree(blocks, blockId);
@@ -26,6 +23,9 @@ export async function GET(request: Request) {
return NextResponse.json({ ok: true, block: hit.block });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -45,4 +45,5 @@ export async function GET(request: Request) {
if (!hit) return NextResponse.json({ error: "块不存在或无权限" }, { status: 404 });
return NextResponse.json({ ok: true, block: hit.block });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { getBlocksFromDocumentContent, removeBlockSubtree, withBlocksWrittenBack } from "@/lib/blocks";
@@ -26,28 +24,30 @@ export async function POST(request: Request) {
}
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
const source = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
const source = await client.query(api.documents.getContent, { id: sourceDocumentId });
if (!source) return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 });
const sourceBlocks = getBlocksFromDocumentContent(source.content);
const removedRes = removeBlockSubtree(sourceBlocks, blockId);
if (!removedRes.removed) return NextResponse.json({ error: "源块不存在或无权限" }, { status: 404 });
const target = await client.query(api.documents.getContent, { userId: auth.userId, id: targetDocumentId });
const target = await client.query(api.documents.getContent, { id: targetDocumentId });
if (!target) return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
const targetBlocks = getBlocksFromDocumentContent(target.content);
const nextSourcePayload = withBlocksWrittenBack(source.content, removedRes.nextBlocks);
const nextTargetPayload = withBlocksWrittenBack(target.content, [...targetBlocks, removedRes.removed]);
await client.mutation(api.documents.updateContent, { userId: auth.userId, id: sourceDocumentId, content: nextSourcePayload });
await client.mutation(api.documents.updateContent, { userId: auth.userId, id: targetDocumentId, content: nextTargetPayload });
await client.mutation(api.documents.updateContent, { id: sourceDocumentId, content: nextSourcePayload });
await client.mutation(api.documents.updateContent, { id: targetDocumentId, content: nextTargetPayload });
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -96,4 +96,5 @@ export async function POST(request: Request) {
if (tgtErr) return NextResponse.json({ error: tgtErr.message }, { status: 500 });
return NextResponse.json({ ok: true });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { getBlocksFromDocumentContent, replaceBlockInTree, withBlocksWrittenBack } from "@/lib/blocks";
@@ -20,9 +18,8 @@ export async function POST(request: Request) {
}
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const doc = await client.query(api.documents.getContent, { userId: auth.userId, id: sourceDocumentId });
const { client } = await getAuthedConvexClient();
const doc = await client.query(api.documents.getContent, { id: sourceDocumentId });
if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 });
const blocks = getBlocksFromDocumentContent(doc.content);
@@ -30,10 +27,13 @@ export async function POST(request: Request) {
if (!replaced.ok) return NextResponse.json({ error: "块不存在或无权限" }, { status: 404 });
const payload = withBlocksWrittenBack(doc.content, replaced.nextBlocks);
await client.mutation(api.documents.updateContent, { userId: auth.userId, id: sourceDocumentId, content: payload });
await client.mutation(api.documents.updateContent, { id: sourceDocumentId, content: payload });
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -61,4 +61,5 @@ export async function POST(request: Request) {
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ ok: true });
*/
}
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
@@ -12,7 +12,7 @@ type Payload =
export async function POST(request: Request) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const body = (await request.json().catch(() => null)) as Payload | null;
if (!body) {
@@ -1,14 +1,14 @@
import { NextResponse } from "next/server";
import { randomUUID } from "crypto";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const { ms }: { ms?: number } = await request.json().catch(() => ({}));
const id = randomUUID();
@@ -24,7 +24,7 @@ export async function POST(request: Request) {
export async function GET(request: Request) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const url = new URL(request.url);
const id = url.searchParams.get("id") ?? "";
@@ -1,15 +1,12 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { requireAuthContext } from "@/lib/auth/authContext";
export const dynamic = "force-dynamic";
export async function GET(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const url = new URL(request.url);
const documentId = url.searchParams.get("documentId") ?? "";
@@ -17,9 +14,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
const result = await client.query(api.documents.getContent, {
userId: auth.userId,
id: documentId,
});
@@ -30,36 +26,5 @@ export async function GET(request: Request) {
return NextResponse.json({ content: result.content ?? null });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const url = new URL(request.url);
const documentId = url.searchParams.get("documentId") ?? "";
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const { data: document, error } = await supabase
.from("documents")
.select("id,content")
.eq("id", documentId)
.eq("user_id", session.user.id)
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
if (!document) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
return NextResponse.json({ content: document.content ?? null });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import path from "path";
import { promises as fs } from "fs";
@@ -163,8 +161,7 @@ function replaceAssetRefsInContent(content: Json | null, assetMap: Map<string, {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
const payload = (await request.json()) as CopyTreePayload;
if (!payload?.items?.length) {
@@ -180,7 +177,7 @@ export async function POST(request: Request) {
let workspaceId: string | null = null;
if (targetParentId) {
const targetDoc = await client.query(api.documents.getMeta, { userId: auth.userId, id: targetParentId });
const targetDoc = await client.query(api.documents.getMeta, { id: targetParentId });
if (!targetDoc) {
return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
}
@@ -188,7 +185,7 @@ export async function POST(request: Request) {
}
const sourceIds = Array.from(new Set(normalizedItems.map((it) => it.documentId)));
const firstMeta = await client.query(api.documents.getMeta, { userId: auth.userId, id: sourceIds[0] });
const firstMeta = await client.query(api.documents.getMeta, { id: sourceIds[0] });
if (!firstMeta) {
return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 });
}
@@ -204,7 +201,6 @@ export async function POST(request: Request) {
const wid = workspaceId;
const allDocs = await client.query(api.documents.listAllForCopy, {
userId: auth.userId,
workspaceId: wid,
});
@@ -275,7 +271,6 @@ export async function POST(request: Request) {
titleSet.add(newTitle);
await client.mutation(api.documents.create, {
userId: auth.userId,
id: newId,
workspaceId: wid,
parentId,
@@ -286,7 +281,6 @@ export async function POST(request: Request) {
await ensureDocumentScaffold(newId, newTitle);
await client.mutation(api.mindmaps.copyByDocument, {
userId: auth.userId,
sourceDocId: item.old.id,
targetDocId: newId,
});
@@ -298,6 +292,9 @@ export async function POST(request: Request) {
});
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -543,4 +540,5 @@ export async function POST(request: Request) {
return NextResponse.json({
items: insertedDocs.map((d) => ({ oldId: d.oldId, newId: d.newId })),
});
*/
}
@@ -1,9 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { ensureDefaultWorkspace, resolveActiveWorkspaceId } from "@/lib/workspaces";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
function makeId(): string {
@@ -22,8 +19,7 @@ export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { auth, client } = await getAuthedConvexClient();
const { parentId, title, blocks }: CreateChildPayload = await request.json();
@@ -35,7 +31,7 @@ export async function POST(request: Request) {
let accessScope: "private" | "shared" | "public" = "private";
if (parentId) {
const parentDoc = await client.query(api.documents.getMeta, { userId: auth.userId, id: parentId });
const parentDoc = await client.query(api.documents.getMeta, { id: parentId });
if (!parentDoc) {
return NextResponse.json({ error: "父页面不存在" }, { status: 404 });
}
@@ -43,7 +39,6 @@ export async function POST(request: Request) {
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
} else {
const ensured = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
userId: auth.userId,
fallbackName: auth.name ?? auth.email ?? "我的空间",
workspaceIdIfCreate: makeId(),
});
@@ -58,7 +53,6 @@ export async function POST(request: Request) {
const pageId = makeId();
const created = await client.mutation(api.documents.create, {
userId: auth.userId,
id: pageId,
workspaceId,
parentId: parentId ?? null,
@@ -73,6 +67,9 @@ export async function POST(request: Request) {
});
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -159,4 +156,5 @@ export async function POST(request: Request) {
pageId: data.id,
title: data.title ?? resolvedTitle,
});
*/
}
@@ -1,16 +1,13 @@
import { randomUUID } from "crypto";
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { ensureDefaultWorkspace, resolveActiveWorkspaceId } from "@/lib/workspaces";
import type { Json } from "@/types/supabase";
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
import path from "path";
import { promises as fs } from "fs";
import { getDocumentsBaseDir } from "@/lib/server/local-paths";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getAuthedConvexClient } from "@/lib/convex/route";
const documentsBaseDir = getDocumentsBaseDir();
@@ -41,13 +38,11 @@ export async function POST(request: Request) {
}
async function handleCreateRequestConvex(request: Request) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { auth, client } = await getAuthedConvexClient();
const { parentId }: { parentId?: string | null } = await request.json();
const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
userId: auth.userId,
fallbackName: auth.email ?? auth.name ?? "我的空间",
workspaceIdIfCreate: randomUUID(),
});
@@ -58,7 +53,6 @@ async function handleCreateRequestConvex(request: Request) {
if (parentId) {
const parentDoc = await client.query(api.documents.getMeta, {
userId: auth.userId,
id: parentId,
});
@@ -68,7 +62,7 @@ async function handleCreateRequestConvex(request: Request) {
workspaceId = parentDoc.workspace_id;
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
const parentContentRes = await client.query(api.documents.getContent, { userId: auth.userId, id: parentId });
const parentContentRes = await client.query(api.documents.getContent, { id: parentId });
parentContent = (parentContentRes?.content as Json | null) ?? null;
} else {
workspaceId = workspaceBootstrap.activeWorkspaceId || null;
@@ -80,7 +74,6 @@ async function handleCreateRequestConvex(request: Request) {
const id = randomUUID();
const data = await client.mutation(api.documents.create, {
userId: auth.userId,
id,
workspaceId,
parentId: parentId ?? null,
@@ -108,7 +101,6 @@ async function handleCreateRequestConvex(request: Request) {
const payload = composeContentWithBlocks(parentContent, nextBlocks);
await client.mutation(api.documents.updateContent, {
userId: auth.userId,
id: parentId,
content: payload,
});
@@ -118,6 +110,9 @@ async function handleCreateRequestConvex(request: Request) {
}
async function handleCreateRequest(request: Request) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -228,4 +223,5 @@ async function handleCreateRequest(request: Request) {
}
return NextResponse.json(data);
*/
}
@@ -1,24 +1,24 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId } = await request.json();
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const client = getConvexHttpClient();
await client.mutation(api.documents.softDelete, { userId: auth.userId, id: documentId });
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.softDelete, { id: documentId });
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -48,4 +48,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import path from "path";
import { promises as fs } from "fs";
@@ -48,15 +46,14 @@ async function copyMindmapIfExists(sourceId: string, targetId: string) {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
const { documentId }: DuplicatePayload = await request.json();
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const sourceDoc = await client.query(api.documents.getMeta, { userId: auth.userId, id: documentId });
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
@@ -70,7 +67,6 @@ export async function POST(request: Request) {
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const duplicated = await client.mutation(api.documents.duplicate, {
userId: auth.userId,
sourceId: documentId,
newId,
title: duplicatedTitle,
@@ -78,7 +74,6 @@ export async function POST(request: Request) {
await ensureDocumentScaffold(duplicated.id, duplicated.title ?? duplicatedTitle);
await client.mutation(api.mindmaps.copyByDocument, {
userId: auth.userId,
sourceDocId: documentId,
targetDocId: duplicated.id,
});
@@ -91,6 +86,9 @@ export async function POST(request: Request) {
});
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -164,4 +162,5 @@ export async function POST(request: Request) {
await copyMindmapIfExists(sourceDoc.id, duplicated.id);
return NextResponse.json(duplicated);
*/
}
@@ -1,6 +1,5 @@
import { randomUUID } from "crypto";
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { Json } from "@/types/supabase";
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
import { isConvexEnabled } from "@/lib/convex/enabled";
@@ -22,12 +21,12 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const sourceDoc = await client.query(api.documents.getMeta, { userId: auth.userId, id: sourceId });
const sourceDoc = await client.query(api.documents.getMeta, { id: sourceId });
if (!sourceDoc) {
return NextResponse.json({ error: "原始页面不存在或无权限" }, { status: 404 });
}
const targetContent = await client.query(api.documents.getContent, { userId: auth.userId, id: targetId });
const targetContent = await client.query(api.documents.getContent, { id: targetId });
if (!targetContent) {
return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
}
@@ -48,7 +47,6 @@ export async function POST(request: Request) {
const payload: Json = composeContentWithBlocks(targetContent.content, nextBlocks);
await client.mutation(api.documents.updateContent, {
userId: auth.userId,
id: targetId,
content: payload,
});
@@ -56,6 +54,9 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -119,4 +120,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ ok: true });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
@@ -13,16 +11,18 @@ interface EmptyTrashPayload {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { workspaceId }: EmptyTrashPayload = await request.json();
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const client = getConvexHttpClient();
await client.mutation(api.documents.emptyTrashByWorkspace, { userId: auth.userId, workspaceId });
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.emptyTrashByWorkspace, { workspaceId });
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -65,4 +65,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true });
*/
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
interface MovePayload {
@@ -13,12 +11,10 @@ interface MovePayload {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId, parentId = null, position }: MovePayload = await request.json();
const sortOrder = Number.isFinite(position) ? Math.floor(position) : 0;
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.move, {
userId: auth.userId,
id: documentId,
parentId,
sortOrder,
@@ -26,6 +22,9 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -52,4 +51,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ ok: true });
*/
}
@@ -1,38 +1,23 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import type { PageOptionsState } from "@/types/page-options";
import type { Database } from "@/types/supabase";
type OptionsPayload = {
documentId: string;
options: Partial<PageOptionsState>;
};
const COLUMN_MAP: Record<keyof PageOptionsState, keyof Database["public"]["Tables"]["documents"]["Row"]> = {
wideLayout: "wide_layout",
smallText: "use_small_text",
showHeadingNumbers: "show_heading_numbers",
showToc: "show_toc",
showStructure: "show_structure",
protectEditing: "protect_editing",
showWordCount: "show_word_count",
};
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId, options }: OptionsPayload = await request.json();
if (!documentId || !options) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.updateOptions, {
userId: auth.userId,
id: documentId,
options: {
wideLayout: options.wideLayout,
@@ -48,43 +33,5 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { documentId, options }: OptionsPayload = await request.json();
if (!documentId || !options) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const updatePayload: Record<string, boolean> = {};
(Object.keys(options) as (keyof PageOptionsState)[]).forEach((key) => {
const column = COLUMN_MAP[key];
if (!column) return;
const value = options[key];
if (typeof value === "boolean") {
updatePayload[column as string] = value;
}
});
if (Object.keys(updatePayload).length === 0) {
return NextResponse.json({ error: "缺少可更新的选项" }, { status: 400 });
}
const { error } = await supabase
.from("documents")
.update(updatePayload)
.eq("id", documentId)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,24 +1,24 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId } = await request.json();
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const client = getConvexHttpClient();
await client.mutation(api.documents.purge, { userId: auth.userId, id: documentId });
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.purge, { id: documentId });
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -45,4 +45,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true });
*/
}
@@ -1,24 +1,24 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId } = await request.json();
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
const client = getConvexHttpClient();
await client.mutation(api.documents.restore, { userId: auth.userId, id: documentId });
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.restore, { id: documentId });
return NextResponse.json({ success: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -50,4 +50,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true });
*/
}
@@ -1,9 +1,7 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { requireAuthContext } from "@/lib/auth/authContext";
interface SavePayload {
documentId: string;
@@ -12,37 +10,14 @@ interface SavePayload {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId, content }: SavePayload = await request.json();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.updateContent, {
userId: auth.userId,
id: documentId,
content,
});
return NextResponse.json({ ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { documentId, content }: SavePayload = await request.json();
const { error } = await supabase
.from("documents")
.update({ content })
.eq("id", documentId)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import type { DocumentStats } from "@/types/page-options";
@@ -13,15 +11,13 @@ interface StatsPayload {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId, stats }: StatsPayload = await request.json();
if (!documentId || !stats) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.updateStats, {
userId: auth.userId,
id: documentId,
wordCount: stats.wordCount,
characterCount: stats.characterCount,
@@ -31,33 +27,5 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { documentId, stats }: StatsPayload = await request.json();
if (!documentId || !stats) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const { error } = await supabase
.from("documents")
.update({
word_count: stats.wordCount,
character_count: stats.characterCount,
block_count: stats.blockCount,
})
.eq("id", documentId)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,9 +1,7 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { requireAuthContext } from "@/lib/auth/authContext";
interface RenamePayload {
documentId: string;
@@ -12,37 +10,14 @@ interface RenamePayload {
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { documentId, title }: RenamePayload = await request.json();
const client = getConvexHttpClient();
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.updateTitle, {
userId: auth.userId,
id: documentId,
title,
});
return NextResponse.json({ ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { documentId, title }: RenamePayload = await request.json();
const { error } = await supabase
.from("documents")
.update({ title })
.eq("id", documentId)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,9 +1,8 @@
"use server";
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
export async function GET(request: Request) {
@@ -17,7 +16,7 @@ export async function GET(request: Request) {
// Convex 模式
if (isConvexEnabled()) {
try {
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const result = await client.query(api.tables.getByGridKey, { gridKey });
if (!result) {
@@ -36,6 +35,9 @@ export async function GET(request: Request) {
}
// Supabase 模式
return NextResponse.json({ code: 501, msg: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const { data, error } = await supabase
.from("document_tables")
@@ -56,4 +58,5 @@ export async function GET(request: Request) {
gridKey: data.grid_key,
},
});
*/
}
@@ -2,10 +2,9 @@
import { NextResponse } from "next/server";
import { createDefaultTableSnapshot } from "@/lib/online-table";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { DocumentTableSnapshot, TableSchema } from "@/types/online-table";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
const fallbackSchema: TableSchema = {
@@ -25,7 +24,7 @@ export async function POST(request: Request) {
// Convex 模式
if (isConvexEnabled()) {
try {
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const tableData = await client.query(api.tables.getByGridKeyFull, { gridKey });
if (!tableData) {
@@ -54,6 +53,9 @@ export async function POST(request: Request) {
}
// Supabase 模式
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const { data, error } = await supabase
.from("document_tables")
@@ -80,4 +82,5 @@ export async function POST(request: Request) {
return new NextResponse(body, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
*/
}
@@ -3,12 +3,14 @@
import { Buffer } from "node:buffer";
import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
const BUCKET = "media";
const DIRECTORY = "luckysheet";
export async function POST(request: Request) {
return NextResponse.json({ code: 501, msg: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const formData = await request.formData();
const file = formData.get("image");
@@ -39,4 +41,5 @@ export async function POST(request: Request) {
msg: "ok",
url: publicUrl,
});
*/
}
@@ -1,10 +1,9 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { MediaAsset } from "@/types/media";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
@@ -29,7 +28,7 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const items = await client.query(api.mediaAssets.listByWorkspace, {
userId: auth.userId,
workspaceId,
@@ -41,7 +40,9 @@ export async function GET(request: Request) {
return NextResponse.json({ items: (items ?? []) as MediaAsset[] });
}
const supabase = await createSupabaseRouteClient();
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
const supabase = null as any;
const {
data: { user },
error: authError,
@@ -130,7 +131,7 @@ export async function POST(request: Request) {
updated_at: now,
};
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
await client.mutation(api.mediaAssets.create, {
userId: auth.userId,
asset: {
@@ -151,7 +152,9 @@ export async function POST(request: Request) {
return NextResponse.json({ asset });
}
const supabase = await createSupabaseRouteClient();
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
const supabase = null as any;
const {
data: { user },
error: authError,
@@ -1,12 +1,11 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { makeUniqueFileName } from "@/lib/file-tree/naming";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
@@ -84,7 +83,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const assets = (await client.query(api.mediaAssets.listByIds, {
userId: auth.userId,
ids: payload.assetIds,
@@ -142,7 +141,6 @@ export async function POST(request: Request) {
}
const targetDoc = await client.query(api.documents.getMeta, {
userId: auth.userId,
id: payload.targetDocumentId,
});
@@ -213,6 +211,9 @@ export async function POST(request: Request) {
}
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -410,4 +411,5 @@ export async function POST(request: Request) {
const message = err instanceof Error ? err.message : "操作失败";
return NextResponse.json({ error: message }, { status: 500 });
}
*/
}
@@ -1,9 +1,8 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
@@ -45,7 +44,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const res = await client.mutation(api.mediaAssets.emptyTrashByWorkspace, {
userId: auth.userId,
workspaceId,
@@ -55,6 +54,9 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true, ...(res as any) });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -114,4 +116,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true, updated: assetIds.length });
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { isConvexEnabled } from "@/lib/convex/enabled";
@@ -9,6 +8,9 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Convex 模式暂不支持 OCR" }, { status: 501 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -51,4 +53,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ ok: true });
*/
}
@@ -1,9 +1,8 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
@@ -45,7 +44,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
}
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
const res = await client.mutation(api.mediaAssets.purgeById, {
userId: auth.userId,
id: assetId,
@@ -55,6 +54,9 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true, ...(res as any) });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -115,4 +117,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ success: true });
*/
}
+7 -159
View File
@@ -1,74 +1,19 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import supabaseAdmin from "@/lib/supabase/admin";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { HttpError } from "@/lib/auth/authContext";
import { getAuthedConvexClient } from "@/lib/convex/route";
export const dynamic = "force-dynamic";
const isUuid = (value: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
const resolveAssetObjectLocation = async (params: {
bucket?: string | null;
storagePath?: string | null;
workspaceId?: string | null;
fileName?: string | null;
}) => {
const { bucket, storagePath, workspaceId, fileName } = params;
const ws = (workspaceId ?? "").trim();
const fn = (fileName ?? "").trim();
if (!ws || !isUuid(ws) || !fn) return null;
try {
const storage = supabaseAdmin.schema("storage");
// 1) 若已给出 bucket/path,先校验是否存在
const b = (bucket ?? "").trim();
const p = (storagePath ?? "").trim();
if (b && p) {
const { data: exact, error: exactError } = await storage
.from("objects")
.select("bucket_id,name")
.eq("bucket_id", b)
.eq("name", p)
.limit(1);
if (!exactError && exact && exact.length > 0) {
return { bucket: b, path: p };
}
}
// 2) 兼容历史数据:按 workspace_id/%/file_name 查找
const pattern = `${ws}/%/${fn}`;
const { data: candidates, error: candError } = await storage
.from("objects")
.select("bucket_id,name,created_at")
.like("name", pattern)
.order("created_at", { ascending: false })
.limit(20);
if (candError || !candidates || candidates.length === 0) return null;
const exactCandidate =
candidates.find((o) => String(o?.name || "").endsWith(`/${fn}`)) ??
candidates[0];
if (!exactCandidate?.bucket_id || !exactCandidate?.name) return null;
return { bucket: String(exactCandidate.bucket_id), path: String(exactCandidate.name) };
} catch {
return null;
}
};
export async function GET(request: Request) {
if (isConvexEnabled()) {
let auth;
let client;
try {
auth = await requireAuthContext();
const authed = await getAuthedConvexClient();
auth = authed.auth;
client = authed.client;
} catch (err) {
if (err instanceof HttpError) {
return NextResponse.json({ error: "未登录" }, { status: err.status });
@@ -83,7 +28,6 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
}
const client = getConvexHttpClient();
const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId });
if (!asset) {
return NextResponse.json({ error: "资源不存在" }, { status: 404 });
@@ -106,101 +50,5 @@ export async function GET(request: Request) {
});
}
const supabase = await createSupabaseRouteClient();
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const assetId = searchParams.get("assetId");
if (!assetId) {
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
}
// 获取资源信息
const { data: asset, error: assetError } = await supabase
.from("media_assets")
.select("*")
.eq("id", assetId)
.single();
if (assetError || !asset) {
return NextResponse.json({ error: "资源不存在" }, { status: 404 });
}
// 兼容:早期/异常写入的 media_assets 可能缺少 bucket/storage_path
// 导致生成的签名链接指向不存在的对象(ONLYOFFICE 会报“下载失败”)。
let bucket = (asset.bucket as string | null) ?? null;
let storagePath = (asset.storage_path as string | null) ?? null;
if (!bucket || !storagePath) {
const resolved = await resolveAssetObjectLocation({
bucket,
storagePath,
workspaceId: asset.workspace_id ?? null,
fileName: asset.file_name ?? null,
});
if (!resolved) {
return NextResponse.json({ error: "未找到存储对象,无法生成签名链接" }, { status: 404 });
}
bucket = resolved.bucket;
storagePath = resolved.path;
// 尽量回填,避免后续重复修复
await supabase
.from("media_assets")
.update({ bucket, storage_path: storagePath })
.eq("id", asset.id);
}
// 对于图片,添加高质量参数以获得更好的显示效果
// Supabase Storage 支持通过 URL 参数控制图片质量和尺寸
const isImage = asset.mime_type?.startsWith("image/");
let signedUrl: string;
if (isImage) {
// 使用 createSignedUrl 并添加高质量参数
// 注意:transform 参数需要在签名时指定
const { data: signedUrlData, error: signError } = await supabase.storage
.from(bucket)
.createSignedUrl(storagePath, 60 * 60, {
// 添加转换参数以获得高质量图片
transform: {
quality: 95, // 高质量
format: "origin", // 保持原始格式
},
});
if (signError || !signedUrlData) {
return NextResponse.json({ error: "生成签名 URL 失败" }, { status: 500 });
}
signedUrl = signedUrlData.signedUrl;
} else {
// 非图片文件,直接生成签名 URL
const { data: signedUrlData, error: signError } = await supabase.storage
.from(bucket)
.createSignedUrl(storagePath, 60 * 60);
if (signError || !signedUrlData) {
return NextResponse.json({ error: "生成签名 URL 失败" }, { status: 500 });
}
signedUrl = signedUrlData.signedUrl;
}
signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl);
return NextResponse.json({
signedUrl,
asset: {
id: asset.id,
file_name: asset.file_name,
mime_type: asset.mime_type,
file_size: asset.file_size,
},
});
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,102 +1,11 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import supabaseAdmin from "@/lib/supabase/admin";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
export const dynamic = "force-dynamic";
const base64Url = (input: string) =>
Buffer.from(input)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const parseStoragePath = (fileUrl: string) => {
try {
const url = new URL(fileUrl);
const segments = url.pathname.split("/").filter(Boolean);
const objectIdx = segments.findIndex((seg) => seg === "object");
if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
// pattern 1: /storage/v1/object/public/<bucket>/<path...>
if (segments[objectIdx + 1] === "public") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
// pattern 2: /storage/v1/object/sign/<bucket>/<path...> (token in query)
if (segments[objectIdx + 1] === "sign") {
const bucket = segments[objectIdx + 2];
const path = segments.slice(objectIdx + 3).join("/");
return { bucket, path };
}
return null;
} catch {
return null;
}
};
const isUuid = (value: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
const tryResolveExistingObjectPath = async (params: {
bucket: string;
path: string;
fileName?: string;
}) => {
const { bucket, path, fileName } = params;
if (!bucket || !path) return null;
try {
const storage = supabaseAdmin.schema("storage");
// 1) 先尝试精确匹配(最快且最准确)
const { data: exact, error: exactError } = await storage
.from("objects")
.select("name")
.eq("bucket_id", bucket)
.eq("name", path)
.limit(1);
if (!exactError && exact && exact.length > 0 && exact[0]?.name) {
return String(exact[0].name);
}
// 2) 兼容历史数据:老链接中间 UUID 可能用的是 document_id
// 但实际对象通常是 workspace_id/<其他uuid>/<file_name>。
const segments = path.split("/").filter(Boolean);
const workspaceId = segments[0] ?? "";
const wantedName = (fileName ?? segments[segments.length - 1] ?? "").trim();
if (!workspaceId || !isUuid(workspaceId) || !wantedName) return null;
const pattern = `${workspaceId}/%/${wantedName}`;
const { data: candidates, error: candError } = await storage
.from("objects")
.select("name,created_at")
.eq("bucket_id", bucket)
.like("name", pattern)
.order("created_at", { ascending: false })
.limit(20);
if (candError || !candidates || candidates.length === 0) return null;
// 说明:LIKE 会把 `_` 当作通配符,这里用 endsWith 再做一次精确过滤。
const exactCandidate =
candidates.find((o) => String(o?.name || "").endsWith(`/${wantedName}`)) ??
candidates[0];
if (!exactCandidate?.name) return null;
return String(exactCandidate.name);
} catch {
return null;
}
};
export async function GET(request: Request) {
if (isConvexEnabled()) {
// 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。
// 由于外链本身已经是可访问的 URL,这里只做最小透传。
try {
await requireAuthContext();
} catch (err) {
@@ -111,105 +20,12 @@ export async function GET(request: Request) {
if (!fileUrl) {
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
}
// 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。
// 由于外链本身已经是可访问的 URL,这里只做最小透传。
return NextResponse.json({ signedUrl: fileUrl });
}
// 说明:在 Cloudflare Tunnel 场景下,后端收到的 Host 可能是 localhost
// 但 ONLYOFFICE 文档服务器拉取 document.url 时必须使用公网可达的域名。
// 因此这里优先使用运行时配置(public/mnote-env.json / env)里的公网 Origin
// 再回退到 request.url 解析出的 origin。
const runtimeCfg = getMnoteRuntimeConfig();
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const fileUrl = searchParams.get("fileUrl");
const fileName = searchParams.get("fileName") ?? undefined;
const forOnlyOffice = searchParams.get("for") === "onlyoffice";
if (searchParams.get("debug") === "1") {
return NextResponse.json({
keyLen: (process.env.SUPABASE_SERVICE_ROLE_KEY || "").length,
url: process.env.SUPABASE_URL,
});
}
if (!fileUrl) {
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
}
// 说明:
// - 非 ONLYOFFICE:必须能解析为 Supabase Storage 路径,便于生成带 download 的临时签名 URL。
// - ONLYOFFICE:允许传入任意可访问 URL(包括外链或已签名 URL),并尽量“刷新一次签名”,刷新失败则回退到原 URL。
const parsed = parseStoragePath(fileUrl);
if (!forOnlyOffice) {
if (!parsed) {
return NextResponse.json({ error: "无法解析 Supabase 存储路径" }, { status: 400 });
}
const { bucket, path } = parsed;
const resolvedPath =
(await tryResolveExistingObjectPath({ bucket, path, fileName })) ?? null;
if (!resolvedPath) {
return NextResponse.json(
{ error: "存储对象不存在,无法生成签名链接" },
{ status: 404 },
);
}
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(
resolvedPath,
60 * 60,
{ download: fileName },
);
if (error || !data?.signedUrl) {
return NextResponse.json({ error: error?.message ?? "生成签名 URL 失败" }, { status: 500 });
}
// 返回给浏览器的 URL 必须是公网可达的(不能是 127.0.0.1:18000
const signedUrl = rewriteToPublicOrigin(data.signedUrl, runtimeCfg.supabaseUrl);
return NextResponse.json({ signedUrl });
}
// ONLYOFFICE 需要“可被文档服务器拉取”的 URL:不要强制 downloadContent-Disposition: attachment)。
// 这里优先尝试重新生成 1 小时签名 URL;如果失败(例如策略限制/路径无法解析),则回退到原 URL。
let upstreamUrl = rewriteToPublicOrigin(fileUrl, runtimeCfg.supabaseUrl);
if (parsed) {
const { bucket, path } = parsed;
const resolvedPath =
(await tryResolveExistingObjectPath({ bucket, path, fileName })) ?? null;
if (!resolvedPath) {
return NextResponse.json(
{ error: "存储对象不存在,无法生成签名链接" },
{ status: 404 },
);
}
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(
resolvedPath,
60 * 60,
);
if (!error && data?.signedUrl) {
upstreamUrl = rewriteToPublicOrigin(data.signedUrl, runtimeCfg.supabaseUrl);
}
}
// 关键:Supabase Storage 的 signedUrl 自带 `?token=...`ONLYOFFICE 会把 URL 上的
// `token` 参数当作自己的 JWT token 去校验,导致报 “文档安全令牌的格式不正确/invalid signature”。
//
// 解决:对 ONLYOFFICE 返回一个“代理 URL”,把真正的 URL 编码到 `u=...` 中,避免
// `token` 参数出现在 document.url 上。
const defaultOrigin = new URL(request.url).origin;
const proxyBase = (runtimeCfg.onlyofficeProxyOrigin || runtimeCfg.cloudflareAppOrigin || defaultOrigin).replace(/\/+$/, "");
const proxyUrl = new URL("/api/onlyoffice/proxy", proxyBase);
proxyUrl.searchParams.set("u", base64Url(upstreamUrl));
return NextResponse.json({ signedUrl: proxyUrl.toString() });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { MediaAsset } from "@/types/media";
import { extname } from "path";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
@@ -7,7 +6,7 @@ import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
@@ -50,7 +49,7 @@ export async function POST(request: Request) {
? crypto.randomUUID()
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const assetType = resolveAssetType(file.type || "");
const client = getConvexHttpClient();
const client = await getConvexAuthedHttpClient();
// 1) 获取 Convex 的上传 URL(短时有效)
const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId: auth.userId });
@@ -101,6 +100,9 @@ export async function POST(request: Request) {
}
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -190,4 +192,5 @@ export async function POST(request: Request) {
console.error(error);
return NextResponse.json({ error: "上传失败" }, { status: 500 });
}
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig";
import { loadLocalAiConfig } from "@/lib/ai/localAiConfig";
import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat";
@@ -313,6 +312,9 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少 documentId/mindmapId/messages" }, { status: 400 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -833,4 +835,5 @@ export async function POST(request: Request) {
},
{ status: 200 },
);
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { detectLocalMindmapFiles } from "@/lib/server/mindmap-files";
import type { MediaAsset } from "@/types/media";
import path from "path";
@@ -40,6 +39,9 @@ async function listTestPdfs(): Promise<AgentAssetItem[]> {
}
export async function GET(request: Request) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -115,4 +117,5 @@ export async function GET(request: Request) {
documentId,
items,
});
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig";
import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat";
import { applyMindmapOps, type MindmapOp, type MindmapTreeNode, type NodeRef } from "@/lib/mindmap/mindmapOps";
@@ -93,6 +92,9 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "缺少 documentId/mindmapId/targetUid" }, { status: 400 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -308,4 +310,5 @@ export async function POST(request: Request) {
searxCount: searxResults.length,
},
});
*/
}
@@ -1,101 +1,30 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { promises as fs } from "fs";
import path from "path";
import { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from "@/lib/server/local-paths";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
interface EmptyTrashPayload {
workspaceId?: string;
}
async function purgeTrashFolder(folder: string): Promise<number> {
const trashDir = path.join(folder, ".trash");
try {
const entries = await fs.readdir(trashDir);
let removed = 0;
for (const name of entries) {
await fs.rm(path.join(trashDir, name), { force: true, recursive: true });
removed += 1;
}
return removed;
} catch {
return 0;
}
}
export async function POST(request: Request) {
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const { client } = await getAuthedConvexClient();
const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({}));
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
try {
const result = await client.mutation(api.mindmaps.emptyTrashByWorkspace, {
userId: auth.userId,
workspaceId,
});
const result = await client.mutation(api.mindmaps.emptyTrashByWorkspace, { workspaceId });
return NextResponse.json({ ok: true, removed: result?.deletedCount ?? 0 });
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
}
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({}));
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const { data: membership, error: membershipError } = await supabase
.from("workspace_members")
.select("workspace_id")
.eq("workspace_id", workspaceId)
.eq("user_id", session.user.id)
.limit(1);
if (membershipError) {
return NextResponse.json({ error: membershipError.message }, { status: 500 });
}
if (!membership || membership.length === 0) {
return NextResponse.json({ error: "无权操作该工作空间" }, { status: 403 });
}
const { data: documents, error: docError } = await supabase
.from("documents")
.select("id")
.eq("workspace_id", workspaceId)
.eq("user_id", session.user.id)
.limit(5000);
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 500 });
}
const docIds = (documents ?? []).map((row) => row.id).filter(Boolean);
let removed = 0;
for (const docId of docIds) {
removed += await purgeTrashFolder(path.join(preferredBaseDir, docId));
removed += await purgeTrashFolder(path.join(legacyBaseDir, docId));
}
return NextResponse.json({ ok: true, removed });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { applyMindmapOps, type MindmapOp } from "@/lib/mindmap/mindmapOps";
import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -24,7 +22,7 @@ export async function POST(
const { docId, mindmapId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const { client } = await getAuthedConvexClient();
const payload = (await request.json().catch(() => null)) as RequestPayload | null;
const ops = Array.isArray(payload?.ops) ? payload!.ops : [];
@@ -32,21 +30,15 @@ export async function POST(
return NextResponse.json({ error: "缺少 ops" }, { status: 400 });
}
if (ops.length > 80) {
return NextResponse.json({ error: "ops 过多(最 80" }, { status: 400 });
return NextResponse.json({ error: "ops 过多(最 80" }, { status: 400 });
}
try {
const current = await client.query(api.mindmaps.get, {
userId: auth.userId,
docId,
mindmapId,
});
const current = await client.query(api.mindmaps.get, { docId, mindmapId });
const baseData = current?.data ?? defaultMindmapData;
const { data: nextData, applied, errors } = applyMindmapOps(baseData, ops);
await client.mutation(api.mindmaps.put, {
userId: auth.userId,
docId,
mindmapId,
data: nextData,
@@ -69,54 +61,6 @@ export async function POST(
}
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
// 校验页面归属
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id,title,mindmap_data")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const payload = (await request.json().catch(() => null)) as RequestPayload | null;
const ops = Array.isArray(payload?.ops) ? payload!.ops : [];
if (!ops.length) {
return NextResponse.json({ error: "缺少 ops" }, { status: 400 });
}
if (ops.length > 80) {
return NextResponse.json({ error: "ops 过多(最多 80" }, { status: 400 });
}
const local = await readMindmapLocal(docId, mindmapId);
const baseData = local.ok ? local.data : (doc.mindmap_data ?? defaultMindmapData);
const { data: nextData, applied, errors } = applyMindmapOps(baseData, ops);
await writeMindmapLocal(docId, mindmapId, nextData, doc.title ?? "无标题");
return NextResponse.json({
ok: true,
applied,
errors,
data: nextData,
meta: {
documentId: docId,
mindmapId,
actor: payload?.actor ?? null,
reason: payload?.reason ?? null,
},
});
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,8 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { promises as fs } from "fs";
import path from "path";
import { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from "@/lib/server/local-paths";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -12,104 +8,6 @@ const defaultMindmapData = {
children: [],
};
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
async function ensureDir(dir: string) {
await fs.mkdir(dir, { recursive: true });
}
async function removeFileSafe(file: string) {
try {
await fs.rm(file, { force: true });
} catch {
// ignore
}
}
async function ensureIndexFile(folder: string, title = "无标题") {
const indexFile = path.join(folder, "index.md");
try {
await fs.access(indexFile);
} catch {
await fs.writeFile(indexFile, `# ${title}\n`, "utf8");
}
}
function resolveMindmapFileName(mindmapId: string) {
if (mindmapId === "legacy" || mindmapId.startsWith("legacy-")) {
return "mindmap.json";
}
return `mindmap-${mindmapId}.json`;
}
async function tryReadJson(file: string) {
try {
const content = await fs.readFile(file, "utf8");
return JSON.parse(content);
} catch {
return null;
}
}
type TrashedMindmapMeta = {
docId: string;
mindmapId: string;
originalFileName: string;
originalPath: string;
trashedFileName: string;
deleted_at: string;
};
async function fileExists(file: string) {
try {
await fs.access(file);
return true;
} catch {
return false;
}
}
async function moveToTrash(file: string, meta: Omit<TrashedMindmapMeta, "trashedFileName">) {
if (!(await fileExists(file))) {
return null;
}
const dir = path.dirname(file);
const trashDir = path.join(dir, ".trash");
await ensureDir(trashDir);
const originalFileName = path.basename(file);
const trashedFileName = `${originalFileName}.${Date.now()}.deleted`;
const trashedPath = path.join(trashDir, trashedFileName);
const metaPath = path.join(trashDir, `${trashedFileName}.json`);
await fs.rename(file, trashedPath);
await fs.writeFile(
metaPath,
JSON.stringify({ ...meta, originalFileName, trashedFileName }, null, 2),
"utf8",
);
return { trashedPath, metaPath };
}
async function listTrashMetas(folder: string): Promise<Array<{ meta: TrashedMindmapMeta; metaPath: string; trashedPath: string }>> {
const trashDir = path.join(folder, ".trash");
try {
const entries = await fs.readdir(trashDir);
const results: Array<{ meta: TrashedMindmapMeta; metaPath: string; trashedPath: string }> = [];
for (const name of entries) {
if (!name.endsWith(".deleted.json")) continue;
const metaPath = path.join(trashDir, name);
const metaRaw = await tryReadJson(metaPath);
const meta = metaRaw as TrashedMindmapMeta | null;
if (!meta?.docId || !meta?.mindmapId || !meta?.trashedFileName || !meta?.originalPath) continue;
const trashedPath = path.join(trashDir, meta.trashedFileName);
results.push({ meta, metaPath, trashedPath });
}
return results;
} catch {
return [];
}
}
export async function GET(
_req: Request,
{ params }: { params: Promise<{ docId: string; mindmapId: string }> },
@@ -117,51 +15,12 @@ export async function GET(
const { docId, mindmapId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const res = await client.query(api.mindmaps.get, {
userId: auth.userId,
docId,
mindmapId,
});
const { client } = await getAuthedConvexClient();
const res = await client.query(api.mindmaps.get, { docId, mindmapId });
return NextResponse.json({ data: res?.data ?? defaultMindmapData, source: "convex" });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const folder = path.join(preferredBaseDir, docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
const legacyFile = path.join(folder, "mindmap.json");
const legacyDirFile =
path.basename(file) === "mindmap.json"
? path.join(legacyBaseDir, docId, "mindmap.json")
: null;
const localData =
(await tryReadJson(file)) ??
(await tryReadJson(legacyFile)) ??
(legacyDirFile ? await tryReadJson(legacyDirFile) : null);
if (localData) {
return NextResponse.json({ data: localData, source: "local" });
}
// 兼容旧版:没有本地文件时,回退到 documents.mindmap_data(仅能表示单个旧导图)
const { data, error } = await supabase
.from("documents")
.select("mindmap_data")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
const payload = data?.mindmap_data ?? defaultMindmapData;
return NextResponse.json({ data: payload, source: "supabase" });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function POST(
@@ -171,7 +30,7 @@ export async function POST(
const { docId, mindmapId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const { client } = await getAuthedConvexClient();
const { data, createOnly } = (await request.json().catch(() => ({ data: null }))) as {
data?: unknown;
createOnly?: boolean;
@@ -179,59 +38,18 @@ export async function POST(
try {
const result = await client.mutation(api.mindmaps.put, {
userId: auth.userId,
docId,
mindmapId,
data: data ?? defaultMindmapData,
...(typeof createOnly === "boolean" ? { createOnly } : {}),
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
return NextResponse.json(result ?? { ok: true });
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
}
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
// 校验页面归属(避免任意写入)
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id,title")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const { data, createOnly } = (await request.json().catch(() => ({ data: null }))) as {
data?: unknown;
createOnly?: boolean;
};
const folder = path.join(preferredBaseDir, docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
try {
await ensureDir(folder);
await ensureIndexFile(folder, doc.title ?? "无标题");
// 仅创建:避免“初始化写入”覆盖用户/AI 刚保存的内容(典型于快速操作 + 异步时序)
if (createOnly && (await fileExists(file))) {
return NextResponse.json({ ok: true, created: false, skipped: true });
}
await fs.writeFile(file, JSON.stringify(data ?? defaultMindmapData, null, 2), "utf8");
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
return NextResponse.json({ ok: true, created: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function DELETE(
@@ -241,72 +59,16 @@ export async function DELETE(
const { docId, mindmapId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const { client } = await getAuthedConvexClient();
try {
const result = await client.mutation(api.mindmaps.softDelete, {
userId: auth.userId,
docId,
mindmapId,
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
const result = await client.mutation(api.mindmaps.softDelete, { docId, mindmapId });
return NextResponse.json(result ?? { ok: true });
} catch (error) {
return NextResponse.json({ error: (error as Error).message }, { status: 400 });
}
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
// 校验页面归属
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const deletedAt = new Date().toISOString();
const fileName = resolveMindmapFileName(mindmapId);
const preferredFolder = path.join(preferredBaseDir, docId);
const preferredFile = path.join(preferredFolder, fileName);
const candidates: string[] = [preferredFile];
if (fileName === "mindmap.json") {
candidates.push(path.join(legacyBaseDir, docId, "mindmap.json"));
}
let moved = 0;
for (const file of candidates) {
const movedInfo = await moveToTrash(file, {
docId,
mindmapId,
originalFileName: path.basename(file),
originalPath: file,
deleted_at: deletedAt,
});
if (movedInfo) {
moved += 1;
}
}
if (moved === 0) {
// 兼容:文件不存在也视为成功(避免前端卡死)
return NextResponse.json({ ok: true, moved: 0 });
}
return NextResponse.json({ ok: true, moved });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function PATCH(
@@ -316,7 +78,7 @@ export async function PATCH(
const { docId, mindmapId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const { client } = await getAuthedConvexClient();
const { action } = (await request.json().catch(() => ({}))) as { action?: string };
if (action !== "restore" && action !== "purge") {
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
@@ -324,20 +86,11 @@ export async function PATCH(
try {
if (action === "purge") {
const result = await client.mutation(api.mindmaps.purge, {
userId: auth.userId,
docId,
mindmapId,
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
const result = await client.mutation(api.mindmaps.purge, { docId, mindmapId });
return NextResponse.json(result ?? { ok: true });
}
const result = await client.mutation(api.mindmaps.restore, {
userId: auth.userId,
docId,
mindmapId,
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
const result = await client.mutation(api.mindmaps.restore, { docId, mindmapId });
return NextResponse.json(result ?? { ok: true });
} catch (error) {
const msg = (error as Error).message ?? "操作失败";
const status = msg.includes("未找到") ? 404 : 400;
@@ -345,68 +98,6 @@ export async function PATCH(
}
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { action } = (await request.json().catch(() => ({}))) as { action?: string };
if (action !== "restore" && action !== "purge") {
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
}
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const preferredFolder = path.join(preferredBaseDir, docId);
const legacyFolder = path.join(legacyBaseDir, docId);
const entries = [
...(await listTrashMetas(preferredFolder)),
...(await listTrashMetas(legacyFolder)),
].filter((item) => item.meta.mindmapId === mindmapId && item.meta.docId === docId);
if (entries.length === 0) {
return NextResponse.json({ error: "未找到可操作的垃圾桶记录" }, { status: 404 });
}
if (action === "purge") {
let purged = 0;
for (const item of entries) {
await removeFileSafe(item.trashedPath);
await removeFileSafe(item.metaPath);
purged += 1;
}
return NextResponse.json({ ok: true, purged });
}
// restore:恢复最新的一条
const latest = entries
.slice()
.sort((a, b) => (b.meta.deleted_at ?? "").localeCompare(a.meta.deleted_at ?? ""))[0];
if (!latest?.meta?.originalPath) {
return NextResponse.json({ error: "垃圾桶记录损坏" }, { status: 500 });
}
if (await fileExists(latest.meta.originalPath)) {
return NextResponse.json({ error: "目标文件已存在,无法恢复" }, { status: 409 });
}
await ensureDir(path.dirname(latest.meta.originalPath));
await fs.rename(latest.trashedPath, latest.meta.originalPath);
await removeFileSafe(latest.metaPath);
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,8 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { promises as fs } from "fs";
import path from "path";
import { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from "@/lib/server/local-paths";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -12,189 +8,56 @@ const defaultMindmapData = {
children: [],
};
// 新版:与页面文件夹(index.md 所在处)对齐,放在 public/documents/<docId>/mindmap.json
// 旧版遗留:public/mindmaps/<docId>/mindmap.json
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
async function ensureDir(dir: string) {
await fs.mkdir(dir, { recursive: true });
}
async function removeFileSafe(file: string) {
try {
await fs.rm(file, { force: true });
} catch {
// 忽略删除失败(例如文件不存在)
}
}
async function ensureIndexFile(folder: string, title = "无标题") {
const indexFile = path.join(folder, "index.md");
try {
await fs.access(indexFile);
} catch {
await fs.writeFile(indexFile, `# ${title}\n`, "utf8");
}
}
export async function GET(
_req: Request,
{ params }: { params: Promise<{ docId: string }> },
) {
const { docId: id } = await params;
const { docId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const mindmapId = `legacy-${id}`;
const res = await client.query(api.mindmaps.get, {
userId: auth.userId,
docId: id,
mindmapId,
});
const { client } = await getAuthedConvexClient();
const mindmapId = `legacy-${docId}`;
const res = await client.query(api.mindmaps.get, { docId, mindmapId });
return NextResponse.json({ data: res?.data ?? defaultMindmapData, source: "convex" });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const preferredFolder = path.join(preferredBaseDir, id);
const preferredFile = path.join(preferredFolder, "mindmap.json");
const legacyFolder = path.join(legacyBaseDir, id);
const legacyFile = path.join(legacyFolder, "mindmap.json");
const tryRead = async (file: string) => {
try {
const content = await fs.readFile(file, "utf8");
return JSON.parse(content);
} catch {
return null;
}
};
const localData =
(await tryRead(preferredFile)) ??
(await tryRead(legacyFile));
if (localData) {
return NextResponse.json({ data: localData, source: "local" });
}
// fallback Supabase
const { data, error } = await supabase
.from("documents")
.select("mindmap_data")
.eq("id", id)
.eq("user_id", session.user.id)
.maybeSingle();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
const payload = data?.mindmap_data ?? defaultMindmapData;
return NextResponse.json({ data: payload, source: "supabase" });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ docId: string }> },
) {
const { docId: id } = await params;
const { docId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const mindmapId = `legacy-${id}`;
const { data } = await request.json();
const { client } = await getAuthedConvexClient();
const mindmapId = `legacy-${docId}`;
const payload = (await request.json().catch(() => ({}))) as { data?: unknown };
const result = await client.mutation(api.mindmaps.put, {
userId: auth.userId,
docId: id,
docId,
mindmapId,
data: data ?? defaultMindmapData,
data: payload.data ?? defaultMindmapData,
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
return NextResponse.json(result ?? { ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { data } = await request.json();
const folder = path.join(preferredBaseDir, id);
const file = path.join(folder, "mindmap.json");
try {
await ensureDir(folder);
await ensureIndexFile(folder);
await fs.writeFile(
file,
JSON.stringify(data ?? defaultMindmapData, null, 2),
"utf8",
);
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
const { error } = await supabase
.from("documents")
.update({ mindmap_data: data ?? defaultMindmapData })
.eq("id", id)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ docId: string }> },
) {
const { docId: id } = await params;
const { docId } = await params;
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const mindmapId = `legacy-${id}`;
const result = await client.mutation(api.mindmaps.softDelete, {
userId: auth.userId,
docId: id,
mindmapId,
});
return NextResponse.json({ ok: true, ...(result ?? {}) });
const { client } = await getAuthedConvexClient();
const mindmapId = `legacy-${docId}`;
const result = await client.mutation(api.mindmaps.softDelete, { docId, mindmapId });
return NextResponse.json(result ?? { ok: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const preferredFolder = path.join(preferredBaseDir, id);
const preferredFile = path.join(preferredFolder, "mindmap.json");
const legacyFolder = path.join(legacyBaseDir, id);
const legacyFile = path.join(legacyFolder, "mindmap.json");
await Promise.all([removeFileSafe(preferredFile), removeFileSafe(legacyFile)]);
const { error } = await supabase
.from("documents")
.update({ mindmap_data: null })
.eq("id", id)
.eq("user_id", session.user.id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ ok: true });
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import supabaseAdmin from "@/lib/supabase/admin";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
@@ -133,34 +132,5 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 1 });
}
}
const { data: asset, error: assetError } = await supabaseAdmin
.from("media_assets")
.select("id,bucket,storage_path,mime_type")
.eq("id", assetId)
.single();
if (assetError || !asset) {
return NextResponse.json({ error: 1 });
}
const downloadUrl = tryRewriteOnlyOfficeDownloadUrl(body.url);
const upstream = await fetch(downloadUrl, { method: "GET", redirect: "follow" });
if (!upstream.ok) {
return NextResponse.json({ error: 1 });
}
const buf = Buffer.from(await upstream.arrayBuffer());
const { error: uploadError } = await supabaseAdmin.storage
.from(asset.bucket)
.upload(asset.storage_path, buf, {
upsert: true,
contentType: asset.mime_type || "application/octet-stream",
});
if (uploadError) {
return NextResponse.json({ error: 1 });
}
return NextResponse.json({ error: 0 });
return NextResponse.json({ error: 1 });
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -28,6 +27,9 @@ export async function GET(request: Request) {
return NextResponse.json({ backlinks });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -73,4 +75,5 @@ export async function GET(request: Request) {
}
return NextResponse.json({ backlinks: data ?? [] });
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -43,6 +42,9 @@ export async function POST(request: Request) {
return NextResponse.json({ reference });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -92,4 +94,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ reference: data });
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -34,211 +33,12 @@ const TIME_FIELD_COLUMN = {
created: "created_at",
} as const;
const escapeLike = (value: string): string => value.replace(/[%_\\]/g, (match) => `\\${match}`);
interface DocumentRow {
id: string;
title: string | null;
raw_text: string | null;
updated_at: string | null;
created_at: string | null;
}
const buildIsoBoundary = (value: string, isEnd = false): string | null => {
if (!value) {
return null;
}
const normalized = value.trim();
if (!normalized) {
return null;
}
const suffix = isEnd ? "T23:59:59.999Z" : "T00:00:00.000Z";
const date = new Date(`${normalized}${suffix}`);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toISOString();
};
const mapDocumentToResult = (
row: DocumentRow,
keyword: string | null,
forcedMatch?: DocumentSearchResult["matchField"],
): DocumentSearchResult => {
const normalizedKeyword = keyword?.trim() ?? "";
const normalizedTitle = row.title ?? "无标题";
let matchField: DocumentSearchResult["matchField"] = "recent";
if (forcedMatch) {
matchField = forcedMatch;
} else if (normalizedKeyword) {
const matchesTitle = normalizedTitle.toLowerCase().includes(normalizedKeyword.toLowerCase());
matchField = matchesTitle ? "title" : "content";
}
return {
id: row.id,
title: normalizedTitle,
snippet: buildSnippet(row.raw_text, normalizedKeyword || null),
updatedAt: row.updated_at,
createdAt: row.created_at,
matchField,
hasOcr: Boolean(row.raw_text),
publicPath: `/documents/${row.id}`,
score: matchField === "title" ? 2 : 1,
};
};
type RouteSupabaseClient = Awaited<ReturnType<typeof createSupabaseRouteClient>>;
const fetchRecentResults = async (
supabase: RouteSupabaseClient,
userId: string,
workspaceId: string,
): Promise<DocumentSearchResult[]> => {
const { data: recentRows, error: recentError } = await supabase
.from("user_recent_pages")
.select("document_id,last_accessed_at")
.eq("user_id", userId)
.eq("workspace_id", workspaceId)
.order("last_accessed_at", { ascending: false })
.limit(10);
if (recentError || !recentRows || recentRows.length === 0) {
return [];
}
const documentIds = recentRows.map((row) => row.document_id);
const { data: docRows, error: docsError } = await supabase
.from("documents")
.select("id,title,updated_at,created_at,raw_text")
.in("id", documentIds)
.is("deleted_at", null);
if (docsError || !docRows) {
return [];
}
const docMap = new Map(docRows.map((row) => [row.id, row]));
return recentRows
.map((row) => docMap.get(row.document_id))
.filter((row): row is DocumentRow => Boolean(row))
.map((row) => mapDocumentToResult(row, null, "recent"));
};
const fetchOcrMatches = async (
supabase: RouteSupabaseClient,
workspaceId: string,
likePattern: string,
limit: number,
) => {
const { data, error } = await supabase
.from("media_assets")
.select("document_id,ocr_text")
.eq("workspace_id", workspaceId)
.is("deleted_at", null)
.not("ocr_text", "is", null)
.ilike("ocr_text", likePattern)
.limit(limit);
if (error || !data) {
return [];
}
return data;
};
export async function POST(request: Request) {
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
const payload = (await request.json()) as DocumentSearchRequest;
const workspaceId = payload.workspaceId;
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const filters: DocumentSearchFilters = {
...DEFAULT_FILTERS,
...payload.filters,
};
const limit = Math.min(payload.limit ?? 30, MAX_LIMIT);
const normalizedQuery = payload.query?.trim() ?? "";
const normalizedLower = normalizedQuery.toLowerCase();
const docs = await client.query(api.documents.listByWorkspace, {
userId: auth.userId,
workspaceId,
});
const sortedByRecent = [...docs].sort((a, b) => {
const ta = a.updated_at ?? a.created_at ?? "";
const tb = b.updated_at ?? b.created_at ?? "";
return tb.localeCompare(ta);
});
const recentRows = await client.query(api.recents.listByWorkspace, {
userId: auth.userId,
workspaceId,
limit: 10,
});
const docMap = new Map(docs.map((d) => [d.id, d]));
const recent: DocumentSearchResult[] = recentRows
.map((r) => docMap.get(r.document_id))
.filter((row): row is (typeof docs)[number] => Boolean(row))
.map((row) => ({
id: row.id,
title: row.title ?? "无标题",
snippet: "",
updatedAt: row.updated_at ?? null,
createdAt: row.created_at ?? null,
matchField: "recent",
hasOcr: false,
publicPath: `/documents/${row.id}`,
score: 0,
}));
if (!normalizedQuery) {
const response: DocumentSearchResponse = { results: [], recent };
return NextResponse.json(response);
}
const narrowed = sortedByRecent.filter((row) => {
if (filters.onlyCurrentPage && payload.documentId) {
if (row.id !== payload.documentId) return false;
}
const title = (row.title ?? "无标题").toLowerCase();
return title.includes(normalizedLower);
});
const results: DocumentSearchResult[] = narrowed.slice(0, limit).map((row) => ({
id: row.id,
title: row.title ?? "无标题",
snippet: buildSnippet(row.title ?? "", normalizedQuery),
updatedAt: row.updated_at ?? null,
createdAt: row.created_at ?? null,
matchField: "title",
hasOcr: false,
publicPath: `/documents/${row.id}`,
score: 2,
}));
const response: DocumentSearchResponse = {
results,
recent,
};
return NextResponse.json(response);
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
const { auth, client } = await getAuthedConvexClient();
const payload = (await request.json()) as DocumentSearchRequest;
const workspaceId = payload.workspaceId;
@@ -251,140 +51,82 @@ export async function POST(request: Request) {
...payload.filters,
};
const { data: membership, error: membershipError } = await supabase
.from("workspace_members")
.select("workspace_id")
.eq("workspace_id", workspaceId)
.eq("user_id", session.user.id)
.limit(1);
if (membershipError) {
return NextResponse.json({ error: membershipError.message }, { status: 500 });
}
if (!membership || membership.length === 0) {
return NextResponse.json({ error: "无权访问该工作空间" }, { status: 403 });
}
if (filters.onlyCurrentPage && !payload.documentId) {
filters.onlyCurrentPage = false;
}
const limit = Math.min(payload.limit ?? 30, MAX_LIMIT);
const normalizedQuery = payload.query?.trim() ?? "";
const likePattern = filters.exact ? normalizedQuery : `%${escapeLike(normalizedQuery)}%`;
const timeColumn = TIME_FIELD_COLUMN[filters.timeField ?? "updated"];
const normalizedLower = normalizedQuery.toLowerCase();
let builder = supabase
.from("documents")
.select("id,title,updated_at,created_at,raw_text,is_starred,access_scope")
.eq("workspace_id", workspaceId)
.is("deleted_at", null)
.order(timeColumn, { ascending: false })
.limit(limit);
const docs = await client.query(api.documents.listByWorkspace, { workspaceId });
const docMap = new Map(docs.map((d) => [d.id, d]));
if (filters.onlyCurrentPage && payload.documentId) {
builder = builder.eq("id", payload.documentId);
const recentRows = await client.query(api.recents.listByWorkspace, {
userId: auth.userId,
workspaceId,
limit: 10,
});
const recent: DocumentSearchResult[] = recentRows
.map((r) => docMap.get(r.document_id))
.filter((row): row is (typeof docs)[number] => Boolean(row))
.map((row) => ({
id: row.id,
title: row.title ?? "无标题",
snippet: "",
updatedAt: row.updated_at ?? null,
createdAt: row.created_at ?? null,
matchField: "recent",
hasOcr: false,
publicPath: `/documents/${row.id}`,
score: 0,
}));
if (!normalizedQuery) {
const response: DocumentSearchResponse = { results: [], recent };
return NextResponse.json(response);
}
const customFrom = filters.customRange?.from ? buildIsoBoundary(filters.customRange.from, false) : null;
const customTo = filters.customRange?.to ? buildIsoBoundary(filters.customRange.to, true) : null;
const timeRangeMs = TIME_RANGE_TO_MS[filters.timeRange];
const timeField = TIME_FIELD_COLUMN[filters.timeField];
const boundaryIso =
typeof timeRangeMs === "number"
? new Date(Date.now() - timeRangeMs).toISOString()
: null;
if (customFrom) {
builder = builder.gte(timeColumn, customFrom);
}
if (customTo) {
builder = builder.lte(timeColumn, customTo);
}
if (!customFrom && !customTo) {
const now = Date.now();
const offset = TIME_RANGE_TO_MS[filters.timeRange];
if (offset) {
const from = new Date(now - offset).toISOString();
builder = builder.gte(timeColumn, from);
}
}
if (normalizedQuery) {
if (filters.titleOnly) {
builder = filters.exact ? builder.eq("title", normalizedQuery) : builder.ilike("title", likePattern);
} else {
const clauses = [
`title.${filters.exact ? `eq.${normalizedQuery}` : `ilike.${likePattern}`}`,
];
if (filters.includeOcr) {
clauses.push(`raw_text.ilike.${likePattern}`);
const narrowed = [...docs]
.sort((a, b) => {
const ta = a.updated_at ?? a.created_at ?? "";
const tb = b.updated_at ?? b.created_at ?? "";
return tb.localeCompare(ta);
})
.filter((row) => {
if (filters.onlyCurrentPage && payload.documentId) {
if (row.id !== payload.documentId) return false;
}
builder = builder.or(clauses.join(","));
}
}
const shouldSearchOcr = Boolean(filters.includeOcr && normalizedQuery);
const [{ data, error }, recentResults, ocrMatches] = await Promise.all([
builder,
fetchRecentResults(supabase, session.user.id, workspaceId),
shouldSearchOcr ? fetchOcrMatches(supabase, workspaceId, likePattern, limit) : Promise.resolve([]),
]);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
let results: DocumentSearchResult[] = (data ?? []).map((row) =>
mapDocumentToResult(row, normalizedQuery || null),
);
if (ocrMatches.length > 0 && normalizedQuery) {
const snippetMap = new Map<string, string>();
ocrMatches.forEach((row: { document_id: string; ocr_text: string | null }) => {
if (!row.ocr_text) return;
if (!snippetMap.has(row.document_id)) {
snippetMap.set(row.document_id, row.ocr_text);
if (boundaryIso) {
const ts = (row as any)?.[timeField] ?? null;
if (!ts || typeof ts !== "string") return false;
if (ts < boundaryIso) return false;
}
const title = (row.title ?? "无标题").toLowerCase();
if (filters.exact) {
return title === normalizedLower;
}
return title.includes(normalizedLower);
});
if (snippetMap.size > 0) {
const resultMap = new Map(results.map((item) => [item.id, item]));
const missingDocIds: string[] = [];
snippetMap.forEach((text, docId) => {
const snippet = buildSnippet(text, normalizedQuery || null);
if (resultMap.has(docId)) {
const existing = resultMap.get(docId)!;
resultMap.set(docId, {
...existing,
snippet: snippet || existing.snippet,
hasOcr: true,
matchField: "content",
});
} else {
missingDocIds.push(docId);
}
});
let extraResults: DocumentSearchResult[] = [];
if (missingDocIds.length > 0) {
const { data: extraDocs } = await supabase
.from("documents")
.select("id,title,updated_at,created_at,raw_text")
.in("id", missingDocIds)
.is("deleted_at", null);
extraResults =
extraDocs?.map((row) => {
const snippetText = snippetMap.get(row.id) ?? row.raw_text ?? "";
return {
...mapDocumentToResult(row, normalizedQuery || null, "content"),
snippet: buildSnippet(snippetText, normalizedQuery || null),
hasOcr: true,
};
}) ?? [];
}
results = [...resultMap.values(), ...extraResults];
}
}
const response: DocumentSearchResponse = {
results,
recent: recentResults,
};
const results: DocumentSearchResult[] = narrowed.slice(0, limit).map((row) => ({
id: row.id,
title: row.title ?? "无标题",
snippet: buildSnippet(row.title ?? "", normalizedQuery),
updatedAt: row.updated_at ?? null,
createdAt: row.created_at ?? null,
matchField: "title",
hasOcr: false,
publicPath: `/documents/${row.id}`,
score: 2,
}));
const response: DocumentSearchResponse = { results, recent };
return NextResponse.json(response);
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
@@ -28,6 +27,9 @@ export async function POST(request: Request) {
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -72,4 +74,5 @@ export async function POST(request: Request) {
}
return NextResponse.json({ ok: true });
*/
}
+6 -18
View File
@@ -1,19 +1,9 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { ensureDefaultWorkspace, fetchWorkspaceSummaries } from "@/lib/workspaces";
import { fetchSidebarDataset } from "@/lib/sidebar-tree";
import type { SidebarInitialData } from "@/components/sidebar/types";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getConvexHttpClient } from "@/lib/convex/server";
import { api } from "@/lib/convex/api";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { randomUUID } from "crypto";
import {
detectLocalMindmapFiles,
detectLocalMindmapDocs,
detectLocalMindmapImageAssetIdsByMindmapId,
detectLocalTrashedMindmapAssets,
} from "@/lib/server/mindmap-files";
import type { MediaAsset } from "@/types/media";
export const dynamic = "force-dynamic";
@@ -72,20 +62,17 @@ function extractMindmapImageAssetIdsFromData(input: unknown): string[] {
export async function GET(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const client = getConvexHttpClient();
const { auth, client } = await getAuthedConvexClient();
const url = new URL(request.url);
const workspaceIdParam = url.searchParams.get("workspaceId");
const bootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
userId: auth.userId,
fallbackName: auth.email ?? auth.name ?? "我的空间",
workspaceIdIfCreate: randomUUID(),
});
const summaries = await client.query(api.workspaces.fetchWorkspaceSummaries, {
userId: auth.userId,
});
const workspaces = summaries.workspaces.length > 0 ? summaries.workspaces : bootstrap.workspaces;
@@ -98,17 +85,14 @@ export async function GET(request: Request) {
try {
const documents = await client.query(api.documents.listByWorkspace, {
userId: auth.userId,
workspaceId: targetWorkspaceId,
});
const trashedDocuments = await client.query(api.documents.listTrashedByWorkspace, {
userId: auth.userId,
workspaceId: targetWorkspaceId,
});
const mindmapRows = await client.query(api.mindmaps.listByWorkspace, {
userId: auth.userId,
workspaceId: targetWorkspaceId,
includeDeleted: true,
});
@@ -200,6 +184,9 @@ export async function GET(request: Request) {
}
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -301,4 +288,5 @@ export async function GET(request: Request) {
{ status: 500 },
);
}
*/
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import type { DocumentTableSnapshot, TableRowData } from "@/types/online-table";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
@@ -25,25 +24,12 @@ const extractTableId = async (context: RouteContext) => {
return (await context.params).tableId;
};
const getAuthUser = async () => {
const supabase = await createSupabaseRouteClient();
const { data, error } = await supabase.auth.getUser();
if (error) {
console.error("Auth getUser error:", error);
}
if (!data?.user) {
return { supabase, user: null };
}
return { supabase, user: data.user };
};
export async function GET(request: Request, context: RouteContext) {
export async function GET(_request: Request, context: RouteContext) {
const tableId = await extractTableId(context);
if (!tableId) {
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
}
// Convex 模式
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
try {
@@ -63,36 +49,7 @@ export async function GET(request: Request, context: RouteContext) {
}
}
// Supabase 模式
const { supabase, user } = await getAuthUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
// 假设 RLS 确保了用户只能访问其有权限的表格
const { data: tableData, error } = await supabase
.from("document_tables")
.select("*")
.eq("id", tableId)
.single();
if (error) {
console.error("Error fetching table:", error);
return NextResponse.json({ error: "Failed to fetch table data" }, { status: 500 });
}
if (!tableData) {
return NextResponse.json({ error: "Table not found" }, { status: 404 });
}
return NextResponse.json(tableData, { status: 200 });
} catch (error) {
console.error("API error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function PATCH(request: Request, context: RouteContext) {
@@ -101,13 +58,18 @@ export async function PATCH(request: Request, context: RouteContext) {
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
}
// Convex 模式
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
let body: UpdateTableRequest;
try {
const body = await request.json() as UpdateTableRequest;
body = (await request.json()) as UpdateTableRequest;
} catch (error) {
console.error("Invalid payload:", error);
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
}
const result = await client.mutation(api.tables.update, {
try {
await client.mutation(api.tables.update, {
userId: auth.userId,
tableId,
title: body.title,
@@ -117,7 +79,6 @@ export async function PATCH(request: Request, context: RouteContext) {
rows: body.rows,
});
// 重新获取更新后的表格数据
const updated = await client.query(api.tables.get, {
userId: auth.userId,
tableId,
@@ -130,125 +91,15 @@ export async function PATCH(request: Request, context: RouteContext) {
}
}
// Supabase 模式
const { supabase, user } = await getAuthUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: UpdateTableRequest | null = null;
try {
body = await request.json() as UpdateTableRequest;
} catch (error) {
console.error("Invalid payload:", error);
return NextResponse.json({ error: "Invalid payload" }, { status: 400 });
}
try {
const { data: tableMeta, error: metaError } = await supabase
.from("document_tables")
.select("id, workspace_id, document_id, grid_key")
.eq("id", tableId)
.single();
if (metaError || !tableMeta) {
console.error("Table not found or load error:", metaError);
return NextResponse.json({ error: "Table not found" }, { status: 404 });
}
const updatePayload: Record<string, unknown> = {
updated_by: user.id,
last_synced_at: new Date().toISOString(),
};
if (body?.title !== undefined) {
updatePayload.title = body.title;
}
if (body?.schema) {
updatePayload.schema = body.schema;
}
if (body?.snapshot !== undefined) {
updatePayload.snapshot = body.snapshot ?? {};
}
if (body?.viewPreferences) {
updatePayload.view_preferences = body.viewPreferences;
}
const { error: updateError } = await supabase
.from("document_tables")
.update(updatePayload)
.eq("id", tableId);
if (updateError) {
console.error("Failed to update table:", updateError);
return NextResponse.json({ error: "Failed to update table", details: updateError.message }, { status: 500 });
}
const nextRows = body?.rows ?? body?.snapshot?.rows ?? [];
if (Array.isArray(nextRows)) {
const { error: deleteRowsError } = await supabase
.from("document_table_rows")
.delete()
.eq("table_id", tableId);
if (deleteRowsError) {
console.error("Failed to clear old rows:", deleteRowsError);
return NextResponse.json({ error: "Failed to update rows", details: deleteRowsError.message }, { status: 500 });
}
const sanitizedRows = nextRows
.filter((row) => row && typeof row === "object" && Object.keys(row).length > 0);
if (sanitizedRows.length > 0) {
const rowsToInsert = sanitizedRows.map((row, index) => ({
table_id: tableId,
workspace_id: tableMeta.workspace_id,
document_id: tableMeta.document_id,
row_index: index,
row_data: row,
row_hash: null,
is_deleted: false,
updated_by: user.id,
}));
const { error: insertError } = await supabase
.from("document_table_rows")
.insert(rowsToInsert);
if (insertError) {
console.error("Failed to insert rows:", insertError);
return NextResponse.json({ error: "Failed to insert rows", details: insertError.message }, { status: 500 });
}
}
}
const { data: latest, error: fetchError } = await supabase
.from("document_tables")
.select("*")
.eq("id", tableId)
.single();
if (fetchError || !latest) {
console.error("Failed to fetch updated table:", fetchError);
return NextResponse.json({ error: "Failed to load updated table", details: fetchError?.message }, { status: 500 });
}
return NextResponse.json(latest, { status: 200 });
} catch (error) {
console.error("API error:", error);
const message = error instanceof Error ? error.message : "Internal Server Error";
return NextResponse.json({ error: message }, { status: 500 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
export async function DELETE(request: Request, context: RouteContext) {
export async function DELETE(_request: Request, context: RouteContext) {
const tableId = await extractTableId(context);
if (!tableId) {
return NextResponse.json({ error: "Missing tableId" }, { status: 400 });
}
// Convex 模式
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
try {
@@ -264,37 +115,6 @@ export async function DELETE(request: Request, context: RouteContext) {
}
}
// Supabase 模式
const { supabase, user } = await getAuthUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const { error: deleteRowsError } = await supabase
.from("document_table_rows")
.delete()
.eq("table_id", tableId);
if (deleteRowsError) {
console.error("Failed to delete table rows:", deleteRowsError);
return NextResponse.json({ error: "Failed to delete table rows" }, { status: 500 });
}
const { error: deleteTableError } = await supabase
.from("document_tables")
.delete()
.eq("id", tableId);
if (deleteTableError) {
console.error("Failed to delete table:", deleteTableError);
return NextResponse.json({ error: "Failed to delete table" }, { status: 500 });
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("API error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { DocumentTableSnapshot, TableSchema } from "@/types/online-table";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
@@ -27,7 +26,6 @@ export async function POST(request: Request) {
// 获取 document 所在的 workspace_id
const document = await client.query(api.documents.getMeta, {
userId: auth.userId,
id: documentId,
});
@@ -54,6 +52,9 @@ export async function POST(request: Request) {
}
// Supabase 模式
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
/*
const supabase = await createSupabaseRouteClient();
const { data: { user } } = await supabase.auth.getUser();
@@ -112,4 +113,5 @@ export async function POST(request: Request) {
console.error("API error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
*/
}
@@ -1,75 +1,24 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { requireAuthContext } from "@/lib/auth/authContext";
import { getConvexHttpClient } from "@/lib/convex/server";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
if (isConvexEnabled()) {
const auth = await requireAuthContext();
const { client } = await getAuthedConvexClient();
const payload = await request.json().catch(() => ({}));
const workspaceId = payload.workspaceId as string | undefined;
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const client = getConvexHttpClient();
await client.mutation(api.workspaces.switchDefaultWorkspace, {
userId: auth.userId,
workspaceId,
});
return NextResponse.json({ success: true });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const payload = await request.json().catch(() => ({}));
const workspaceId = payload.workspaceId as string | undefined;
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const { data: membership, error: membershipError } = await supabase
.from("workspace_members")
.select("id")
.eq("workspace_id", workspaceId)
.eq("user_id", session.user.id)
.single();
if (membershipError || !membership) {
return NextResponse.json({ error: "无权切换至该工作空间" }, { status: 403 });
}
const { error: resetError } = await supabase
.from("workspace_members")
.update({ is_default: false })
.eq("user_id", session.user.id);
if (resetError) {
return NextResponse.json({ error: resetError.message }, { status: 500 });
}
const { error: switchError } = await supabase
.from("workspace_members")
.update({ is_default: true })
.eq("user_id", session.user.id)
.eq("workspace_id", workspaceId);
if (switchError) {
return NextResponse.json({ error: switchError.message }, { status: 500 });
}
return NextResponse.json({ success: true });
return NextResponse.json({ error: "仅支持 Convex 模式" }, { status: 501 });
}