0.2.1 onlyoffice修复
This commit is contained in:
@@ -1,10 +1,46 @@
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = searchParams.get("workspaceId");
|
||||
const limit = Number.parseInt(searchParams.get("limit") ?? "12", 10);
|
||||
const assetType = searchParams.get("assetType") ?? undefined;
|
||||
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = getConvexHttpClient();
|
||||
const items = await client.query(api.mediaAssets.listByWorkspace, {
|
||||
userId: auth.userId,
|
||||
workspaceId,
|
||||
assetType,
|
||||
includeDeleted: false,
|
||||
limit: Number.isNaN(limit) ? 12 : limit,
|
||||
});
|
||||
|
||||
return NextResponse.json({ items: (items ?? []) as MediaAsset[] });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { user },
|
||||
@@ -46,6 +82,75 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload = (await request.json()) as {
|
||||
workspaceId: string;
|
||||
documentId: string;
|
||||
fileUrl: string;
|
||||
thumbnailUrl?: string;
|
||||
assetType?: string;
|
||||
fileName?: string;
|
||||
fileSize?: number;
|
||||
mimeType?: string;
|
||||
};
|
||||
|
||||
if (!payload.workspaceId || !payload.documentId || !payload.fileUrl) {
|
||||
return NextResponse.json({ error: "参数不完整" }, { status: 400 });
|
||||
}
|
||||
|
||||
const assetId =
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const asset: MediaAsset = {
|
||||
id: assetId,
|
||||
workspace_id: payload.workspaceId,
|
||||
document_id: payload.documentId,
|
||||
file_url: payload.fileUrl,
|
||||
thumbnail_url: payload.thumbnailUrl ?? payload.fileUrl,
|
||||
asset_type: payload.assetType ?? "image",
|
||||
file_name: payload.fileName ?? null,
|
||||
file_size: payload.fileSize ?? null,
|
||||
mime_type: payload.mimeType ?? null,
|
||||
ocr_text: null,
|
||||
ocr_status: null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
const client = getConvexHttpClient();
|
||||
await client.mutation(api.mediaAssets.create, {
|
||||
userId: auth.userId,
|
||||
asset: {
|
||||
id: asset.id,
|
||||
workspace_id: asset.workspace_id,
|
||||
document_id: asset.document_id,
|
||||
asset_type: asset.asset_type,
|
||||
file_url: asset.file_url,
|
||||
thumbnail_url: asset.thumbnail_url,
|
||||
bucket: null,
|
||||
storage_path: null,
|
||||
file_name: asset.file_name,
|
||||
file_size: asset.file_size,
|
||||
mime_type: asset.mime_type,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ asset });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { extname } from "path";
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -65,6 +68,151 @@ function sanitizeSubPath(input: string | undefined): string {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const payload = (await request.json()) as BatchPayload;
|
||||
if (!payload?.action || !payload.assetIds?.length) {
|
||||
return NextResponse.json({ error: "缺少参数" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = getConvexHttpClient();
|
||||
const assets = (await client.query(api.mediaAssets.listByIds, {
|
||||
userId: auth.userId,
|
||||
ids: payload.assetIds,
|
||||
})) as any[];
|
||||
|
||||
if (!assets?.length) {
|
||||
return NextResponse.json({ error: "未找到附件" }, { status: 404 });
|
||||
}
|
||||
|
||||
const nowIso = () => new Date().toISOString();
|
||||
|
||||
try {
|
||||
switch (payload.action) {
|
||||
case "delete": {
|
||||
for (const a of assets) {
|
||||
await client.mutation(api.mediaAssets.patchById, {
|
||||
userId: auth.userId,
|
||||
id: String(a.id),
|
||||
patch: { deleted_at: nowIso(), deleted_by: auth.userId },
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
case "restore": {
|
||||
for (const a of assets) {
|
||||
await client.mutation(api.mediaAssets.patchById, {
|
||||
userId: auth.userId,
|
||||
id: String(a.id),
|
||||
patch: { deleted_at: null, deleted_by: null, purged_at: null },
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
case "rename": {
|
||||
if (payload.assetIds.length !== 1 || !payload.newName) {
|
||||
return NextResponse.json({ error: "重命名需要单个文件与新名称" }, { status: 400 });
|
||||
}
|
||||
const asset = assets[0];
|
||||
const currentName = String(asset.file_name ?? "");
|
||||
const ext = currentName.includes(".") ? `.${currentName.split(".").pop()}` : "";
|
||||
const newFileName = payload.newName.includes(".") || !ext ? payload.newName : `${payload.newName}${ext}`;
|
||||
const safeName = newFileName.replace(/[\\/]/g, "_");
|
||||
|
||||
await client.mutation(api.mediaAssets.patchById, {
|
||||
userId: auth.userId,
|
||||
id: String(asset.id),
|
||||
patch: { file_name: safeName },
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
case "copy":
|
||||
case "move": {
|
||||
if (!payload.targetDocumentId) {
|
||||
return NextResponse.json({ error: "缺少目标页面" }, { status: 400 });
|
||||
}
|
||||
|
||||
const targetDoc = await client.query(api.documents.getMeta, {
|
||||
userId: auth.userId,
|
||||
id: payload.targetDocumentId,
|
||||
});
|
||||
|
||||
if (!targetDoc) {
|
||||
return NextResponse.json({ error: "目标页面不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const existing = (await client.query(api.mediaAssets.listByDocument, {
|
||||
userId: auth.userId,
|
||||
documentId: payload.targetDocumentId,
|
||||
limit: 500,
|
||||
})) as any[];
|
||||
const existingNames = new Set<string>(
|
||||
(existing ?? []).map((r) => (r?.file_name ?? "").toString()).filter(Boolean),
|
||||
);
|
||||
|
||||
const results: any[] = [];
|
||||
|
||||
for (const asset of assets) {
|
||||
const fileName = makeUniqueFileName(asset.file_name ?? "附件", existingNames).replace(/[\\/]/g, "_");
|
||||
const storageId = (asset as { storage_id?: string | null })?.storage_id ?? null;
|
||||
if (!storageId) continue;
|
||||
|
||||
if (payload.action === "copy") {
|
||||
const newId =
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const created = await client.mutation(api.mediaAssets.createWithStorage, {
|
||||
userId: auth.userId,
|
||||
storageId: storageId as any,
|
||||
asset: {
|
||||
id: newId,
|
||||
workspace_id: String(targetDoc.workspace_id),
|
||||
document_id: String(payload.targetDocumentId),
|
||||
asset_type: String(asset.asset_type ?? "file"),
|
||||
file_name: fileName,
|
||||
file_size: typeof asset.file_size === "number" ? asset.file_size : null,
|
||||
mime_type: (asset.mime_type ?? null) as any,
|
||||
},
|
||||
});
|
||||
|
||||
results.push(created);
|
||||
} else {
|
||||
await client.mutation(api.mediaAssets.patchById, {
|
||||
userId: auth.userId,
|
||||
id: String(asset.id),
|
||||
patch: {
|
||||
workspace_id: String(targetDoc.workspace_id),
|
||||
document_id: String(payload.targetDocumentId),
|
||||
file_name: fileName,
|
||||
},
|
||||
});
|
||||
|
||||
results.push({ ...asset, workspace_id: String(targetDoc.workspace_id), document_id: String(payload.targetDocumentId), file_name: fileName });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ items: results });
|
||||
}
|
||||
default:
|
||||
return NextResponse.json({ error: "不支持的操作" }, { status: 400 });
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "操作失败";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -25,6 +29,32 @@ function makeExpiredDeletedAt(): string {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = 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 = getConvexHttpClient();
|
||||
const res = await client.mutation(api.mediaAssets.emptyTrashByWorkspace, {
|
||||
userId: auth.userId,
|
||||
workspaceId,
|
||||
expiredDeletedAt: makeExpiredDeletedAt(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, ...(res as any) });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
@@ -85,4 +115,3 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ success: true, updated: assetIds.length });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
// 说明:OCR 链路目前仍依赖 Supabase JWT/表结构,迁移阶段先显式禁用,避免 UI/接口误用。
|
||||
return NextResponse.json({ error: "Convex 模式暂不支持 OCR" }, { status: 501 });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -25,6 +29,32 @@ function makeExpiredDeletedAt(): string {
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { assetId }: PurgePayload = await request.json().catch(() => ({}));
|
||||
if (!assetId) {
|
||||
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const client = getConvexHttpClient();
|
||||
const res = await client.mutation(api.mediaAssets.purgeById, {
|
||||
userId: auth.userId,
|
||||
id: assetId,
|
||||
expiredDeletedAt: makeExpiredDeletedAt(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, ...(res as any) });
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
@@ -86,4 +116,3 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ 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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -61,6 +65,47 @@ const resolveAssetObjectLocation = async (params: {
|
||||
};
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
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 client = getConvexHttpClient();
|
||||
const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId });
|
||||
if (!asset) {
|
||||
return NextResponse.json({ error: "资源不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
const refreshed = await client.mutation(api.mediaAssets.refreshUrl, { userId: auth.userId, id: assetId });
|
||||
const signedUrl = (refreshed as { signedUrl?: string | null } | null)?.signedUrl ?? null;
|
||||
if (!signedUrl) {
|
||||
return NextResponse.json({ error: "生成签名链接失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
signedUrl,
|
||||
asset: {
|
||||
id: asset.id,
|
||||
file_name: asset.file_name,
|
||||
mime_type: asset.mime_type,
|
||||
file_size: asset.file_size,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -3,6 +3,8 @@ 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";
|
||||
|
||||
@@ -92,6 +94,26 @@ const tryResolveExistingObjectPath = async (params: {
|
||||
};
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
// 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。
|
||||
// 由于外链本身已经是可访问的 URL,这里只做最小透传。
|
||||
try {
|
||||
requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileUrl = searchParams.get("fileUrl");
|
||||
if (!fileUrl) {
|
||||
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json({ signedUrl: fileUrl });
|
||||
}
|
||||
|
||||
// 说明:在 Cloudflare Tunnel 场景下,后端收到的 Host 可能是 localhost,
|
||||
// 但 ONLYOFFICE 文档服务器拉取 document.url 时必须使用公网可达的域名。
|
||||
// 因此这里优先使用运行时配置(public/mnote-env.json / env)里的公网 Origin,
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { MediaAsset } from "@/types/media";
|
||||
import { extname } from "path";
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -17,6 +21,86 @@ const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" =>
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (isConvexEnabled()) {
|
||||
let auth;
|
||||
try {
|
||||
auth = requireAuthContext();
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: err.status });
|
||||
}
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file");
|
||||
const workspaceId = String(formData.get("workspaceId") ?? "");
|
||||
const documentId = String(formData.get("documentId") ?? "");
|
||||
const mindmapIdRaw = String(formData.get("mindmapId") ?? "").trim();
|
||||
|
||||
if (!(file instanceof File) || !workspaceId || !documentId) {
|
||||
return NextResponse.json({ error: "缺少必要参数" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const assetId =
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
const assetType = resolveAssetType(file.type || "");
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
// 1) 获取 Convex 的上传 URL(短时有效)
|
||||
const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId: auth.userId });
|
||||
if (!uploadUrl || typeof uploadUrl !== "string") {
|
||||
return NextResponse.json({ error: "获取上传地址失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
// 2) 上传文件到 Convex Files
|
||||
const uploadRes = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type || "application/octet-stream" },
|
||||
body: buffer,
|
||||
});
|
||||
if (!uploadRes.ok) {
|
||||
const text = await uploadRes.text().catch(() => "");
|
||||
return NextResponse.json({ error: `上传到 Convex 失败:${uploadRes.status} ${text}` }, { status: 500 });
|
||||
}
|
||||
const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null;
|
||||
const storageId = uploadJson?.storageId ?? "";
|
||||
if (!storageId) {
|
||||
return NextResponse.json({ error: "上传到 Convex 失败:缺少 storageId" }, { status: 500 });
|
||||
}
|
||||
|
||||
// 3) 写入 Convex 的 media_assets 元数据(并记录 storageId)
|
||||
const created = await client.mutation(api.mediaAssets.createWithStorage, {
|
||||
userId: auth.userId,
|
||||
storageId: storageId as any,
|
||||
asset: {
|
||||
id: assetId,
|
||||
workspace_id: workspaceId,
|
||||
document_id: documentId,
|
||||
asset_type: assetType,
|
||||
file_name: file.name || null,
|
||||
file_size: file.size,
|
||||
mime_type: file.type || null,
|
||||
},
|
||||
});
|
||||
|
||||
const asset = created as unknown as MediaAsset;
|
||||
|
||||
return NextResponse.json({
|
||||
asset,
|
||||
mindmapUrl: `asset:${asset.id}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: "上传失败" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
|
||||
Reference in New Issue
Block a user