"use client"; import { useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel"; import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin"; type EditorMode = "view" | "edit"; const MNOTE_AGENT_PLUGIN_GUID = "asc.{F2B9A7B4-3A22-4E0E-8B32-3B0DE7AE8A60}"; const loadScript = (src: string) => new Promise((resolve, reject) => { const existing = document.querySelector(`script[src="${src}"]`); if (existing) { existing.addEventListener("load", () => resolve(), { once: true }); resolve(); return; } const script = document.createElement("script"); script.src = src; script.async = true; script.onload = () => resolve(); script.onerror = () => reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`)); document.body.appendChild(script); }); const hashKey = (input: string) => { let hash = 0; for (let i = 0; i < input.length; i += 1) { hash = (hash << 5) - hash + input.charCodeAt(i); hash |= 0; } return Math.abs(hash).toString(); }; const docTypeFromExt = (ext: string) => { const word = ["doc", "docx", "odt", "rtf"]; const slide = ["ppt", "pptx", "odp"]; const sheet = ["xls", "xlsx", "ods", "csv"]; const pdf = ["pdf"]; // 说明:ONLYOFFICE 文档类型使用 word/cell/slide/pdf(旧的 text/spreadsheet/presentation 已逐步弃用) if (word.includes(ext)) return "word"; if (slide.includes(ext)) return "slide"; if (sheet.includes(ext)) return "cell"; if (pdf.includes(ext)) return "pdf"; return "word"; }; export default function OnlyOfficePage() { const params = useSearchParams(); const fileUrl = params.get("fileUrl") ?? ""; const fileName = params.get("fileName") ?? "未命名文档"; const fileType = (params.get("fileType") ?? "docx").toLowerCase(); const mode = (params.get("mode") ?? "edit") as EditorMode; const assetId = params.get("assetId") ?? ""; const [error, setError] = useState(null); const baseUrl = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL; const storageHostOverride = process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE; const targetDocType = useMemo(() => docTypeFromExt(fileType), [fileType]); const resolvedFileUrl = useMemo(() => { if (!fileUrl) return ""; // 说明:OnlyOffice 的 document.url 由“文档服务器”拉取(不是浏览器)。 // 如果我们已经配置了专用回源(storageHostOverride),就不要再把 URL 改写成公网, // 否则会把 http://host.docker.internal:18000 错误改成 https://host.docker.internal:18000, // 导致 ONLYOFFICE 报 “下载失败(EPROTO wrong version number)”。 const base = storageHostOverride ? fileUrl : rewriteToPublicOrigin(fileUrl, process.env.NEXT_PUBLIC_SUPABASE_URL); try { const u = new URL(base); if (!storageHostOverride) return u.toString(); // 兼容两种写法:hostname 或完整 origin(https://xxx) if (/^https?:\/\//i.test(storageHostOverride)) { const ov = new URL(storageHostOverride); u.protocol = ov.protocol; u.host = ov.host; return u.toString(); } u.hostname = storageHostOverride; return u.toString(); } catch { return base; } }, [fileUrl, storageHostOverride]); useEffect(() => { if (!baseUrl) { setError("缺少 NEXT_PUBLIC_ONLYOFFICE_BASE_URL 配置,无法加载编辑器。"); return; } if (!fileUrl) { setError("缺少 fileUrl 参数。"); return; } const scriptUrl = `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`; loadScript(scriptUrl) .then(() => { // @ts-expect-error ONLYOFFICE 全局对象 if (!window.DocsAPI) { throw new Error("未检测到 DocsAPI,请检查 ONLYOFFICE 版本。"); } // eslint-disable-next-line new-cap,@typescript-eslint/no-explicit-any const pluginConfigUrl = `${window.location.origin}/onlyoffice/plugins/agent-tools/config.json`; new (window as any).DocsAPI.DocEditor("onlyoffice-frame", { width: "100%", height: "100%", document: { fileType, title: fileName, url: resolvedFileUrl, key: hashKey(`${resolvedFileUrl}-${fileName}`), }, documentType: targetDocType, editorConfig: { mode: mode === "view" ? "view" : "edit", lang: "zh-CN", customization: { feedback: { visible: false }, }, plugins: { autostart: [MNOTE_AGENT_PLUGIN_GUID], pluginsData: [pluginConfigUrl], }, }, }); }) .catch((err: Error) => { setError(err.message); }); }, [baseUrl, fileName, fileType, mode, resolvedFileUrl, targetDocType]); if (error) { return (

ONLYOFFICE 加载失败

{error}

); } return (
); }