Files
ttstd-infinite-canvas/web/src/lib/canvas/canvas-resource-references.ts
爱笑的毛毛虫 eceaf7ac41 feat(canvas): 视频节点截取重生成 - 支持多段/时间线导入并调用视频模型批量重生成 (#197)
* feat(canvas): 视频节点截取重生成 - 支持多段/时间线导入并调用视频模型批量重生成

* test(canvas): 视频节点时间线导入 - 修正旧数据标题回退测试

* style(canvas): 视频节点截取重生成 - 格式化新增文件并修正测试

---------

Co-authored-by: sikongyue <3155157671@qq.com>
2026-08-10 00:13:02 +08:00

138 lines
7.0 KiB
TypeScript

import { imageReferenceLabel } from "@/lib/image-reference-prompt";
import { seedanceReferenceLabel } from "@/lib/seedance-video";
import type { Skill } from "@/services/api/skills";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "@/types/canvas";
export type CanvasResourceKind = "image" | "video" | "audio" | "text" | "skill" | "character";
export type CanvasResourceReference = {
id: string;
nodeId: string;
kind: CanvasResourceKind;
label: string;
title: string;
previewUrl?: string;
storageKey?: string;
text?: string;
active: boolean;
sourceType?: CanvasNodeType;
skill?: Skill;
};
export function canvasResourceMentionToken(reference: CanvasResourceReference) {
if (reference.kind === "skill" && reference.skill?.skill_id) return `@[skill:${reference.skill.skill_id}]`;
return `@[node:${reference.nodeId}]`;
}
export function buildCanvasResourceReferences(nodes: CanvasNodeData[], connections: CanvasConnection[], contextNodeId?: string | null) {
const contextNodes = contextNodeId ? getMentionResourceNodes(contextNodeId, nodes, connections) : [];
const globalReferences = labelResourceNodes(nodes.filter(isResourceNode), false);
const activeByNodeId = new Map(labelResourceNodes(contextNodes, true).map((reference) => [reference.nodeId, reference]));
return globalReferences.map((reference) => activeByNodeId.get(reference.nodeId) || reference);
}
export function buildNodeMentionReferences(node: CanvasNodeData, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
return labelResourceNodes(getMentionResourceNodes(node.id, nodes, connections), true);
}
export function getMentionResourceNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const configInputs = getConnectedConfigResourceNodes(nodeId, nodes, connections);
if (configInputs.length) return configInputs;
const ownInputs = getContextResourceNodes(nodeId, nodes, connections);
if (ownInputs.length) return ownInputs;
const node = nodes.find((item) => item.id === nodeId);
return node && isResourceNode(node) ? [node] : [];
}
export function getGenerationResourceNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const configInputs = getConnectedConfigResourceNodes(nodeId, nodes, connections);
if (configInputs.length) return configInputs;
const ownInputs = getContextResourceNodes(nodeId, nodes, connections);
if (ownInputs.length) return ownInputs;
return [];
}
/** 收集节点自身及其上游链路中的视频节点,用于时间线片段导入定位真正的视频源。 */
export function collectUpstreamVideoNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]): CanvasNodeData[] {
const queue = [nodeId];
const visited = new Set<string>();
const result: CanvasNodeData[] = [];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) continue;
visited.add(currentId);
const node = nodes.find((item) => item.id === currentId);
if (node?.type === CanvasNodeType.Video && Boolean(node.metadata?.content || node.metadata?.storageKey)) result.push(node);
connections.filter((connection) => connection.toNodeId === currentId).forEach((connection) => queue.push(connection.fromNodeId));
}
return result;
}
function getContextResourceNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
return connections
.filter((connection) => connection.toNodeId === nodeId)
.map((connection) => nodes.find((node) => node.id === connection.fromNodeId))
.filter((node): node is CanvasNodeData => Boolean(node && isResourceNode(node)));
}
function getConnectedConfigResourceNodes(nodeId: string, nodes: CanvasNodeData[], connections: CanvasConnection[]) {
const configConnection = connections.find((connection) => connection.fromNodeId === nodeId && nodes.find((node) => node.id === connection.toNodeId)?.type === CanvasNodeType.Config);
if (!configConnection) return [];
return getContextResourceNodes(configConnection.toNodeId, nodes, connections).filter((node) => node.id !== nodeId);
}
function labelResourceNodes(nodes: CanvasNodeData[], active: boolean) {
const counts: Record<CanvasResourceKind, number> = { image: 0, video: 0, audio: 0, text: 0, skill: 0, character: 0 };
let drawingCount = 0;
return nodes.flatMap((node): CanvasResourceReference[] => {
const kind = resourceKind(node);
if (!kind) return [];
const index = node.type === CanvasNodeType.Drawing ? drawingCount++ : counts[kind]++;
const label = node.type === CanvasNodeType.Drawing ? `绘图${index + 1}` : labelForKind(kind, index);
return [
{
id: node.id,
nodeId: node.id,
kind,
label,
title: node.title || label,
previewUrl: node.metadata?.workflowKind === "character" ? node.metadata.characterCoverUrl : node.type === CanvasNodeType.Drawing ? node.metadata?.drawingPreviewUrl : node.metadata?.content,
storageKey: node.metadata?.storageKey,
text: node.metadata?.workflowKind === "character" ? node.metadata.characterPrompt : node.type === CanvasNodeType.Text ? node.metadata?.content || node.metadata?.prompt : node.type === CanvasNodeType.Skill ? skillResourceText(node) : undefined,
active,
sourceType: node.type,
},
];
});
}
function labelForKind(kind: CanvasResourceKind, index: number) {
if (kind === "character") return `角色${index + 1}`;
if (kind === "image") return imageReferenceLabel(index);
if (kind === "video") return seedanceReferenceLabel("video", index);
if (kind === "audio") return seedanceReferenceLabel("audio", index);
if (kind === "skill") return `技能${index + 1}`;
return `文本${index + 1}`;
}
function isResourceNode(node: CanvasNodeData) {
return Boolean(resourceKind(node));
}
function resourceKind(node: CanvasNodeData): CanvasResourceKind | null {
if (node.metadata?.workflowKind === "character" && node.metadata.characterAssetId) return "character";
if (node.type === CanvasNodeType.Image && node.metadata?.content) return "image";
if (node.type === CanvasNodeType.Drawing && node.metadata?.drawingId) return "image";
if (node.type === CanvasNodeType.Video && node.metadata?.content) return "video";
if (node.type === CanvasNodeType.Audio && node.metadata?.content) return "audio";
if (node.type === CanvasNodeType.Text && (node.metadata?.content || node.metadata?.prompt)) return "text";
if (node.type === CanvasNodeType.Skill && (node.metadata?.skillSnapshot || node.metadata?.content)) return "text";
return null;
}
function skillResourceText(node: CanvasNodeData) {
const skill = node.metadata?.skillSnapshot;
if (!skill) return node.metadata?.content || "";
return [skill.name, skill.description, skill.template, skill.outputContract].filter(Boolean).join("\n\n");
}