chore: upgrade onlyoffice to 9.2.1
This commit is contained in:
@@ -26,3 +26,9 @@ __pycache__/
|
|||||||
# 大模型文件,防止超出 LFS 配额
|
# 大模型文件,防止超出 LFS 配额
|
||||||
services/mineru/models/
|
services/mineru/models/
|
||||||
*.safetensors
|
*.safetensors
|
||||||
|
# ONLYOFFICE 挂载/同步目录(体积很大,禁止入库)
|
||||||
|
/onlyoffice-web-apps/
|
||||||
|
/onlyoffice-plugins/
|
||||||
|
/onlyoffice-data/
|
||||||
|
/onlyoffice-fonts/
|
||||||
|
/onlyoffice-pg-data/
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
onlyoffice-dev:
|
||||||
|
image: onlyoffice/documentserver:9.2.1
|
||||||
|
container_name: onlyoffice-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8081:80" # 本地访问 http://localhost:8081
|
||||||
|
environment:
|
||||||
|
- JWT_ENABLED=false # 关闭内置 JWT,改用外部 Supabase JWT
|
||||||
|
- DOCUMENT_SERVER_JWT_ENABLED=false
|
||||||
|
- LANG=zh_CN.UTF-8
|
||||||
|
- LC_ALL=zh_CN.UTF-8
|
||||||
|
- ALLOW_PRIVATE_IP_ADDRESS=true # 允许访问宿主机私网地址(Supabase 签名 URL)
|
||||||
|
- DB_TYPE=postgres
|
||||||
|
- DB_HOST=onlyoffice-postgres
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_NAME=onlyoffice
|
||||||
|
- DB_USER=onlyoffice
|
||||||
|
- DB_PWD=onlyoffice
|
||||||
|
volumes:
|
||||||
|
- ./onlyoffice-web-apps:/var/www/onlyoffice/documentserver/web-apps # 前端源码,可直接改工具栏/菜单
|
||||||
|
- ./onlyoffice-plugins:/var/www/onlyoffice/documentserver/sdkjs-plugins # 插件源码,支持热更新
|
||||||
|
- ./onlyoffice-data:/var/www/onlyoffice/Data # 用户数据、缓存
|
||||||
|
- ./onlyoffice-fonts:/usr/share/fonts/truetype/custom # 自定义字体(可选)
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway" # 方便插件/前端直连宿主机 FastAPI/LightRAG
|
||||||
|
depends_on:
|
||||||
|
- onlyoffice-postgres
|
||||||
|
|
||||||
|
onlyoffice-postgres:
|
||||||
|
image: postgres:14
|
||||||
|
container_name: onlyoffice-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=onlyoffice
|
||||||
|
- POSTGRES_USER=onlyoffice
|
||||||
|
- POSTGRES_PASSWORD=onlyoffice
|
||||||
|
volumes:
|
||||||
|
- onlyoffice-pg-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "55432:5432"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: onlyoffice-dev-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
onlyoffice-pg-data:
|
||||||
+28
-1
@@ -16,6 +16,7 @@ const { spawn } = require("child_process");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const net = require("net");
|
const net = require("net");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
const rootDir = path.resolve(__dirname, "..");
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
const frontendDir = path.join(rootDir, "wolai-frontend");
|
const frontendDir = path.join(rootDir, "wolai-frontend");
|
||||||
@@ -47,6 +48,32 @@ const tasks = [
|
|||||||
const children = [];
|
const children = [];
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
|
|
||||||
|
function loadEnvFile(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) return {};
|
||||||
|
const content = fs.readFileSync(filePath, "utf8");
|
||||||
|
return content
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter((line) => line.trim() && !line.trim().startsWith("#"))
|
||||||
|
.reduce((acc, line) => {
|
||||||
|
const idx = line.indexOf("=");
|
||||||
|
if (idx === -1) return acc;
|
||||||
|
const key = line.slice(0, idx).trim();
|
||||||
|
const value = line.slice(idx + 1).trim();
|
||||||
|
acc[key] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFromRoot = loadEnvFile(path.join(rootDir, ".env.local"));
|
||||||
|
const envFromFrontend = loadEnvFile(path.join(frontendDir, ".env.local"));
|
||||||
|
const envFromBackend = loadEnvFile(path.join(backendDir, ".env.local"));
|
||||||
|
const mergedEnv = {
|
||||||
|
...process.env,
|
||||||
|
...envFromRoot,
|
||||||
|
...envFromFrontend,
|
||||||
|
...envFromBackend,
|
||||||
|
};
|
||||||
|
|
||||||
function logPrefix(name, message) {
|
function logPrefix(name, message) {
|
||||||
console.log(`[${name}] ${message}`);
|
console.log(`[${name}] ${message}`);
|
||||||
}
|
}
|
||||||
@@ -57,7 +84,7 @@ function startTask(task) {
|
|||||||
cwd: task.cwd,
|
cwd: task.cwd,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: true,
|
shell: true,
|
||||||
env: { ...process.env },
|
env: mergedEnv,
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("exit", (code, signal) => {
|
child.on("exit", (code, signal) => {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import supabaseAdmin from "@/lib/supabase/admin";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const parseStoragePath = (fileUrl: string) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(fileUrl);
|
||||||
|
const segments = url.pathname.split("/").filter(Boolean);
|
||||||
|
const objectIdx = segments.findIndex((seg) => seg === "object");
|
||||||
|
if (objectIdx === -1 || objectIdx + 2 >= segments.length) return null;
|
||||||
|
// pattern 1: /storage/v1/object/public/<bucket>/<path...>
|
||||||
|
if (segments[objectIdx + 1] === "public") {
|
||||||
|
const bucket = segments[objectIdx + 2];
|
||||||
|
const path = segments.slice(objectIdx + 3).join("/");
|
||||||
|
return { bucket, path };
|
||||||
|
}
|
||||||
|
// pattern 2: /storage/v1/object/sign/<bucket>/<path...> (token in query)
|
||||||
|
if (segments[objectIdx + 1] === "sign") {
|
||||||
|
const bucket = segments[objectIdx + 2];
|
||||||
|
const path = segments.slice(objectIdx + 3).join("/");
|
||||||
|
return { bucket, path };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const fileUrl = searchParams.get("fileUrl");
|
||||||
|
const fileName = searchParams.get("fileName") ?? undefined;
|
||||||
|
const forOnlyOffice = searchParams.get("for") === "onlyoffice";
|
||||||
|
const hostOverride = process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE;
|
||||||
|
if (searchParams.get("debug") === "1") {
|
||||||
|
return NextResponse.json({
|
||||||
|
keyLen: (process.env.SUPABASE_SERVICE_ROLE_KEY || "").length,
|
||||||
|
url: process.env.SUPABASE_URL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileUrl) {
|
||||||
|
return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseStoragePath(fileUrl);
|
||||||
|
if (!parsed) {
|
||||||
|
return NextResponse.json({ error: "无法解析 Supabase 存储路径" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { bucket, path } = parsed;
|
||||||
|
|
||||||
|
const { data, error } = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ signedUrl });
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { extname } from "path";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const MEDIA_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_MEDIA_BUCKET ?? "media";
|
const DOC_BUCKET = process.env.NEXT_PUBLIC_SUPABASE_DOC_BUCKET ?? "documents";
|
||||||
|
|
||||||
const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" => {
|
const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" => {
|
||||||
if (mime.startsWith("image/")) return "image";
|
if (mime.startsWith("image/")) return "image";
|
||||||
@@ -41,7 +41,7 @@ export async function POST(request: Request) {
|
|||||||
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
|
const path = `${workspaceId}/${Date.now()}-${uniqueId}${extension}`;
|
||||||
const assetType = resolveAssetType(file.type || "");
|
const assetType = resolveAssetType(file.type || "");
|
||||||
|
|
||||||
const { error: uploadError } = await supabase.storage.from(MEDIA_BUCKET).upload(path, buffer, {
|
const { error: uploadError } = await supabase.storage.from(DOC_BUCKET).upload(path, buffer, {
|
||||||
contentType: file.type,
|
contentType: file.type,
|
||||||
upsert: false,
|
upsert: false,
|
||||||
});
|
});
|
||||||
@@ -50,17 +50,17 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ error: uploadError.message }, { status: 500 });
|
return NextResponse.json({ error: uploadError.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
// 为私有桶生成临时访问链接(7 天);前端可在需要时通过 /api/media/signed-url 刷新
|
||||||
data: { publicUrl },
|
const { data: signed } = await supabase.storage.from(DOC_BUCKET).createSignedUrl(path, 60 * 60 * 24 * 7);
|
||||||
} = supabase.storage.from(MEDIA_BUCKET).getPublicUrl(path);
|
const signedUrl = signed?.signedUrl ?? "";
|
||||||
|
|
||||||
const { data: asset, error } = await supabase
|
const { data: asset, error } = await supabase
|
||||||
.from("media_assets")
|
.from("media_assets")
|
||||||
.insert({
|
.insert({
|
||||||
workspace_id: workspaceId,
|
workspace_id: workspaceId,
|
||||||
document_id: documentId,
|
document_id: documentId,
|
||||||
file_url: publicUrl,
|
file_url: signedUrl,
|
||||||
thumbnail_url: publicUrl,
|
thumbnail_url: signedUrl,
|
||||||
asset_type: assetType,
|
asset_type: assetType,
|
||||||
file_name: file.name,
|
file_name: file.name,
|
||||||
file_size: file.size,
|
file_size: file.size,
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
type EditorMode = "view" | "edit";
|
||||||
|
|
||||||
|
const loadScript = (src: string) =>
|
||||||
|
new Promise<void>((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"];
|
||||||
|
if (word.includes(ext)) return "text";
|
||||||
|
if (slide.includes(ext)) return "presentation";
|
||||||
|
if (sheet.includes(ext)) return "spreadsheet";
|
||||||
|
return "text";
|
||||||
|
};
|
||||||
|
|
||||||
|
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 [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 resolvedFileUrl = useMemo(() => {
|
||||||
|
if (!fileUrl) return "";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return u.toString();
|
||||||
|
} catch {
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
}, [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
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
setError(err.message);
|
||||||
|
});
|
||||||
|
}, [baseUrl, fileName, fileType, mode, resolvedFileUrl, targetDocType]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col items-center justify-center gap-3 bg-slate-50">
|
||||||
|
<p className="text-base font-semibold text-red-600">ONLYOFFICE 加载失败</p>
|
||||||
|
<p className="text-sm text-gray-600">{error}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div id="onlyoffice-frame" className="h-screen w-screen bg-slate-50" />;
|
||||||
|
}
|
||||||
@@ -94,7 +94,20 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
|||||||
const displayFileName = block.props.fileName ?? deriveFileName(fileUrl);
|
const displayFileName = block.props.fileName ?? deriveFileName(fileUrl);
|
||||||
const captionRef = useRef<HTMLInputElement | null>(null);
|
const captionRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [captionEditing, setCaptionEditing] = useState(false);
|
const [captionEditing, setCaptionEditing] = useState(false);
|
||||||
const shouldShowCaption = captionEditing || Boolean(block.props.caption);
|
const shouldShowCaption = captionEditing || Boolean(block.props.caption);
|
||||||
|
const officeBase = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL;
|
||||||
|
const extension = useMemo(() => {
|
||||||
|
const name = (block.props.fileName || deriveFileName(fileUrl)).toLowerCase();
|
||||||
|
const match = /\.([a-z0-9]+)$/.exec(name);
|
||||||
|
return match?.[1] ?? "";
|
||||||
|
}, [block.props.fileName, fileUrl]);
|
||||||
|
const isOfficeDoc = useMemo(
|
||||||
|
() =>
|
||||||
|
["doc", "docx", "ppt", "pptx", "xls", "xlsx", "odp", "ods", "odt"].includes(
|
||||||
|
extension,
|
||||||
|
),
|
||||||
|
[extension],
|
||||||
|
);
|
||||||
|
|
||||||
const handleChoose = () => {
|
const handleChoose = () => {
|
||||||
openPicker({
|
openPicker({
|
||||||
@@ -207,11 +220,38 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
|||||||
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openWithOnlyOffice = async () => {
|
||||||
|
if (!fileUrl) return;
|
||||||
|
if (!officeBase) {
|
||||||
|
window.alert("缺少 ONLYOFFICE 地址,请在 .env.local 配置 NEXT_PUBLIC_ONLYOFFICE_BASE_URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/media/signed-url?fileUrl=${encodeURIComponent(fileUrl)}&fileName=${encodeURIComponent(
|
||||||
|
displayFileName,
|
||||||
|
)}&for=onlyoffice`,
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const payload = await res.json().catch(() => null);
|
||||||
|
throw new Error(payload?.error ?? "生成签名链接失败");
|
||||||
|
}
|
||||||
|
const { signedUrl } = (await res.json()) as { signedUrl: string };
|
||||||
|
const target = new URL("/onlyoffice", window.location.origin);
|
||||||
|
target.searchParams.set("fileUrl", signedUrl);
|
||||||
|
target.searchParams.set("fileName", displayFileName);
|
||||||
|
target.searchParams.set("fileType", extension || "docx");
|
||||||
|
window.open(target.toString(), "_blank", "noopener,noreferrer");
|
||||||
|
} catch (error) {
|
||||||
|
window.alert((error as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const downloadAsset = () => {
|
const downloadAsset = () => {
|
||||||
if (!fileUrl) return;
|
if (!fileUrl) return;
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.href = fileUrl;
|
anchor.href = fileUrl;
|
||||||
anchor.download = block.props.fileName || block.props.caption || typeLabel;
|
anchor.download = block.props.fileName || block.props.caption || typeLabel;
|
||||||
anchor.click();
|
anchor.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,7 +318,7 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
|||||||
if (assetType === "file") {
|
if (assetType === "file") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 rounded-2xl border border-gray-200 bg-white p-4 shadow-sm">
|
<div className="flex items-center gap-3 rounded-2xl border border-gray-200 bg-white p-4 shadow-sm">
|
||||||
<span className="rounded-full bg-[#2563eb]/10 p-3 text-[#2563eb]">
|
<span className="rounded-full bg-[#2563eb]/10 p-3 text-[#2563eb]">
|
||||||
<Paperclip className="h-5 w-5" />
|
<Paperclip className="h-5 w-5" />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -361,7 +401,17 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wolai-media" ref={mediaRef}>
|
<div className="wolai-media" ref={mediaRef}>
|
||||||
<div className="wolai-media__canvas" style={resolvedWidth ? { width: `${resolvedWidth}px` } : undefined}>
|
<div
|
||||||
|
className="wolai-media__canvas"
|
||||||
|
style={resolvedWidth ? { width: `${resolvedWidth}px` } : undefined}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
if (assetType === "file" && isOfficeDoc) {
|
||||||
|
void openWithOnlyOffice();
|
||||||
|
} else if (assetType === "file") {
|
||||||
|
viewOriginal();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{block.props.linkUrl ? (
|
{block.props.linkUrl ? (
|
||||||
<a href={block.props.linkUrl} target="_blank" rel="noopener noreferrer">
|
<a href={block.props.linkUrl} target="_blank" rel="noopener noreferrer">
|
||||||
{figure}
|
{figure}
|
||||||
@@ -410,6 +460,9 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
|||||||
<DropdownMenuItem onClick={handleLink}>{dropdownLinkLabel}</DropdownMenuItem>
|
<DropdownMenuItem onClick={handleLink}>{dropdownLinkLabel}</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handleCopyLink(fileUrl)}>复制链接</DropdownMenuItem>
|
<DropdownMenuItem onClick={() => handleCopyLink(fileUrl)}>复制链接</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={viewOriginal}>查看原文件</DropdownMenuItem>
|
<DropdownMenuItem onClick={viewOriginal}>查看原文件</DropdownMenuItem>
|
||||||
|
{assetType === "file" && isOfficeDoc && (
|
||||||
|
<DropdownMenuItem onClick={() => void openWithOnlyOffice()}>使用 ONLYOFFICE 打开</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem onClick={downloadAsset}>下载到本地</DropdownMenuItem>
|
<DropdownMenuItem onClick={downloadAsset}>下载到本地</DropdownMenuItem>
|
||||||
{canTriggerOcr && (
|
{canTriggerOcr && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
|
const supabaseAdmin = createClient(
|
||||||
|
process.env.SUPABASE_URL ?? "",
|
||||||
|
process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
autoRefreshToken: false,
|
||||||
|
persistSession: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default supabaseAdmin;
|
||||||
Reference in New Issue
Block a user