0.3.1 UI修复
This commit is contained in:
@@ -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 });
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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:不要强制 download(Content-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 });
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user