import { NextResponse } from "next/server"; import { api } from "@/lib/convex/api"; import { HttpError } from "@/lib/auth/authContext"; import { getAuthedConvexClient } from "@/lib/convex/route"; import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url"; import { documentBridgeErrorResponse } from "@/lib/documents/bridge"; import { prepareOnlyOfficeForcesave } from "@/lib/onlyoffice/rust-adapter"; export const dynamic = "force-dynamic"; export async function POST(request: Request) { const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl(); let auth; let client; try { const authed = await getAuthedConvexClient(); auth = authed.auth; client = authed.client; } 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 = String(searchParams.get("assetId") || "").trim(); const key = String(searchParams.get("key") || "").trim(); if (!assetId) return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); if (!key) return NextResponse.json({ error: "缺少 key" }, { status: 400 }); const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId }); if (!asset) { return NextResponse.json({ error: "资源不存在" }, { status: 404 }); } // 只读权限:不允许触发 forcesave(避免只读用户间接写回存储) const docId = String((asset as any).document_id || "").trim(); if (!docId) { return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); } const doc = await client.query(api.documents.getMeta, { id: docId }); if (!doc || (doc as any).can_edit === false) { return NextResponse.json({ error: "无权修改(只读共享的文件)" }, { status: 403 }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const tryJson = async (res: Response) => (await res.json().catch(() => null)) as any; try { const prepared = await prepareOnlyOfficeForcesave({ request, assetId, key, onlyofficeInternalUrl, secret: process.env.ONLYOFFICE_JWT_SECRET || "", actorId: auth.userId, documentId: docId, workspaceId: String((asset as any).workspace_id || "").trim() || null, }); const errors: Record = {}; for (const outbound of prepared.requests) { const response = await fetch(outbound.url, { method: outbound.method, headers: Object.fromEntries(outbound.headers.map((item) => [item.name, item.value])), body: outbound.bodyJson, }); const payload = await tryJson(response); if (response.ok && Number(payload?.error ?? 0) === 0) { return NextResponse.json({ ok: true, via: outbound.via, result: payload }); } errors[outbound.via] = payload; } return NextResponse.json( { error: "触发 forcesave 失败", detail: errors }, { status: 502 }, ); } catch (error) { if (error instanceof Error && error.name === "DocumentBridgeError") { return documentBridgeErrorResponse(error); } console.error("[onlyoffice/forcesave] failed:", error); return NextResponse.json({ error: "触发 forcesave 失败" }, { status: 502 }); } }