fix(*): 创作工作台 - 修复 Agent、资源引用、附件交互与 Agnes 视频协议

This commit is contained in:
ddCat
2026-08-27 22:12:04 +08:00
parent 0e33cc805e
commit 4675cf4c99
33 changed files with 1218 additions and 239 deletions

View File

@@ -65,7 +65,7 @@ import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-co
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
import { useFocusMode } from "@/hooks/use-focus-mode";
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
import { getContextResourceNodes, removeCanvasResourceMention, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { getContextResourceNodes, normalizeCanvasNodeMentionTokens, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components/canvas/canvas-workspace-overlays";
import { CanvasOverlayLayerContainer, CanvasOverlayLayerProvider } from "@/components/canvas/canvas-overlay-layer";
import { CanvasLeaferGraphicsLayer } from "@/components/canvas/canvas-leafer-graphics-layer";
@@ -147,6 +147,31 @@ const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-d
const NODE_STATUS_SUCCESS = "success" as const;
const EMPTY_RESOURCE_REFERENCES: CanvasResourceReference[] = [];
async function copyImageToSystemClipboard(source: string) {
if (typeof ClipboardItem === "undefined" || !navigator.clipboard?.write) throw new Error("当前浏览器不支持复制图片");
const response = await fetch(source);
if (!response.ok) throw new Error(`图片读取失败(HTTP ${response.status})`);
const sourceBlob = await response.blob();
const blob = sourceBlob.type === "image/png" ? sourceBlob : await convertClipboardImageToPNG(sourceBlob);
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
}
async function convertClipboardImageToPNG(blob: Blob) {
if (typeof createImageBitmap !== "function") throw new Error("当前浏览器无法转换这张图片的格式");
const bitmap = await createImageBitmap(blob);
try {
const canvas = document.createElement("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const context = canvas.getContext("2d");
if (!context) throw new Error("当前浏览器无法处理这张图片");
context.drawImage(bitmap, 0, 0);
return await new Promise<Blob>((resolve, reject) => canvas.toBlob((value) => value ? resolve(value) : reject(new Error("图片格式转换失败")), "image/png"));
} finally {
bitmap.close();
}
}
function visibleGenerationBatch(node: CanvasNodeData) {
const batches = node.metadata?.generationBatches || [];
for (let index = batches.length - 1; index >= 0; index -= 1) {
@@ -1081,15 +1106,11 @@ function InfiniteCanvasPage() {
.filter((connection) => connection.fromNodeId === referenceNodeId && (connection.toNodeId === targetNodeId || connection.toNodeId === configNodeId))
.map((connection) => connection.id),
);
const targetNode = nodesRef.current.find((node) => node.id === targetNodeId);
const currentPrompt = targetNode?.metadata?.composerContent ?? targetNode?.metadata?.prompt ?? "";
const nextPrompt = removeCanvasResourceMention(currentPrompt, reference);
if (nextPrompt !== currentPrompt) handleNodePromptChange(targetNodeId, nextPrompt);
if (!removedConnectionIds.size) return;
connectionsRef.current = connectionsRef.current.filter((connection) => !removedConnectionIds.has(connection.id));
setConnections(connectionsRef.current);
setSelectedConnectionId((current) => current && removedConnectionIds.has(current) ? null : current);
}, [connectionsRef, handleNodePromptChange, nodesRef, setConnections, setSelectedConnectionId]);
}, [connectionsRef, nodesRef, setConnections, setSelectedConnectionId]);
const handleProjectFolderInsert = useCallback((folderId: string) => {
const folder = linkedProjectQuery.data?.assetFolders.find((item) => item.id === folderId);
@@ -1211,6 +1232,26 @@ function InfiniteCanvasPage() {
scriptEditorNodeId,
dialogNodeId,
});
useEffect(() => {
setNodes((current) => {
let changed = false;
const next = current.map((node) => {
const references = mentionReferencesByNodeId.get(node.id);
const savedPrompt = node.metadata?.composerContent ?? node.metadata?.prompt;
if (!references?.length || !savedPrompt?.includes("@[node:")) return node;
const normalizedPrompt = normalizeCanvasNodeMentionTokens(savedPrompt, references);
if (normalizedPrompt === savedPrompt) return node;
changed = true;
return {
...node,
metadata: node.metadata?.composerContent !== undefined
? { ...node.metadata, composerContent: normalizedPrompt }
: { ...node.metadata, prompt: normalizedPrompt },
};
});
return changed ? next : current;
});
}, [mentionReferencesByNodeId, setNodes]);
const dialogNode = dialogNodeId ? nodeById.get(dialogNodeId) || null : null;
const subtitleNode = subtitleNodeId ? nodeById.get(subtitleNodeId) || null : null;
const timelineNode = timelineNodeId ? nodeById.get(timelineNodeId) || null : null;
@@ -1449,29 +1490,26 @@ function InfiniteCanvasPage() {
const copyNodeContentToClipboard = useCallback(
async (node: CanvasNodeData | null) => {
releaseCopiedNodesPastePriority();
const content = node?.metadata?.content;
if (!node || !content) {
const content = node?.metadata?.content?.trim();
const resourceId = resourceIdFromStorageKey(node?.metadata?.storageKey);
const copySource = content || (node?.type === CanvasNodeType.Image && resourceId ? resourceFileUrl(resourceId) : "");
if (!node || !copySource) {
message.warning("没有可复制的内容");
return;
}
try {
if (node.type === CanvasNodeType.Image && typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
const response = await fetch(content);
const blob = await response.blob();
await navigator.clipboard.write([new ClipboardItem({ [blob.type || "image/png"]: blob })]);
if (node.type === CanvasNodeType.Image) {
await copyImageToSystemClipboard(copySource);
message.success("图片已复制");
return;
}
if (!navigator.clipboard?.writeText) {
message.warning("当前浏览器不支持写入剪贴板");
return;
}
await navigator.clipboard.writeText(content);
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(copySource);
else if (!copyToClipboard(copySource)) throw new Error("当前浏览器不支持写入剪贴板");
message.success(node.type === CanvasNodeType.Text ? "文本已复制" : "内容链接已复制");
} catch {
message.error("复制失败,请检查浏览器剪贴板权限");
} catch (error) {
message.error(error instanceof Error ? error.message : "复制失败,请检查浏览器剪贴板权限");
}
},
[message, releaseCopiedNodesPastePriority],
@@ -1670,8 +1708,8 @@ function InfiniteCanvasPage() {
y: sourceNode.position.y + sourceNode.height / 2,
},
{
prompt: `@[node:${sourceNode.id}]`,
composerContent: `@[node:${sourceNode.id}]`,
prompt: "@文本1",
composerContent: "@文本1",
model: effectiveConfig.imageModel || effectiveConfig.model,
size: effectiveConfig.size,
quality: effectiveConfig.quality,

View File

@@ -145,7 +145,7 @@ export function useCanvasMediaTools({
generationMode: "text",
model: effectiveConfig.textModel || effectiveConfig.model || defaultConfig.textModel,
count: 1,
composerContent: `参考图片:@[node:${node.id}]\n任务说明:@[node:${textNode.id}]`,
composerContent: "参考图片:@图片1\n任务说明:@文本1",
}),
title: "反推提示词结果",
};
@@ -163,7 +163,7 @@ export function useCanvasMediaTools({
return;
}
const portraitTextureSettings = { ...DEFAULT_PORTRAIT_TEXTURE_SETTINGS, ...node.metadata?.portraitTexture };
const composerContent = node.metadata?.composerContent?.trim() || node.metadata?.prompt?.trim() || `@[node:${node.id}]`;
const composerContent = node.metadata?.composerContent?.trim() || node.metadata?.prompt?.trim() || "@图片1";
setHoveredNodeId(null);
setToolbarNodeId(null);
setNodes((current) => current.map((item) => item.id === node.id ? { ...item, metadata: { ...item.metadata, prompt: composerContent, composerContent, portraitTexture: portraitTextureSettings } } : item));

View File

@@ -200,7 +200,7 @@ export function useCanvasStoryboard({
const existing = row.imageNodeId ? nextNodes.find((node) => node.id === row.imageNodeId && node.type === CanvasNodeType.Image) : undefined;
const existingMetadata = existing?.metadata?.content ? existing.metadata : resetGenerationTaskMetadata(existing?.metadata);
const referenceIds = storyboardRowReferenceNodeIds(scriptNode, row, nextNodes, nextConnections, false, existing?.id);
const composerContent = storyboardComposerContent(prompt, referenceIds);
const composerContent = storyboardComposerContent(prompt, referenceIds, nextNodes);
const imageNode = existing
? { ...existing, metadata: { ...existingMetadata, prompt, composerContent, ...storyboardPromptTemplateMetadata(row, "image"), workflowKind: "shot" as const, workflowTitle: `镜头 ${row.shotNumber} 分镜图`, shotIndex: row.shotNumber } }
: createCanvasNode(CanvasNodeType.Image, { x: startX + imageSpec.width / 2, y: scriptNode.position.y + index * (imageSpec.height + 36) + imageSpec.height / 2 }, { prompt, composerContent, ...storyboardPromptTemplateMetadata(row, "image"), workflowKind: "shot", workflowTitle: `镜头 ${row.shotNumber} 分镜图`, shotIndex: row.shotNumber, status: NODE_STATUS_IDLE });
@@ -285,7 +285,7 @@ export function useCanvasStoryboard({
const existingIndex = row.videoNodeId ? nextNodes.findIndex((node) => node.id === row.videoNodeId && node.type === CanvasNodeType.Video) : -1;
const existing = existingIndex >= 0 ? nextNodes[existingIndex] : undefined;
const referenceIds = storyboardRowReferenceNodeIds(scriptNode, row, nextNodes, nextConnections, false, existing?.id);
const composerContent = storyboardComposerContent(prompt, referenceIds);
const composerContent = storyboardComposerContent(prompt, referenceIds, nextNodes);
if (existingIndex >= 0) {
const existing = nextNodes[existingIndex];
const existingMetadata = existing.metadata?.content ? existing.metadata : resetGenerationTaskMetadata(existing.metadata);
@@ -447,7 +447,7 @@ export function useCanvasStoryboard({
const prompt = (row.videoMotionPrompt || row.plotDescription).trim();
const existing = row.videoNodeId ? nextNodes.find((node) => node.id === row.videoNodeId && node.type === CanvasNodeType.Video) : undefined;
const referenceIds = storyboardRowReferenceNodeIds(currentScriptNode, row, nextNodes, nextConnections, true, existing?.id);
const composerContent = storyboardComposerContent(prompt, referenceIds);
const composerContent = storyboardComposerContent(prompt, referenceIds, nextNodes);
const existingMetadata = existing?.metadata?.content ? existing.metadata : resetGenerationTaskMetadata(existing?.metadata);
const videoNode = existing
? { ...existing, metadata: { ...existingMetadata, prompt, composerContent, model: videoModel, ...storyboardPromptTemplateMetadata(row, "video"), workflowKind: "shot" as const, workflowTitle: `镜头 ${row.shotNumber} 视频`, shotIndex: row.shotNumber, generationMode: "video" as const, videoEditOperation: "image_to_video" as const, videoStartFrameNodeId: row.imageNodeId, seconds: String(row.durationSeconds) } }