0.4.0 convex及界面修改

This commit is contained in:
liaibo
2026-02-01 08:47:40 +08:00
parent d1f055f51a
commit af92c4b149
636 changed files with 7522 additions and 1815 deletions
@@ -34,8 +34,15 @@ export async function POST(request: Request) {
const target = await client.query(api.documents.getContent, { id: targetDocumentId });
if (!target) return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
const targetMeta = await client.query(api.documents.getMeta, { id: targetDocumentId });
const targetBlocks = getBlocksFromDocumentContent(target.content);
const anchorId = (targetMeta as any)?.embed_default_block_id as string | null | undefined;
const anchorIndex =
typeof anchorId === "string" && anchorId.trim()
? targetBlocks.findIndex((b) => String((b as any)?.id ?? "") === anchorId)
: -1;
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : targetBlocks.length;
const referenceBlock = {
id: randomUUID(),
type: "blockReference",
@@ -48,7 +55,7 @@ export async function POST(request: Request) {
children: [],
};
const nextBlocks = [...targetBlocks, referenceBlock];
const nextBlocks = [...targetBlocks.slice(0, insertIndex), referenceBlock, ...targetBlocks.slice(insertIndex)];
const payload = withBlocksWrittenBack(target.content, nextBlocks);
await client.mutation(api.documents.updateContent, { id: targetDocumentId, content: payload });
@@ -30,10 +30,18 @@ export async function POST(request: Request) {
if (!targetContent) {
return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
}
const targetMeta = await client.query(api.documents.getMeta, { id: targetId });
const currentBlocks = extractBlocksFromContent(targetContent.content);
const anchorId = (targetMeta as any)?.embed_default_block_id as string | null | undefined;
const anchorIndex =
typeof anchorId === "string" && anchorId.trim()
? currentBlocks.findIndex((b) => typeof b === "object" && b !== null && (b as any).id === anchorId)
: -1;
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : currentBlocks.length;
const nextBlocks: Json[] = [
...currentBlocks,
...currentBlocks.slice(0, insertIndex),
{
id: randomUUID(),
type: "pageReference",
@@ -42,6 +50,7 @@ export async function POST(request: Request) {
title: sourceDoc.title ?? "无标题",
},
},
...currentBlocks.slice(insertIndex),
];
const payload: Json = composeContentWithBlocks(targetContent.content, nextBlocks);
@@ -27,6 +27,12 @@ export async function POST(request: Request) {
showStructure: options.showStructure,
protectEditing: options.protectEditing,
showWordCount: options.showWordCount,
collapseBacklinks: options.collapseBacklinks,
pageFont: options.pageFont,
layoutDensity: options.layoutDensity,
hideChildPages: options.hideChildPages,
showBlockRefCount: options.showBlockRefCount,
embedDefaultBlockId: typeof options.embedDefaultBlockId === "string" ? options.embedDefaultBlockId : null,
},
});
@@ -22,6 +22,8 @@ export async function POST(request: Request) {
wordCount: stats.wordCount,
characterCount: stats.characterCount,
blockCount: stats.blockCount,
todoTotal: stats.todoTotal,
todoDone: stats.todoDone,
});
return NextResponse.json({ ok: true });
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
type TemplatePayload = {
documentId: string;
isTemplate: boolean;
};
export async function POST(request: Request) {
if (isConvexEnabled()) {
const { documentId, isTemplate }: TemplatePayload = await request.json();
if (!documentId || typeof isTemplate !== "boolean") {
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
}
const { client } = await getAuthedConvexClient();
await client.mutation(api.documents.setTemplate, { id: documentId, isTemplate });
return NextResponse.json({ ok: true });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -4,6 +4,7 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
@@ -37,7 +38,13 @@ export async function GET(request: Request) {
limit: Number.isNaN(limit) ? 12 : limit,
});
return NextResponse.json({ items: (items ?? []) as MediaAsset[] });
const safeItems = (items ?? []).map((a: any) => ({
...a,
file_url: maybeProxyForBrowserUrl(request, String(a?.file_url ?? "")),
thumbnail_url: maybeProxyForBrowserUrl(request, String(a?.thumbnail_url ?? a?.file_url ?? "")),
})) as MediaAsset[];
return NextResponse.json({ items: safeItems });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
@@ -149,7 +156,13 @@ export async function POST(request: Request) {
},
});
return NextResponse.json({ asset });
const safeAsset = {
...asset,
file_url: maybeProxyForBrowserUrl(request, String(asset?.file_url ?? "")),
thumbnail_url: maybeProxyForBrowserUrl(request, String(asset?.thumbnail_url ?? asset?.file_url ?? "")),
} as MediaAsset;
return NextResponse.json({ asset: safeAsset });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
@@ -6,6 +6,7 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
@@ -200,8 +201,14 @@ export async function POST(request: Request) {
}
}
return NextResponse.json({ items: results });
}
const safeItems = (results ?? []).map((a: any) => ({
...a,
file_url: maybeProxyForBrowserUrl(request, String(a?.file_url ?? "")),
thumbnail_url: maybeProxyForBrowserUrl(request, String(a?.thumbnail_url ?? a?.file_url ?? "")),
}));
return NextResponse.json({ items: safeItems });
}
default:
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
}
@@ -4,6 +4,7 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
@@ -34,9 +35,14 @@ export async function GET(request: Request) {
limit: Number.isNaN(limit) ? 200 : limit,
});
return NextResponse.json({ items: (items ?? []) as MediaAsset[] });
const safeItems = (items ?? []).map((a: any) => ({
...a,
file_url: maybeProxyForBrowserUrl(request, String(a?.file_url ?? "")),
thumbnail_url: maybeProxyForBrowserUrl(request, String(a?.thumbnail_url ?? a?.file_url ?? "")),
})) as MediaAsset[];
return NextResponse.json({ items: safeItems });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -10,21 +10,10 @@ interface EmptyTrashPayload {
workspaceId?: string;
}
function resolveGraceSeconds(): number {
const raw =
process.env.DELETE_GRACE_SECONDS ??
process.env.NEXT_PUBLIC_DELETE_GRACE_SECONDS ??
"600";
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) {
return 600;
}
return Math.floor(parsed);
}
function makeExpiredDeletedAt(): string {
const graceSeconds = resolveGraceSeconds();
return new Date(Date.now() - (graceSeconds + 5) * 1000).toISOString();
function makeExpiredDeletedAtForForceEmpty(): string {
// “清空附件垃圾桶”是一个显式的强制操作:直接清空所有垃圾桶内的附件,不受 10 分钟宽限期限制。
// Convex 侧按 `deleted_at <= expiredDeletedAt` 做过滤,因此这里给一个很靠后的时间上界即可。
return new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).toISOString();
}
export async function POST(request: Request) {
@@ -48,7 +37,7 @@ export async function POST(request: Request) {
const res = await client.mutation(api.mediaAssets.emptyTrashByWorkspace, {
userId: auth.userId,
workspaceId,
expiredDeletedAt: makeExpiredDeletedAt(),
expiredDeletedAt: makeExpiredDeletedAtForForceEmpty(),
});
return NextResponse.json({ success: true, ...(res as any) });
+2 -36
View File
@@ -3,44 +3,10 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
import { api } from "@/lib/convex/api";
import { HttpError } from "@/lib/auth/authContext";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
const base64UrlEncodeUtf8 = (input: string) => {
return Buffer.from(input, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
};
const getRequestOrigin = (request: Request) => {
const xfProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
const xfHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
const host = xfHost || request.headers.get("host") || new URL(request.url).host;
const protocol = (xfProto || new URL(request.url).protocol.replace(/:$/, "")) + ":";
return `${protocol}//${host}`;
};
const isLocalHostname = (hostname: string) =>
hostname === "127.0.0.1" || hostname === "localhost" || hostname === "host.docker.internal";
const maybeProxyForBrowser = (request: Request, rawUrl: string) => {
const input = String(rawUrl || "").trim();
if (!input) return input;
if (input.startsWith("/api/onlyoffice/proxy")) return input;
try {
const u = new URL(input);
if (!isLocalHostname(u.hostname)) return input;
const origin = getRequestOrigin(request);
const proxy = new URL("/api/onlyoffice/proxy", origin);
proxy.searchParams.set("u", base64UrlEncodeUtf8(input));
return proxy.toString();
} catch {
return input;
}
};
export async function GET(request: Request) {
if (isConvexEnabled()) {
let auth;
@@ -75,7 +41,7 @@ export async function GET(request: Request) {
}
return NextResponse.json({
signedUrl: maybeProxyForBrowser(request, signedUrl),
signedUrl: maybeProxyForBrowserUrl(request, signedUrl),
asset: {
id: asset.id,
file_name: asset.file_name,
@@ -1,44 +1,10 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
const base64UrlEncodeUtf8 = (input: string) => {
return Buffer.from(input, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
};
const getRequestOrigin = (request: Request) => {
const xfProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
const xfHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
const host = xfHost || request.headers.get("host") || new URL(request.url).host;
const protocol = (xfProto || new URL(request.url).protocol.replace(/:$/, "")) + ":";
return `${protocol}//${host}`;
};
const isLocalHostname = (hostname: string) =>
hostname === "127.0.0.1" || hostname === "localhost" || hostname === "host.docker.internal";
const maybeProxyForBrowser = (request: Request, rawUrl: string) => {
const input = String(rawUrl || "").trim();
if (!input) return input;
if (input.startsWith("/api/onlyoffice/proxy")) return input;
try {
const u = new URL(input);
if (!isLocalHostname(u.hostname)) return input;
const origin = getRequestOrigin(request);
const proxy = new URL("/api/onlyoffice/proxy", origin);
proxy.searchParams.set("u", base64UrlEncodeUtf8(input));
return proxy.toString();
} catch {
return input;
}
};
export async function GET(request: Request) {
if (isConvexEnabled()) {
try {
@@ -58,7 +24,7 @@ export async function GET(request: Request) {
// 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。
// 由于外链本身已经是可访问的 URL,这里只做最小透传。
return NextResponse.json({ signedUrl: maybeProxyForBrowser(request, fileUrl) });
return NextResponse.json({ signedUrl: maybeProxyForBrowserUrl(request, fileUrl) });
}
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
@@ -7,6 +7,7 @@ import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
export const dynamic = "force-dynamic";
@@ -89,9 +90,14 @@ export async function POST(request: Request) {
});
const asset = created as unknown as MediaAsset;
const safeAsset = {
...(asset as any),
file_url: maybeProxyForBrowserUrl(request, String((asset as any)?.file_url ?? "")),
thumbnail_url: maybeProxyForBrowserUrl(request, String((asset as any)?.thumbnail_url ?? (asset as any)?.file_url ?? "")),
} as MediaAsset;
return NextResponse.json({
asset,
asset: safeAsset,
mindmapUrl: `asset:${asset.id}`,
});
} catch (error) {
@@ -3,6 +3,11 @@ import { detectLocalMindmapFiles } from "@/lib/server/mindmap-files";
import type { MediaAsset } from "@/types/media";
import path from "path";
import { promises as fs } from "fs";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { api } from "@/lib/convex/api";
import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser";
import { HttpError } from "@/lib/auth/authContext";
export const dynamic = "force-dynamic";
@@ -39,7 +44,89 @@ async function listTestPdfs(): Promise<AgentAssetItem[]> {
}
export async function GET(request: Request) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
const { searchParams } = new URL(request.url);
const documentId = String(searchParams.get("documentId") ?? "").trim();
const q = String(searchParams.get("q") ?? "").trim().toLowerCase();
const workspaceOnly = String(searchParams.get("workspaceOnly") ?? "").trim() === "1";
if (!documentId) {
return NextResponse.json({ error: "缺少 documentId" }, { status: 400 });
}
let auth: { userId: string };
let client: any;
try {
const res = await getAuthedConvexClient();
auth = res.auth;
client = res.client;
} catch (err) {
if (err instanceof HttpError) {
return NextResponse.json({ error: "未登录" }, { status: err.status });
}
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const doc = await client.query(api.documents.getMeta, { id: documentId });
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
if (workspaceOnly) {
return NextResponse.json({
ok: true,
workspaceId: (doc as any).workspace_id ?? null,
documentId,
items: [],
});
}
// 说明:这里的附件列表主要用于 Mindmap AI Agent 选择引用素材。
const mediaRows = (await client.query(api.mediaAssets.listByDocument, {
userId: auth.userId,
documentId,
limit: 200,
})) as unknown as MediaAsset[];
const mediaAssets: AgentAssetItem[] = (mediaRows ?? []).map((row) => ({
kind: "media" as const,
id: String((row as any).id ?? ""),
title: String((row as any).file_name ?? (row as any).id ?? "附件"),
fileUrl: maybeProxyForBrowserUrl(request, String((row as any).file_url ?? "")),
mimeType: (row as any).mime_type ?? null,
assetType: (row as any).asset_type ?? null,
fileName: (row as any).file_name ?? null,
}));
const localMindmaps = (await detectLocalMindmapFiles([documentId]))
.filter((x) => x.documentId === documentId)
.map((x) => ({
kind: "local-mindmap" as const,
id: `mindmap:${x.mindmapId}`,
title: x.fileName,
fileUrl: `/documents/${documentId}/${x.fileName}`,
mimeType: "application/json",
assetType: "mindmap",
fileName: x.fileName,
}));
const testPdfs = await listTestPdfs();
let items: AgentAssetItem[] = [...localMindmaps, ...mediaAssets, ...testPdfs];
if (q) {
items = items.filter((it) => {
const hay = `${it.title} ${it.fileName ?? ""}`.toLowerCase();
return hay.includes(q);
});
}
return NextResponse.json({
ok: true,
workspaceId: (doc as any).workspace_id ?? null,
documentId,
items,
});
/*
const supabase = await createSupabaseRouteClient();
+66 -1
View File
@@ -109,6 +109,13 @@ export async function GET(request: Request) {
limit: 2000,
});
const tables = await client.query(api.tables.listByWorkspaceForSearch, {
userId: auth.userId,
workspaceId: targetWorkspaceId,
includeArchived: true,
limit: 3000,
});
const activeMindmaps = (mindmapRows ?? []).filter((r) => !r.deleted_at);
const trashedMindmaps = (mindmapRows ?? []).filter((r) => !!r.deleted_at);
@@ -173,6 +180,63 @@ export async function GET(request: Request) {
};
});
const tableAssets: MediaAsset[] = (tables ?? [])
.filter((row) => !row.is_archived)
.map((row) => {
const base = String(row.title ?? "未命名表格").trim() || "未命名表格";
const fileName = base.toLowerCase().endsWith(".luckysheet") ? base : `${base}.luckysheet`;
return {
id: row.id,
workspace_id: row.workspace_id ?? targetWorkspaceId,
document_id: row.document_id,
asset_type: "luckysheet",
file_url: null,
thumbnail_url: null,
bucket: null,
storage_path: null,
file_name: fileName,
file_size: null,
mime_type: "application/json",
ocr_payload: undefined,
ocr_strategy: null,
ocr_text: null,
ocr_status: null,
signed_url: null,
created_at: row.created_at ?? "",
updated_at: row.updated_at ?? "",
};
});
const trashedTableAssets: MediaAsset[] = (tables ?? [])
.filter((row) => Boolean(row.is_archived))
.map((row) => {
const base = String(row.title ?? "未命名表格").trim() || "未命名表格";
const fileName = base.toLowerCase().endsWith(".luckysheet") ? base : `${base}.luckysheet`;
return {
id: row.id,
workspace_id: row.workspace_id ?? targetWorkspaceId,
document_id: row.document_id,
asset_type: "luckysheet",
file_url: null,
thumbnail_url: null,
bucket: null,
storage_path: null,
file_name: fileName,
file_size: null,
mime_type: "application/json",
ocr_payload: undefined,
ocr_strategy: null,
ocr_text: null,
ocr_status: null,
deleted_at: row.deleted_at ?? row.updated_at ?? null,
deleted_by: row.deleted_by ?? null,
purged_at: row.purged_at ?? null,
signed_url: null,
created_at: row.created_at ?? "",
updated_at: row.updated_at ?? "",
};
});
const payload: SidebarInitialData = {
activeWorkspaceId: targetWorkspaceId,
workspaces,
@@ -180,10 +244,11 @@ export async function GET(request: Request) {
trashedDocuments,
trashedMediaAssets: (trashedMediaAssets ?? []) as MediaAsset[],
trashedMindmapAssets,
trashedTableAssets,
mindmapDocs,
mindmapAssets,
mindmapAssetChildren,
tableAssets: [],
tableAssets,
mediaAssets: (mediaAssets ?? []) as MediaAsset[],
};
@@ -103,7 +103,8 @@ export async function DELETE(_request: Request, context: RouteContext) {
if (isConvexEnabled()) {
const { auth, client } = await getAuthedConvexClient();
try {
await client.mutation(api.tables.purge, {
// 软删除:进入垃圾桶(可恢复)
await client.mutation(api.tables.remove, {
userId: auth.userId,
tableId,
});
@@ -117,4 +118,3 @@ export async function DELETE(_request: Request, context: RouteContext) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { HttpError, requireAuthContext } from "@/lib/auth/authContext";
import { api } from "@/lib/convex/api";
import { getConvexAuthedHttpClient } from "@/lib/convex/server";
export const dynamic = "force-dynamic";
interface EmptyTrashPayload {
workspaceId?: string;
}
function makeExpiredDeletedAtForForceEmpty(): string {
// “清空附件垃圾桶”是一个显式的强制操作:直接清空所有垃圾桶内的表格,不受 10 分钟宽限期限制。
// Convex 侧按 `deleted_at <= expiredDeletedAt` 做过滤,因此这里给一个很靠后的时间上界即可。
return new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).toISOString();
}
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
let auth;
try {
auth = await requireAuthContext();
} catch (err) {
if (err instanceof HttpError) {
return NextResponse.json({ error: "未登录" }, { status: err.status });
}
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({}));
if (!workspaceId) {
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
}
const client = await getConvexAuthedHttpClient();
try {
const res = await client.mutation(api.tables.emptyTrashByWorkspace, {
userId: auth.userId,
workspaceId,
expiredDeletedAt: makeExpiredDeletedAtForForceEmpty(),
});
return NextResponse.json({ success: true, ...(res as any) });
} catch (err) {
const message = err instanceof Error ? err.message : "清空失败";
return NextResponse.json({ error: message }, { status: 500 });
}
}
@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
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()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
const { tableId } = (await request.json().catch(() => ({}))) as { tableId?: string };
if (!tableId) {
return NextResponse.json({ error: "缺少 tableId" }, { status: 400 });
}
const { auth, client } = await getAuthedConvexClient();
try {
await client.mutation(api.tables.purge, { userId: auth.userId, tableId });
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Convex API error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { isConvexEnabled } from "@/lib/convex/enabled";
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()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
const { tableId } = (await request.json().catch(() => ({}))) as { tableId?: string };
if (!tableId) {
return NextResponse.json({ error: "缺少 tableId" }, { status: 400 });
}
const { auth, client } = await getAuthedConvexClient();
try {
await client.mutation(api.tables.restore, { userId: auth.userId, tableId });
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Convex API error:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}