fix(app,canvas): 图片档位并集忽略停用线路,重生结果不再继承旧素材绑定

前台分辨率并集只收启用且权重大于 0 的供应线路,避免停用 4K 线路后仍出现不可路由档位。生成回写新媒体时清除旧 assetId,堵住原地重生后「旧素材 + 新资源」配对。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 00:08:35 +08:00
parent ab6d6d1557
commit d7388fa9a9
5 changed files with 99 additions and 13 deletions

View File

@@ -106,6 +106,19 @@ function workflowMetadataForResultNode(): Partial<CanvasNodeMetadata> {
};
}
// 原地重生会换 storageKey 但继承旧 assetId,形成「旧素材 + 新资源」配对,云端校验会永久拒绝。
// 新媒体结果必须清掉旧绑定,交给入库/修复路径按新资源重绑。
export function applyGeneratedMediaResultMetadata(node: CanvasNodeData, media: CanvasNodeMetadata, extra: Partial<CanvasNodeMetadata> = {}): CanvasNodeMetadata {
return {
...node.metadata,
...workflowMetadataForResultNode(),
...media,
...extra,
errorDetails: undefined,
assetId: undefined,
};
}
export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task: GenerationTask, nodes: CanvasNodeData[] = [node]): Promise<CanvasNodeData> {
const mode = generationTaskMode(task, node.type === CanvasNodeType.Text ? "text" : node.type === CanvasNodeType.Video ? "video" : node.type === CanvasNodeType.Audio ? "audio" : "image");
const prompt = node.metadata?.prompt || task.prompt;
@@ -143,7 +156,7 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task:
width: imageSize.width,
height: imageSize.height,
position: { x: node.position.x + node.width / 2 - imageSize.width / 2, y: node.position.y + node.height / 2 - imageSize.height / 2 },
metadata: { ...node.metadata, ...workflowMetadataForResultNode(), ...imageMetadata(normalizedImage), prompt, ...completedTaskMetadata(task), errorDetails: undefined },
metadata: applyGeneratedMediaResultMetadata(node, imageMetadata(normalizedImage), { prompt, ...completedTaskMetadata(task) }),
};
}
@@ -172,7 +185,7 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task:
...node,
type: CanvasNodeType.Video,
...geometry,
metadata: { ...node.metadata, ...workflowMetadataForResultNode(), ...videoMetadata(video), prompt, ...completedTaskMetadata(task), errorDetails: undefined },
metadata: applyGeneratedMediaResultMetadata(node, videoMetadata(video), { prompt, ...completedTaskMetadata(task) }),
};
}
@@ -181,7 +194,7 @@ export async function buildGenerationTaskNodeResult(node: CanvasNodeData, task:
const audio = result.audio.storageKey
? { url: await resolveMediaUrl(result.audio.storageKey, result.audio.dataUrl), storageKey: result.audio.storageKey, durationMs: result.audio.durationMs, bytes: result.audio.bytes || 0, mimeType: result.audio.mimeType || "audio/mpeg" }
: await storeGeneratedAudio(await (await fetch(result.audio.dataUrl)).blob(), result.audio.format || "mp3");
return { ...node, type: CanvasNodeType.Audio, metadata: { ...node.metadata, ...workflowMetadataForResultNode(), ...audioMetadata(audio), prompt, ...completedTaskMetadata(task), errorDetails: undefined } };
return { ...node, type: CanvasNodeType.Audio, metadata: applyGeneratedMediaResultMetadata(node, audioMetadata(audio), { prompt, ...completedTaskMetadata(task) }) };
}
if (!result.text) throw new Error("后端任务没有返回文本");

View File

@@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test";
import { applyGeneratedMediaResultMetadata, videoMetadata } from "@/lib/canvas/canvas-generation-task-sync";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
function videoNode(assetId: string, storageKey: string): CanvasNodeData {
return {
id: "node-1",
type: CanvasNodeType.Video,
title: "镜头",
position: { x: 0, y: 0 },
width: 320,
height: 180,
metadata: { assetId, storageKey, content: "https://example.test/old.mp4", prompt: "旧提示词" },
};
}
describe("applyGeneratedMediaResultMetadata", () => {
test("clears the previous asset binding when a regenerated media result lands", () => {
const node = videoNode("asset-old", "video:old");
const next = applyGeneratedMediaResultMetadata(node, videoMetadata({
url: "https://example.test/new.mp4",
storageKey: "video:new",
width: 1280,
height: 720,
bytes: 12,
mimeType: "video/mp4",
durationMs: 4000,
}), { prompt: "新提示词" });
expect(next.assetId).toBeUndefined();
expect(next.storageKey).toBe("video:new");
expect(next.content).toBe("https://example.test/new.mp4");
expect(next.prompt).toBe("新提示词");
expect(next.status).toBe("success");
});
});