0.1.14 上线前更改

This commit is contained in:
liaibo
2026-01-11 12:35:53 +08:00
parent be71849aa5
commit 725a60d3aa
44 changed files with 3427 additions and 310 deletions
@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import {
buildClientToolKey,
resolveClientToolCall,
type ClientToolResult,
} from "@/lib/ai-agent/runtime/clientToolBridge";
export const dynamic = "force-dynamic";
type Payload = {
requestId: string;
callId: string;
ok: boolean;
result?: unknown;
error?: string;
};
export async function POST(request: Request) {
const payload = (await request.json().catch(() => null)) as Payload | null;
if (!payload) return NextResponse.json({ error: "缺少请求体" }, { status: 400 });
const requestId = String(payload.requestId ?? "").trim();
const callId = String(payload.callId ?? "").trim();
if (!requestId || !callId) {
return NextResponse.json({ error: "缺少 requestId/callId" }, { status: 400 });
}
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) return NextResponse.json({ error: "未登录" }, { status: 401 });
const result: ClientToolResult = payload.ok
? { ok: true, result: "result" in payload ? payload.result : null }
: { ok: false, error: String(payload.error ?? "客户端工具执行失败") };
const key = buildClientToolKey(requestId, callId);
const resolved = resolveClientToolCall({
key,
userId: session.user.id,
result,
});
if (!resolved.ok) {
return NextResponse.json({ error: resolved.error }, { status: 404 });
}
return NextResponse.json({ ok: true });
}
@@ -12,11 +12,13 @@ import { createRagServerTools } from "@/lib/ai-agent/tools/builtins/rag/lightrag
import { createDocsServerTools, type DocsSupabaseClient } from "@/lib/ai-agent/tools/builtins/docs/docsServerTools";
import { createMediaServerTools, type MediaSupabaseClient } from "@/lib/ai-agent/tools/builtins/media/mediaServerTools";
import { createSlashServerTools, type SlashSupabaseClient } from "@/lib/ai-agent/tools/builtins/slash/slashServerTools";
import { createOnlyOfficeServerTools, type OnlyOfficeSupabaseClient } from "@/lib/ai-agent/tools/builtins/onlyoffice/onlyofficeServerTools";
import { buildClientToolKey, registerClientToolCall } from "@/lib/ai-agent/runtime/clientToolBridge";
export const dynamic = "force-dynamic";
type AgentMessage = { role: "user" | "assistant"; content: string };
type AgentScope = "global" | "mindmap" | "document";
type AgentMessage = { role: "user" | "assistant"; content: string };
type AgentScope = "global" | "mindmap" | "document" | "onlyoffice";
type RequestPayload = {
stream?: boolean;
@@ -36,6 +38,19 @@ type RequestPayload = {
};
const DEFAULT_MAX_STEPS = 10;
const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 60_000;
const makeRunId = () => {
try {
const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto;
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
} catch {
// ignore
}
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
};
const isOnlyOfficeClientTool = (toolId: string) => toolId.startsWith("oo_");
const sseHeaders = {
"Content-Type": "text/event-stream; charset=utf-8",
@@ -90,7 +105,7 @@ export async function POST(request: Request) {
const mindmapId = String(payload.context?.mindmapId ?? "").trim();
const scope: AgentScope = (() => {
const raw = String(payload.scope ?? "").trim();
if (raw === "global" || raw === "mindmap" || raw === "document") return raw;
if (raw === "global" || raw === "mindmap" || raw === "document" || raw === "onlyoffice") return raw;
// 兜底:有 mindmapId 则认为在 mindmap 场景,否则视为全局场景
return mindmapId ? "mindmap" : "global";
})();
@@ -101,7 +116,9 @@ export async function POST(request: Request) {
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.mindmap_read", "toolset.mindmap_write"]
: scope === "document"
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.doc_read", "toolset.doc_write", "toolset.slash_write"]
: ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.slash_write"];
: scope === "onlyoffice"
? ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.onlyoffice_read", "toolset.onlyoffice_write", "toolset.onlyoffice_editor"]
: ["toolset.readonly", "toolset.rag_read", "toolset.media_read", "toolset.docs_read", "toolset.slash_write"];
const allowlist = new Set<string>();
for (const sid of allowToolSetIds) {
const s = registry.toolSetsById.get(sid);
@@ -210,6 +227,15 @@ export async function POST(request: Request) {
})
: null;
const onlyofficeTools =
allowedToolIds.has("asset_extract_outline") || allowedToolIds.has("asset_to_mindmap")
? createOnlyOfficeServerTools({
supabase: supabase as unknown as OnlyOfficeSupabaseClient,
ctx: { userId: session.user.id, documentId: documentId || undefined, attachments },
allowedToolIds,
})
: null;
const slashTools = allowedToolIds.has("slash_run")
? createSlashServerTools({
supabase: supabase as unknown as SlashSupabaseClient,
@@ -219,6 +245,9 @@ export async function POST(request: Request) {
: null;
const runTool = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (isOnlyOfficeClientTool(toolId)) {
throw new Error("oo_* 属于客户端工具:必须使用 stream 模式并在 OnlyOffice 页面内执行");
}
if (toolId === "search_web") {
const query = String(toolArgs.query ?? "").trim();
const count = Number(toolArgs.count ?? 6);
@@ -233,11 +262,15 @@ export async function POST(request: Request) {
return await docsTools.run(toolId, toolArgs);
}
if (toolId === "image_read") {
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
if (!mediaTools) throw new Error(`工具未初始化:${toolId}`);
return await mediaTools.run(toolId, toolArgs);
}
if (toolId.startsWith("asset_")) {
if (!onlyofficeTools) throw new Error(`工具未初始化:${toolId}`);
return await onlyofficeTools.run(toolId, toolArgs);
}
if (toolId === "slash_run") {
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
if (!slashTools) throw new Error(`工具未初始化:${toolId}`);
return await slashTools.run(toolId, toolArgs);
}
if (toolId.startsWith("doc_")) {
@@ -281,7 +314,30 @@ export async function POST(request: Request) {
};
// 先发一个 ready,方便前端快速进入“流式模式”
send("ready", { ok: true });
const requestId = makeRunId();
send("ready", { ok: true, requestId });
let lastToolCall: { id: string; tool: string; args: Record<string, unknown> } | null = null;
const runToolStream = async (toolId: string, toolArgs: Record<string, unknown>) => {
if (isOnlyOfficeClientTool(toolId)) {
const callId = lastToolCall?.tool === toolId ? lastToolCall.id : `call_${Date.now()}`;
const key = buildClientToolKey(requestId, callId);
const wait = registerClientToolCall({
key,
userId: session.user.id,
timeoutMs: DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
});
// 说明:客户端收到该事件后,需要执行插件 API 并回调 /api/ai-agent/client-tool-result
send("client_tool_call", { requestId, callId, tool: toolId, args: toolArgs });
const result = await wait;
if (!result.ok) throw new Error(result.error);
return result.result;
}
return await runTool(toolId, toolArgs);
};
const ping = setInterval(() => {
// 避免某些代理/浏览器长连接超时
@@ -297,12 +353,30 @@ export async function POST(request: Request) {
userMessages: payload.messages.slice(0, 50),
cfg: { ...cfg, model: modelOverride ?? cfg.model },
allowedToolIds,
runTool,
runTool: runToolStream,
maxSteps,
systemContextText,
defaultMindmapTargetUid: selectedUids[0] ?? "",
onEvent: (ev) => {
if (!ev?.type) return;
if (ev.type === "tool_call") {
try {
const d = ev.data as unknown;
const obj =
typeof d === "object" && d
? (d as Record<string, unknown>)
: ({} as Record<string, unknown>);
const id = String(obj.id ?? "").trim();
const tool = String(obj.tool ?? "").trim();
const args =
typeof obj.args === "object" && obj.args
? (obj.args as Record<string, unknown>)
: ({} as Record<string, unknown>);
if (id && tool) lastToolCall = { id, tool, args };
} catch {
// ignore
}
}
send(ev.type, ev.data ?? null);
},
});
@@ -2,6 +2,7 @@ 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";
export const dynamic = "force-dynamic";
@@ -11,6 +12,7 @@ interface BatchPayload {
action: Action;
assetIds: string[];
targetDocumentId?: string;
targetSubPath?: string;
newName?: string;
}
@@ -41,7 +43,7 @@ const parseStoragePath = (fileUrl: string) => {
function resolveAssetLocation(asset: any): { bucket: string; path: string } | null {
if (asset?.storage_path) {
return { bucket: asset.bucket || BUCKET, path: asset.storage_path };
return { bucket: asset.bucket || BUCKET, path: asset.storage_path };
}
if (asset?.file_url) {
return parseStoragePath(asset.file_url);
@@ -49,6 +51,18 @@ function resolveAssetLocation(asset: any): { bucket: string; path: string } | nu
return null;
}
function sanitizeSubPath(input: string | undefined): string {
const raw = typeof input === "string" ? input : "";
const cleaned = raw
.replace(/\\/g, "/")
.split("/")
.map((seg) => seg.trim())
.filter((seg) => Boolean(seg) && seg !== "." && seg !== "..")
.map((seg) => seg.replace(/[\\/]/g, "_"))
.slice(0, 8);
return cleaned.join("/");
}
export async function POST(request: Request) {
const supabase = await createSupabaseRouteClient();
const {
@@ -117,12 +131,21 @@ export async function POST(request: Request) {
typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const targetPath = `${asset.workspace_id}/${asset.document_id}/${Date.now()}-${uniqueId}-${safeName}`;
const baseDir = (() => {
const current = location.path;
const idx = current.lastIndexOf("/");
if (idx > 0) return current.slice(0, idx);
return `${asset.workspace_id}/${asset.document_id}`;
})();
const targetPath = `${baseDir}/${Date.now()}-${uniqueId}-${safeName}`;
const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET;
const moveResult = await supabase.storage.from(bucket).move(location.path, targetPath);
if (moveResult.error) throw moveResult.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { error } = await supabase
.from("media_assets")
.update({
@@ -172,13 +195,18 @@ export async function POST(request: Request) {
typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const targetPath = `${targetDoc.workspace_id}/${payload.targetDocumentId}/${Date.now()}-${uniqueId}-${fileName}`;
const subdir = sanitizeSubPath(payload.targetSubPath);
const prefix = `${targetDoc.workspace_id}/${payload.targetDocumentId}${subdir ? `/${subdir}` : ""}`;
const targetPath = `${prefix}/${Date.now()}-${uniqueId}-${fileName}`;
const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET;
if (payload.action === "copy") {
const copyRes = await supabase.storage.from(bucket).copy(location.path, targetPath);
if (copyRes.error) throw copyRes.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { data: inserted, error } = await supabase
.from("media_assets")
.insert({
@@ -202,7 +230,10 @@ export async function POST(request: Request) {
const moveRes = await supabase.storage.from(bucket).move(location.path, targetPath);
if (moveRes.error) throw moveRes.error;
const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7);
const signedUrl = signed?.signedUrl ?? null;
let signedUrl = signed?.signedUrl ?? null;
if (signedUrl) {
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
const { data: updated, error } = await supabase
.from("media_assets")
.update({
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
@@ -66,6 +67,8 @@ export async function GET(request: Request) {
signedUrl = signedUrlData.signedUrl;
}
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
return NextResponse.json({
signedUrl,
asset: {
@@ -1,8 +1,28 @@
import { NextResponse } from "next/server";
import supabaseAdmin from "@/lib/supabase/admin";
import { rewriteToPublicOrigin } from "@/lib/url/rewritePublicOrigin";
export const dynamic = "force-dynamic";
const applyHostOverride = (rawUrl: string, hostOverride: string) => {
try {
const u = new URL(rawUrl);
// 支持两种写法:hostname 或完整 originhttps://xxx:port
if (/^https?:\/\//i.test(hostOverride)) {
const ov = new URL(hostOverride);
u.protocol = ov.protocol;
u.host = ov.host;
return u.toString();
}
u.hostname = hostOverride;
return u.toString();
} catch {
return rawUrl;
}
};
const parseStoragePath = (fileUrl: string) => {
try {
const url = new URL(fileUrl);
@@ -51,27 +71,22 @@ export async function GET(request: Request) {
const { bucket, path } = parsed;
const { data, error } = await supabaseAdmin.storage
.from(bucket)
.createSignedUrl(path, 60 * 60, { download: fileName });
// OnlyOffice 需要“可被文档服务器拉取”的 URL:不要强制 downloadContent-Disposition: attachment)。
const { data, error } = forOnlyOffice
? await supabaseAdmin.storage.from(bucket).createSignedUrl(path, 60 * 60)
: await supabaseAdmin.storage.from(bucket).createSignedUrl(path, 60 * 60, { download: fileName });
if (error || !data?.signedUrl) {
return NextResponse.json({ error: error?.message ?? "生成签名 URL 失败" }, { status: 500 });
}
let signedUrl = data.signedUrl;
if (
forOnlyOffice &&
hostOverride &&
(signedUrl.includes("127.0.0.1") || signedUrl.includes("localhost"))
) {
try {
const url = new URL(signedUrl);
url.hostname = hostOverride;
signedUrl = url.toString();
} catch {
// ignore parse errors
}
if (forOnlyOffice && hostOverride) {
// 优先按 OnlyOffice 专用回源地址改写(可设置为 http://host.docker.internal:18000 以降低延迟)
signedUrl = applyHostOverride(signedUrl, hostOverride);
} else {
// 返回给浏览器的 URL 必须是公网可达的(不能是 127.0.0.1:18000
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
}
return NextResponse.json({ signedUrl });
@@ -2,6 +2,7 @@ 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";
export const dynamic = "force-dynamic";
@@ -28,6 +29,7 @@ export async function POST(request: Request) {
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 });
@@ -38,7 +40,11 @@ export async function POST(request: Request) {
const buffer = Buffer.from(arrayBuffer);
const extension = extname(file.name || "").replace(/\s+/g, "");
const uniqueId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
// 与 /api/media/batch 的 move/rename 规则对齐:放到 workspaceId/documentId 下
const mindmapId =
mindmapIdRaw && /^[a-zA-Z0-9_-]{1,128}$/.test(mindmapIdRaw) ? mindmapIdRaw : "";
const subdir = mindmapId ? `mindmaps/${mindmapId}` : "";
const path = `${workspaceId}/${documentId}${subdir ? `/${subdir}` : ""}/${Date.now()}-${uniqueId}${extension}`;
const assetType = resolveAssetType(file.type || "");
const { error: uploadError } = await supabase.storage.from(DOC_BUCKET).upload(path, buffer, {
@@ -66,6 +72,8 @@ export async function POST(request: Request) {
signedUrl = signed?.signedUrl ?? "";
}
signedUrl = rewriteToPublicOrigin(signedUrl, process.env.NEXT_PUBLIC_SUPABASE_URL);
const { data: asset, error } = await supabase
.from("media_assets")
.insert({
@@ -6,6 +6,7 @@ import type { SidebarInitialData } from "@/components/sidebar/types";
import {
detectLocalMindmapFiles,
detectLocalMindmapDocs,
detectLocalMindmapImageAssetIdsByMindmapId,
detectLocalTrashedMindmapAssets,
} from "@/lib/mindmap-files";
import type { MediaAsset } from "@/types/media";
@@ -37,6 +38,8 @@ export async function GET(request: Request) {
const dataset = await fetchSidebarDataset(supabase, targetWorkspaceId);
const docIds = dataset.documents.map((d) => d.id);
const localMindmapFiles = await detectLocalMindmapFiles(docIds);
const mindmapAssetChildren =
await detectLocalMindmapImageAssetIdsByMindmapId(localMindmapFiles);
const mindmapDocs = Array.from(new Set([...(dataset.mindmapDocs ?? []), ...(await detectLocalMindmapDocs(docIds))]));
const trashedMindmapAssets = await detectLocalTrashedMindmapAssets(targetWorkspaceId, docIds);
const docById = new Map(dataset.documents.map((d) => [d.id, d]));
@@ -100,6 +103,7 @@ export async function GET(request: Request) {
trashedMindmapAssets,
mindmapDocs,
mindmapAssets,
mindmapAssetChildren,
tableAssets,
mediaAssets: dataset.mediaAssets ?? [],
};
+50 -15
View File
@@ -2,9 +2,13 @@
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<void>((resolve, reject) => {
const existing = document.querySelector(`script[src="${src}"]`);
@@ -34,10 +38,13 @@ const docTypeFromExt = (ext: string) => {
const word = ["doc", "docx", "odt", "rtf"];
const slide = ["ppt", "pptx", "odp"];
const sheet = ["xls", "xlsx", "ods", "csv"];
if (word.includes(ext)) return "text";
if (slide.includes(ext)) return "presentation";
if (sheet.includes(ext)) return "spreadsheet";
return "text";
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() {
@@ -46,27 +53,38 @@ export default function OnlyOfficePage() {
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<string | null>(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 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(fileUrl);
if (
storageHostOverride &&
(u.hostname === "127.0.0.1" ||
u.hostname === "localhost" ||
u.hostname === "host.docker.internal")
) {
u.hostname = storageHostOverride;
const u = new URL(base);
if (!storageHostOverride) return u.toString();
// 兼容两种写法:hostname 或完整 originhttps://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 fileUrl;
return base;
}
}, [fileUrl, storageHostOverride]);
@@ -87,6 +105,7 @@ export default function OnlyOfficePage() {
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%",
@@ -103,6 +122,10 @@ export default function OnlyOfficePage() {
customization: {
feedback: { visible: false },
},
plugins: {
autostart: [MNOTE_AGENT_PLUGIN_GUID],
pluginsData: [pluginConfigUrl],
},
},
});
})
@@ -120,5 +143,17 @@ export default function OnlyOfficePage() {
);
}
return <div id="onlyoffice-frame" className="h-screen w-screen bg-slate-50" />;
return (
<div className="relative h-screen w-screen bg-slate-50">
<div id="onlyoffice-frame" className="h-full w-full" />
<OnlyOfficeAiAgentPanel
openFile={{
id: assetId || `onlyoffice_${hashKey(`${resolvedFileUrl}-${fileName}`)}`,
title: fileName,
fileUrl: resolvedFileUrl,
mimeType: null,
}}
/>
</div>
);
}