feat(previs): 图片生成预演台 - 从图片节点交给 Agent 复现为可编辑 3D 预演

图片工具栏新增「生成预演台」,自动选用支持看图的文本模型并提交复现指令。对话用节点卡片展示「此图」,普通图片演员只写名称时不再要求角色卡绑定。
This commit is contained in:
ddcat666 committed 2026-10-09 17:17:28 +08:00
1 parent ac68378246
commit c0a9250cb3
13 files changed
+282 -23

No files matched your search

+28 -1
View File
@@ -91,6 +91,8 @@ import {
replaceCanvasReferenceMentions,
type CanvasResourceReference,
} from "@/lib/canvas/canvas-resource-references";
import { buildImageToPrevisAgentPrompt, buildImageToPrevisDisplayText } from "@/lib/canvas/image-to-previs-agent";
import type { CloudAgentMessagePresentation } from "@/services/cloud-agent-conversations";
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";
@@ -241,7 +243,7 @@ function CanvasViewportPage() {
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
// 每次发送带递增 id:重复发送同一节点时文本相同,仍需触发一次追加。
const [agentPrefillRequest, setAgentPrefillRequest] = useState<{ id: number; text: string } | null>(null);
const [agentPrefillRequest, setAgentPrefillRequest] = useState<(CloudAgentMessagePresentation & { id: number; text: string; autoSubmit?: boolean; requiresVision?: boolean }) | null>(null);
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
const [canvasAppearance, setCanvasAppearance] = useState<CanvasAppearance>(() => canvasAppearanceForTheme(colorTheme));
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>(DEFAULT_CANVAS_BACKGROUND_MODE);
@@ -423,6 +425,30 @@ function CanvasViewportPage() {
openAgent();
setContextMenu(null);
}, [agentMentionReferences, openAgent]);
const sendImageToPrevisAgent = useCallback((node: CanvasNodeData) => {
if (node.type !== CanvasNodeType.Image) return;
const reference = agentMentionReferences.find((item) => item.nodeId === node.id && item.kind === "image");
if (!reference) {
message.warning("这张图片暂时没有可供 Agent 引用的画布资源");
return;
}
if (!selectedNodeIdsRef.current.has(node.id)) {
const selection = new Set([node.id]);
selectedNodeIdsRef.current = selection;
setSelectedNodeIds(selection);
}
setAgentPrefillRequest((current) => ({
id: (current?.id ?? 0) + 1,
text: buildImageToPrevisAgentPrompt(reference),
displayText: buildImageToPrevisDisplayText(),
canvasReferenceNodeId: reference.nodeId,
autoSubmit: true,
requiresVision: true,
}));
openAgent();
setContextMenu(null);
}, [agentMentionReferences, message, openAgent]);
// 修复素材关联仍遵守当前画布版本,不能替用户确认覆盖云端的新内容。
const confirmForceSaveCanvas = useCallback(() => {
modal.confirm({
@@ -2978,6 +3004,7 @@ function CanvasViewportPage() {
setLightingNodeId((current) => (current === node.id ? null : node.id));
}}
onPanorama={openPanoramaConfig}
onPrevis={sendImageToPrevisAgent}
onViewImage={(node) => setPreviewNodeId(node.id)}
onExtractVideoFrames={openVideoFrameExtractor}
onExtractAudioFromVideo={(node) => void extractAudioFromVideo(node)}
+1 -1
View File
@@ -282,7 +282,7 @@ export default function SharedCanvasPage() {
}} onHoverStart={keepToolbar} onHoverEnd={hideToolbar} onConnectStart={unauthorized} onResize={() => undefined} onContentChange={unauthorized} onRetry={unauthorized} onOpenTaskDetails={unauthorized} onViewImage={(target) => setInfoNodeId(target.id)} onContextMenu={(event, nodeId) => openContextMenu(event, nodeId)} />)}
</CanvasViewport>
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onAnnotationEdit={unauthorized} onTextEdit={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onRemoveBackground={unauthorized} onLayerDecomposition={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onLighting={unauthorized} onPanorama={unauthorized} onViewImage={unauthorized} onExtractVideoFrames={unauthorized} onExtractAudioFromVideo={unauthorized} onTrimVideoSegments={unauthorized} extractingVideoFrames={false} extractingAudio={false} trimmingVideo={false} onSubtitles={unauthorized} onTimeline={unauthorized} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} onNineGrid={unauthorized} />
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onAnnotationEdit={unauthorized} onTextEdit={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onRemoveBackground={unauthorized} onLayerDecomposition={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onLighting={unauthorized} onPanorama={unauthorized} onPrevis={unauthorized} onViewImage={unauthorized} onExtractVideoFrames={unauthorized} onExtractAudioFromVideo={unauthorized} onTrimVideoSegments={unauthorized} extractingVideoFrames={false} extractingAudio={false} trimmingVideo={false} onSubtitles={unauthorized} onTimeline={unauthorized} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} onNineGrid={unauthorized} />
<div className="absolute bottom-5 left-5 z-[var(--z-panel-floating)]"><CanvasZoomControls scale={viewport.k} containerRef={containerRef} onScaleChange={setZoom} onFitContent={resetViewport} isMiniMapOpen={false} onToggleMiniMap={unauthorized} onOpenShortcuts={unauthorized} /></div>
<div className="pointer-events-none absolute bottom-5 right-5 z-[var(--z-panel-floating)] max-w-[340px] text-right text-xs leading-5" style={{ color: theme.node.muted }}>访客操作仅在当前页面临时生效</div>