diff --git a/web/src/components/canvas/canvas-assistant-panel.tsx b/web/src/components/canvas/canvas-assistant-panel.tsx
index c4aae40ad..9d7d11358 100644
--- a/web/src/components/canvas/canvas-assistant-panel.tsx
+++ b/web/src/components/canvas/canvas-assistant-panel.tsx
@@ -936,13 +936,25 @@ function AgentModelIcon({ model }: { model: string }) {
}
function resolveModelIcon(model: string) {
+ // 与 model-picker 保持同一套厂商图标规则。
const name = model.toLowerCase();
if (name.includes("claude") || name.includes("anthropic")) return "/icons/claude.svg";
- if (name.includes("gemini") || name.includes("google")) return "/icons/gemini.svg";
- if (name.includes("gpt") || name.includes("openai")) return "/icons/openai.svg";
+ if (
+ name.includes("gemini") ||
+ name.includes("google") ||
+ name.includes("nano banana") ||
+ name.includes("nanobanana") ||
+ name.includes("imagen") ||
+ name.includes("veo") ||
+ name.includes("omni flash") ||
+ name.includes("omni-flash")
+ ) {
+ return "/icons/gemini.svg";
+ }
+ if (name.includes("gpt") || name.includes("openai") || name.includes("dall-e") || name.includes("dalle")) return "/icons/openai.svg";
if (name.includes("grok")) return "/icons/grok.svg";
if (name.includes("deepseek")) return "/icons/deepseek.svg";
- if (name.includes("glm")) return "/icons/glm.svg";
+ if (name.includes("glm") || name.includes("chatglm")) return "/icons/glm.svg";
return "";
}
diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx
index 3695dfb10..f77d99055 100644
--- a/web/src/components/model-picker.tsx
+++ b/web/src/components/model-picker.tsx
@@ -136,13 +136,15 @@ function ModelPrice({ price, compact = false }: { price: { value: number; unit:
function modelMenuMeta(model: string, capability?: ModelCapability): { description: string; time?: string } {
const name = modelOptionName(model).toLowerCase();
if (capability === "image") {
+ if (name.includes("nano banana") || name.includes("nanobanana") || name.includes("imagen")) return { description: "Gemini 高质量图片生成,适合角色和商业成片" };
if (name.includes("nano") || name.includes("pro")) return { description: "高质量图片生成,适合角色和商业成片" };
if (name.includes("seedream")) return { description: "快速出图,适合批量探索风格" };
if (name.includes("gpt") || name.includes("image")) return { description: "通用图片模型,提示词理解稳定" };
return { description: "图片生成模型" };
}
if (capability === "video") {
- if (name.includes("seedance") || name.includes("veo") || name.includes("sora")) return { description: "镜头生成与图生视频,适合成片流程", time: "3m" };
+ if (name.includes("veo") || name.includes("omni flash") || name.includes("omni-flash")) return { description: "Gemini 镜头生成与图生视频,适合成片流程", time: "3m" };
+ if (name.includes("seedance") || name.includes("sora")) return { description: "镜头生成与图生视频,适合成片流程", time: "3m" };
return { description: "视频生成模型", time: "3m" };
}
if (capability === "audio") return { description: "语音、音效或音乐生成", time: "20s" };
@@ -157,13 +159,25 @@ export function ModelIcon({ model }: { model: string }) {
return icon ?
: ;
}
-function resolveModelIcon(model: string) {
+export function resolveModelIcon(model: string) {
const name = model.toLowerCase();
if (name.includes("claude") || name.includes("anthropic")) return "/icons/claude.svg";
- if (name.includes("gemini") || name.includes("google")) return "/icons/gemini.svg";
- if (name.includes("gpt") || name.includes("openai")) return "/icons/openai.svg";
- if (name.includes("grok") || name.includes("grok")) return "/icons/grok.svg";
- if (name.includes("deepseek") || name.includes("deepseek")) return "/icons/deepseek.svg";
- if (name.includes("glm") || name.includes("glm")) return "/icons/glm.svg";
+ // Flow2API 短名:Nano Banana / Imagen / Veo / Omni 均属 Google Gemini 系。
+ if (
+ name.includes("gemini") ||
+ name.includes("google") ||
+ name.includes("nano banana") ||
+ name.includes("nanobanana") ||
+ name.includes("imagen") ||
+ name.includes("veo") ||
+ name.includes("omni flash") ||
+ name.includes("omni-flash")
+ ) {
+ return "/icons/gemini.svg";
+ }
+ if (name.includes("gpt") || name.includes("openai") || name.includes("dall-e") || name.includes("dalle")) return "/icons/openai.svg";
+ if (name.includes("grok")) return "/icons/grok.svg";
+ if (name.includes("deepseek")) return "/icons/deepseek.svg";
+ if (name.includes("glm") || name.includes("chatglm")) return "/icons/glm.svg";
return "";
}
diff --git a/web/src/lib/canvas/canvas-node-size.ts b/web/src/lib/canvas/canvas-node-size.ts
index b43bb8e7e..e67613c6d 100644
--- a/web/src/lib/canvas/canvas-node-size.ts
+++ b/web/src/lib/canvas/canvas-node-size.ts
@@ -6,10 +6,14 @@ export function fitNodeSize(width: number, height: number, maxWidth = 640, maxHe
}
export function nodeSizeFromRatio(size: string, baseWidth: number, baseHeight: number) {
- const match = size?.match(/^(\d+)(?:x|:)(\d+)/);
+ const raw = String(size || "").trim();
+ if (!raw || raw.toLowerCase() === "auto") return null;
+ // 支持 16:9 / 1024x576 / 16:9-2k
+ const match = raw.match(/^(\d+(?:\.\d+)?)(?:x|:)(\d+(?:\.\d+)?)/i);
if (!match) return null;
const width = Number(match[1]);
const height = Number(match[2]);
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
const ratio = width / Math.max(1, height);
if (ratio < 0.25 || ratio > 4) return { width: baseWidth, height: baseHeight };
return ratio >= baseWidth / baseHeight ? { width: baseWidth, height: baseWidth / ratio } : { width: baseHeight * ratio, height: baseHeight };
diff --git a/web/src/pages/canvas/canvas-image-generation-executor.ts b/web/src/pages/canvas/canvas-image-generation-executor.ts
index 32e2e0c65..2a5e16466 100644
--- a/web/src/pages/canvas/canvas-image-generation-executor.ts
+++ b/web/src/pages/canvas/canvas-image-generation-executor.ts
@@ -2,7 +2,7 @@ import { nanoid } from "nanoid";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { imageMetadata } from "@/lib/canvas/canvas-generation-task-sync";
-import { fitNodeSize } from "@/lib/canvas/canvas-node-size";
+import { fitNodeSize, nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
import { prepareInPlaceMediaVersion } from "@/lib/canvas/canvas-media-versions";
import { buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, runBackendCanvasGenerationTask } from "@/lib/canvas/canvas-project-generation";
import { CONTENT_MODERATION_ERROR_CODE, generationFailureMetadata, type GenerationFailureMetadata } from "@/lib/generation-error";
@@ -45,12 +45,17 @@ export async function executeImageGeneration({
const generationType = referenceImages.length ? ("edit" as const) : ("generation" as const);
const generationMetadata = buildImageGenerationMetadata(generationType, generationConfig, count, referenceImages);
const parentConfig = NODE_DEFAULT_SIZE[isConfigNode ? CanvasNodeType.Config : isImageNode ? CanvasNodeType.Image : CanvasNodeType.Text];
- const imageConfig = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
+ const imageDefaults = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
+ // 生成中占位框按设置比例显示,避免 16:9 任务显示成默认 340x240。
+ const imageConfig = nodeSizeFromRatio(generationConfig.size || "auto", imageDefaults.width, imageDefaults.height) || imageDefaults;
const parentPosition = sourceNode?.position || { x: 0, y: 0 };
const rootId = isImageNode ? nodeId : nanoid();
const childIds = count > 1 ? Array.from({ length: count }, () => nanoid()) : [];
const targetIds = count > 1 ? childIds : [rootId];
registerPendingNodeIds(isEmptyImageNode ? childIds : [rootId, ...childIds]);
+ // 空节点可直接按目标比例改尺寸;已有内容的原位重生保留原尺寸,避免布局跳动。
+ const rootWidth = isImageNode && !isEmptyImageNode ? sourceNode?.width || imageConfig.width : imageConfig.width;
+ const rootHeight = isImageNode && !isEmptyImageNode ? sourceNode?.height || imageConfig.height : imageConfig.height;
const rootNode: CanvasNodeData = {
id: rootId,
@@ -60,13 +65,14 @@ export async function executeImageGeneration({
? parentPosition
: {
x: parentPosition.x + parentConfig.width + 96,
- y: parentPosition.y + parentConfig.height / 2 - imageConfig.height / 2,
+ y: parentPosition.y + parentConfig.height / 2 - rootHeight / 2,
},
- width: isImageNode ? sourceNode?.width || imageConfig.width : imageConfig.width,
- height: isImageNode ? sourceNode?.height || imageConfig.height : imageConfig.height,
+ width: rootWidth,
+ height: rootHeight,
metadata: {
prompt: effectivePrompt,
status: NODE_STATUS_LOADING,
+ size: generationConfig.size,
isBatchRoot: count > 1,
batchChildIds: count > 1 ? childIds : undefined,
batchUsesReferenceImages: referenceImages.length > 0,
@@ -87,7 +93,7 @@ export async function executeImageGeneration({
},
width: imageConfig.width,
height: imageConfig.height,
- metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, batchRootId: count > 1 ? rootId : undefined, ...generationMetadata, generationErrorCode: undefined, failedPromptFingerprint: undefined },
+ metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, size: generationConfig.size, batchRootId: count > 1 ? rootId : undefined, ...generationMetadata, generationErrorCode: undefined, failedPromptFingerprint: undefined },
}));
const batchConnections = [...(isImageNode ? [] : [{ id: nanoid(), fromNodeId: nodeId, toNodeId: rootId }]), ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: rootId, toNodeId: childId }))];
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx
index 91391872c..8afd9f4b8 100644
--- a/web/src/pages/canvas/project.tsx
+++ b/web/src/pages/canvas/project.tsx
@@ -977,8 +977,15 @@ function InfiniteCanvasPage() {
const pasteAtPosition = useCallback(
(position: Position) => {
- if (pasteCopiedNodes(position)) return;
- void pasteSystemClipboard(position).catch(() => message.warning("无法读取剪贴板内容"));
+ void (async () => {
+ try {
+ // 右键粘贴同样优先系统剪贴板图片,再回退内部节点。
+ const handled = await pasteSystemClipboard(position);
+ if (!handled) pasteCopiedNodes(position);
+ } catch {
+ if (!pasteCopiedNodes(position)) message.warning("无法读取剪贴板内容");
+ }
+ })();
},
[message, pasteCopiedNodes, pasteSystemClipboard],
);
diff --git a/web/src/pages/canvas/use-canvas-keyboard.ts b/web/src/pages/canvas/use-canvas-keyboard.ts
index 4ecc4e738..0133f9b18 100644
--- a/web/src/pages/canvas/use-canvas-keyboard.ts
+++ b/web/src/pages/canvas/use-canvas-keyboard.ts
@@ -23,7 +23,7 @@ type UseCanvasKeyboardOptions = {
cancelSelectionBox: () => void;
copySelectedNodes: () => void;
pasteCopiedNodes: () => boolean;
- pasteSystemClipboard: () => unknown;
+ pasteSystemClipboard: (position?: undefined, clipboardEvent?: ClipboardEvent | null) => Promise | boolean;
deleteNodes: (ids: Set) => void;
deleteConnection: (connectionId: string) => void;
deselectCanvas: () => void;
@@ -105,8 +105,8 @@ export function useCanvasKeyboard({
return;
}
if (isModifierShortcut && !event.altKey && key === "v") {
- event.preventDefault();
- if (!pasteCopiedNodes()) void pasteSystemClipboard();
+ // 不在 keydown 直接粘贴节点:交给 paste 事件,优先系统剪贴板图片。
+ // 这里只做标记,真正逻辑在 paste 监听器里。
return;
}
if (event.key === "Delete" || event.key === "Backspace") {
@@ -122,7 +122,22 @@ export function useCanvasKeyboard({
}
};
+ const handlePaste = (event: ClipboardEvent) => {
+ const target = event.target instanceof Element ? event.target : null;
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLSelectElement || target?.closest("[contenteditable='true'],[data-canvas-no-zoom]")) return;
+ // 系统剪贴板有图片时优先粘贴图片;否则再粘贴画布内部复制的节点。
+ event.preventDefault();
+ void (async () => {
+ const handled = await pasteSystemClipboard(undefined, event);
+ if (!handled) pasteCopiedNodes();
+ })();
+ };
+
window.addEventListener("keydown", handleKeyDown, true);
- return () => window.removeEventListener("keydown", handleKeyDown, true);
+ window.addEventListener("paste", handlePaste, true);
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown, true);
+ window.removeEventListener("paste", handlePaste, true);
+ };
}, [cancelSelectionBox, copySelectedNodes, deleteConnection, deleteNodes, deselectCanvas, fitCanvasContent, fitCanvasSelection, nodesRef, pasteCopiedNodes, pasteSystemClipboard, redoCanvas, saveCanvasProject, selectedConnectionId, selectedNodeIdsRef, setAnnotationNodeId, setContextMenu, setCropNodeId, setInfoNodeId, setMaskEditNodeId, setSelectedConnectionId, setSelectedNodeIds, setShortcutRequestNonce, undoCanvas, zoomToActualSize]);
}
diff --git a/web/src/pages/canvas/use-canvas-node-operations.ts b/web/src/pages/canvas/use-canvas-node-operations.ts
index e4141cf48..f052a0bcc 100644
--- a/web/src/pages/canvas/use-canvas-node-operations.ts
+++ b/web/src/pages/canvas/use-canvas-node-operations.ts
@@ -272,6 +272,12 @@ export function useCanvasNodeOperations({
delete metadata.taskCreatedAt;
delete metadata.taskUpdatedAt;
delete metadata.errorDetails;
+ delete metadata.batchRootId;
+ delete metadata.batchChildIds;
+ delete metadata.isBatchRoot;
+ delete metadata.primaryImageId;
+ delete metadata.imageBatchExpanded;
+ delete metadata.batchUsesReferenceImages;
metadata.status = metadata.content ? NODE_STATUS_SUCCESS : NODE_STATUS_IDLE;
metadata.versionOfNodeId = versionRootId;
metadata.versionLabel = versionLabel;
@@ -332,6 +338,17 @@ export function useCanvasNodeOperations({
connections: connectionsRef.current.filter((connection) => copyIds.has(connection.fromNodeId) && copyIds.has(connection.toNodeId)).map((connection) => ({ ...connection })),
};
setHasCopiedNodes(true);
+ // 写入系统剪贴板标记,覆盖旧图片,避免 Ctrl+V 仍优先粘贴系统图片。
+ const marker = `open-ai-canvas-nodes:${Date.now()}:${copiedNodes.length}`;
+ try {
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
+ void navigator.clipboard.write([new ClipboardItem({ "text/plain": new Blob([marker], { type: "text/plain" }) })]);
+ } else if (navigator.clipboard?.writeText) {
+ void navigator.clipboard.writeText(marker);
+ }
+ } catch {
+ // 忽略剪贴板权限失败,内部剪贴板仍可用。
+ }
}, [connectionsRef, nodesRef]);
const copySelectedNodes = useCallback(() => {
@@ -351,21 +368,61 @@ export function useCanvasNodeOperations({
const dx = center.x - (bounds.left + bounds.right) / 2;
const dy = center.y - (bounds.top + bounds.bottom) / 2;
const idMap = new Map(clipboard.nodes.map((node, index) => [node.id, `${node.type}-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 7)}`]));
- const nextNodes = clipboard.nodes.map((node) => ({
- ...node,
- id: idMap.get(node.id)!,
- title: node.title.endsWith(" Copy") ? node.title : `${node.title} Copy`,
- position: { x: node.position.x + dx, y: node.position.y + dy },
- parentId: node.parentId ? idMap.get(node.parentId) : undefined,
- metadata: node.type === CanvasNodeType.Drawing
- ? { ...node.metadata, frame: node.metadata?.frame ? { ...node.metadata.frame } : undefined, drawingId: `${idMap.get(node.id)}-document`, drawingRevision: 0, drawingUpdatedAt: undefined, drawingShapeCount: 0, drawingPageCount: 1 }
- : node.metadata ? { ...node.metadata, frame: node.metadata.frame ? { ...node.metadata.frame } : undefined } : undefined,
- }));
+ const copiedSourceIds = new Set(clipboard.nodes.map((node) => node.id));
+ const nextNodes = clipboard.nodes.map((node) => {
+ const metadata = node.metadata ? { ...node.metadata, frame: node.metadata.frame ? { ...node.metadata.frame } : undefined } : undefined;
+ if (metadata) {
+ // 粘贴必须切断与源批次/任务的绑定,否则拖拽会通过 batchChildIds 带动旧结果。
+ delete metadata.taskId;
+ delete metadata.taskStatus;
+ delete metadata.taskProgress;
+ delete metadata.taskStage;
+ delete metadata.taskCreatedAt;
+ delete metadata.taskUpdatedAt;
+ delete metadata.errorDetails;
+ delete metadata.generationErrorCode;
+ delete metadata.failedPromptFingerprint;
+ delete metadata.batchRootId;
+ delete metadata.batchChildIds;
+ delete metadata.isBatchRoot;
+ delete metadata.primaryImageId;
+ delete metadata.imageBatchExpanded;
+ delete metadata.batchUsesReferenceImages;
+ delete metadata.versionOfNodeId;
+ delete metadata.versionLabel;
+ delete metadata.versionPrimary;
+ metadata.status = metadata.content ? NODE_STATUS_SUCCESS : NODE_STATUS_IDLE;
+ }
+ if (node.type === CanvasNodeType.Drawing && metadata) {
+ metadata.drawingId = `${idMap.get(node.id)}-document`;
+ metadata.drawingRevision = 0;
+ metadata.drawingUpdatedAt = undefined;
+ metadata.drawingShapeCount = 0;
+ metadata.drawingPageCount = 1;
+ }
+ return {
+ ...node,
+ id: idMap.get(node.id)!,
+ title: node.title.endsWith(" Copy") ? node.title : `${node.title} Copy`,
+ position: { x: node.position.x + dx, y: node.position.y + dy },
+ parentId: node.parentId ? idMap.get(node.parentId) : undefined,
+ metadata,
+ };
+ });
+ // 1) 剪贴板内部连线;2) 仍保留到画布上未复制参考节点的入边(只复制结果节点时常见)。
const nextConnections = clipboard.connections.flatMap((connection, index) => {
const fromNodeId = idMap.get(connection.fromNodeId);
const toNodeId = idMap.get(connection.toNodeId);
return fromNodeId && toNodeId ? [{ ...connection, id: `conn-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 7)}`, fromNodeId, toNodeId }] : [];
});
+ connectionsRef.current.forEach((connection) => {
+ if (!copiedSourceIds.has(connection.toNodeId) || copiedSourceIds.has(connection.fromNodeId)) return;
+ const toNodeId = idMap.get(connection.toNodeId);
+ if (!toNodeId) return;
+ // 参考节点仍在画布上时,粘贴结果应重新挂上同一入边。
+ if (!nodesRef.current.some((node) => node.id === connection.fromNodeId)) return;
+ nextConnections.push({ ...connection, id: nanoid(), toNodeId });
+ });
commitNodes([...nodesRef.current, ...nextNodes]);
commitConnections([...connectionsRef.current, ...nextConnections]);
const sourceByTargetId = new Map(clipboard.nodes.map((sourceNode) => [idMap.get(sourceNode.id), sourceNode]));
diff --git a/web/src/pages/canvas/use-canvas-upload.ts b/web/src/pages/canvas/use-canvas-upload.ts
index 13e9a1816..80d0a4904 100644
--- a/web/src/pages/canvas/use-canvas-upload.ts
+++ b/web/src/pages/canvas/use-canvas-upload.ts
@@ -367,28 +367,65 @@ export function useCanvasUpload({
}
}, [message, nodesRef, persistMediaNode, selectInsertedNode, setNodes, startUploadStatus]);
- const pasteSystemClipboard = useCallback(async (position?: Position) => {
- if (!navigator.clipboard) return;
- if (navigator.clipboard.read) {
- const items = await navigator.clipboard.read();
- const imageItem = items.find((item) => item.types.some((type) => type.startsWith("image/")));
- if (imageItem) {
- const imageType = imageItem.types.find((type) => type.startsWith("image/"));
- if (!imageType) return;
- const blob = await imageItem.getType(imageType);
- const file = new File([blob], "clipboard-image.png", { type: imageType });
- const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id));
- if (selected.length === 1 && selected[0].type === CanvasNodeType.Image) {
- if (await replaceNodeMedia(selected[0].id, file)) message.success("已用剪切板图片替换,可撤销恢复");
- return;
+ const pasteSystemClipboard = useCallback(async (position?: Position, clipboardEvent?: ClipboardEvent | null) => {
+ const isNodeMarker = (value: string) => value.trim().startsWith("open-ai-canvas-nodes:");
+ const pasteImageFile = async (file: File) => {
+ const selected = nodesRef.current.filter((node) => selectedNodeIdsRef.current.has(node.id));
+ if (selected.length === 1 && selected[0].type === CanvasNodeType.Image) {
+ if (await replaceNodeMedia(selected[0].id, file)) message.success("已用剪切板图片替换,可撤销恢复");
+ return true;
+ }
+ const inserted = await createImageFileNode(file, position || getCanvasCenter());
+ if (inserted) message.success("已从剪切板添加图片");
+ return Boolean(inserted);
+ };
+
+ // 1) paste 事件里的图片文件(截图/资源管理器复制)优先。
+ const filesFromEvent = clipboardEvent
+ ? Array.from(clipboardEvent.clipboardData?.items || [])
+ .filter((item) => item.kind === "file" && item.type.startsWith("image/"))
+ .map((item) => item.getAsFile())
+ .filter((file): file is File => Boolean(file))
+ : [];
+ if (filesFromEvent.length) return pasteImageFile(filesFromEvent[0]);
+
+ // 2) 若系统文本是节点标记,说明最近一次复制是画布节点,不要再读旧图片。
+ const eventText = clipboardEvent?.clipboardData?.getData("text/plain") || "";
+ if (isNodeMarker(eventText)) return false;
+
+ // 3) 异步读系统剪贴板:有图片才导入;若文本是节点标记则让位给节点粘贴。
+ if (navigator.clipboard?.read) {
+ try {
+ const items = await navigator.clipboard.read();
+ const textItem = items.find((item) => item.types.includes("text/plain"));
+ if (textItem) {
+ const textBlob = await textItem.getType("text/plain");
+ const text = await textBlob.text();
+ if (isNodeMarker(text)) return false;
}
- const inserted = await createImageFileNode(file, position || getCanvasCenter());
- if (inserted) message.success("已从剪切板添加图片");
- return;
+ const imageItem = items.find((item) => item.types.some((type) => type.startsWith("image/")));
+ if (imageItem) {
+ const imageType = imageItem.types.find((type) => type.startsWith("image/"));
+ if (!imageType) return false;
+ const blob = await imageItem.getType(imageType);
+ return pasteImageFile(new File([blob], "clipboard-image.png", { type: imageType }));
+ }
+ } catch {
+ // 无权限时继续文本分支。
}
}
- const text = await navigator.clipboard.readText();
- if (createTextNodeFromClipboard(text, position)) message.success("已从剪切板添加文本");
+
+ try {
+ const text = eventText || (navigator.clipboard?.readText ? await navigator.clipboard.readText() : "");
+ if (isNodeMarker(text)) return false;
+ if (createTextNodeFromClipboard(text, position)) {
+ message.success("已从剪切板添加文本");
+ return true;
+ }
+ } catch {
+ // ignore
+ }
+ return false;
}, [createImageFileNode, createTextNodeFromClipboard, getCanvasCenter, message, nodesRef, replaceNodeMedia, selectedNodeIdsRef]);
const handleImageInputChange = useCallback(async (event: ChangeEvent) => {