@artflo-ai/artflo-openclaw-plugin 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/index.js +73 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-216Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-00-217Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-05-727Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-30-218Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-30-35-728Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-218Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-00-219Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-05-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-30-220Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-31-35-729Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-00-221Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-05-730Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-30-222Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-32-35-731Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-00-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-05-732Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-30-223Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-33-35-734Z+08-lifecycle-heartbeat_ping-2c637466-6c6f-6172-400c-15145e021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-228Z+08-lifecycle-heartbeat_ping-2b637466-6c6f-6172-470c-151459021b0a.json +9 -0
- package/dist/logs/ws-traffic-traces/2026-03-27T20-34-00-229Z+08-lifecycle-heartbeat_ping-2d637466-6c6f-6172-410c-15145f021b0a.json +9 -0
- package/dist/src/config.js +57 -0
- package/dist/src/constants.js +35 -0
- package/dist/src/core/api/api-base.js +12 -0
- package/dist/src/core/api/upload-file.js +59 -0
- package/dist/src/core/canvas/canvas-session-manager.js +189 -0
- package/dist/src/core/canvas/canvas-websocket-client.js +453 -0
- package/dist/src/core/canvas/create-canvas.js +37 -0
- package/dist/src/core/canvas/types.js +23 -0
- package/dist/src/core/canvas/ws-trace.js +42 -0
- package/dist/src/core/config/fetch-client-params.js +20 -0
- package/dist/src/core/config/fetch-vip-info.js +30 -0
- package/dist/src/core/config/model-config-transformer.js +104 -0
- package/dist/src/core/executor/element-builders.js +216 -0
- package/dist/src/core/executor/execute-plan.js +1221 -0
- package/dist/src/core/executor/execution-trace.js +34 -0
- package/dist/src/core/layout/layout-service.js +366 -0
- package/dist/src/core/plan/analyze-plan-groups.js +71 -0
- package/dist/src/core/plan/types.js +1 -0
- package/dist/src/core/plan/validate-plan.js +159 -0
- package/dist/src/paths.js +16 -0
- package/dist/src/services/canvas-session-registry.js +57 -0
- package/dist/src/tools/register-tools.js +669 -0
- package/dist/src/tools/tool-trace.js +19 -0
- package/openclaw.plugin.json +33 -0
- package/package.json +42 -0
- package/skills/artflo-canvas/SKILL.md +118 -0
- package/skills/artflo-canvas/references/graph-rules.md +53 -0
- package/skills/artflo-canvas/references/layout-notes.md +31 -0
- package/skills/artflo-canvas/references/node-schema.json +948 -0
- package/skills/artflo-canvas/references/node-schema.md +188 -0
- package/skills/artflo-canvas/references/planning-guide.md +321 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { CANVAS_NODE_TYPES, buildEdgeElement, buildNodeElement } from './element-builders.js';
|
|
3
|
+
import { calculateBatchGroupLayout, calculatePlanFlowLayout } from '../layout/layout-service.js';
|
|
4
|
+
import { analyzePlanGroups } from '../plan/analyze-plan-groups.js';
|
|
5
|
+
export async function executePlan(args) {
|
|
6
|
+
const { plan, session, traceWriter } = args;
|
|
7
|
+
const nodeMap = {};
|
|
8
|
+
const createdNodeIds = [];
|
|
9
|
+
const createdEdgeIds = [];
|
|
10
|
+
let executedNodeIds = [];
|
|
11
|
+
let executionCompleted = null;
|
|
12
|
+
const existingElements = session.getElements();
|
|
13
|
+
const existingNodeByRef = buildExistingNodeRefMap(existingElements, plan.planId);
|
|
14
|
+
const existingEdgeKeys = buildExistingEdgeKeySet(existingElements);
|
|
15
|
+
const explicitEdgeTargets = new Set(plan.edges.map((edge) => edge.to));
|
|
16
|
+
const groupAnalysis = analyzePlanGroups(plan);
|
|
17
|
+
await traceWriter?.writeMeta('input', {
|
|
18
|
+
plan,
|
|
19
|
+
existingElementCount: existingElements.length,
|
|
20
|
+
existingNodeRefs: Array.from(existingNodeByRef.keys()),
|
|
21
|
+
});
|
|
22
|
+
const refPositionMap = new Map();
|
|
23
|
+
const batchRefs = new Set(plan.nodes.filter((node) => node.type === 'batch').map((node) => node.ref));
|
|
24
|
+
// Defer nodes that transitively depend on a batch node.
|
|
25
|
+
// A batch node expands dynamically after its upstream (e.g. Refine) completes,
|
|
26
|
+
// so anything downstream of a batch must wait.
|
|
27
|
+
const deferredNodeRefs = new Set();
|
|
28
|
+
{
|
|
29
|
+
// Build adjacency: ref -> set of downstream refs
|
|
30
|
+
const downstreamOf = new Map();
|
|
31
|
+
for (const node of plan.nodes) {
|
|
32
|
+
downstreamOf.set(node.ref, new Set());
|
|
33
|
+
}
|
|
34
|
+
for (const edge of plan.edges) {
|
|
35
|
+
downstreamOf.get(edge.from)?.add(edge.to);
|
|
36
|
+
}
|
|
37
|
+
for (const node of plan.nodes) {
|
|
38
|
+
if (node.dependsOn && downstreamOf.has(node.dependsOn)) {
|
|
39
|
+
downstreamOf.get(node.dependsOn).add(node.ref);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// BFS from each batch ref to mark all transitive downstream as deferred
|
|
43
|
+
const queue = Array.from(batchRefs);
|
|
44
|
+
for (const batchRef of queue) {
|
|
45
|
+
deferredNodeRefs.add(batchRef);
|
|
46
|
+
}
|
|
47
|
+
while (queue.length > 0) {
|
|
48
|
+
const current = queue.shift();
|
|
49
|
+
const children = downstreamOf.get(current);
|
|
50
|
+
if (!children)
|
|
51
|
+
continue;
|
|
52
|
+
for (const child of children) {
|
|
53
|
+
if (!deferredNodeRefs.has(child)) {
|
|
54
|
+
deferredNodeRefs.add(child);
|
|
55
|
+
queue.push(child);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const nonDeferredPlan = {
|
|
61
|
+
...plan,
|
|
62
|
+
nodes: plan.nodes.filter((node) => !deferredNodeRefs.has(node.ref)),
|
|
63
|
+
edges: plan.edges.filter((edge) => !deferredNodeRefs.has(edge.from) && !deferredNodeRefs.has(edge.to)),
|
|
64
|
+
};
|
|
65
|
+
const flowLayout = calculatePlanFlowLayout({
|
|
66
|
+
plan: nonDeferredPlan,
|
|
67
|
+
canvasElements: existingElements,
|
|
68
|
+
});
|
|
69
|
+
for (const [ref, position] of flowLayout.positions.entries()) {
|
|
70
|
+
refPositionMap.set(ref, position);
|
|
71
|
+
}
|
|
72
|
+
await traceWriter?.writeRound('flow-layout', {
|
|
73
|
+
roots: flowLayout.roots,
|
|
74
|
+
subtreeHeights: flowLayout.subtreeHeights,
|
|
75
|
+
positions: Object.fromEntries(flowLayout.positions.entries()),
|
|
76
|
+
});
|
|
77
|
+
for (const batch of groupAnalysis.batches) {
|
|
78
|
+
const batchSourceSpec = plan.nodes.find((node) => node.ref === batch.sourceRef);
|
|
79
|
+
if (batchSourceSpec?.type !== 'batch') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const batchSourceId = nodeMap[batch.sourceRef];
|
|
83
|
+
const groupSpecs = batch.groups.map((group) => ({
|
|
84
|
+
childCount: group.childRefs.length,
|
|
85
|
+
childType: group.childType,
|
|
86
|
+
layoutDirection: group.layoutDirection,
|
|
87
|
+
}));
|
|
88
|
+
if (groupSpecs.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
const batchLayout = calculateBatchGroupLayout(session.getElements(), batchSourceId, groupSpecs);
|
|
91
|
+
for (let groupIndex = 0; groupIndex < batch.groups.length; groupIndex += 1) {
|
|
92
|
+
const group = batch.groups[groupIndex];
|
|
93
|
+
const groupLayout = batchLayout.groups[groupIndex];
|
|
94
|
+
if (!groupLayout)
|
|
95
|
+
continue;
|
|
96
|
+
refPositionMap.set(group.headRef, groupLayout.inputPosition);
|
|
97
|
+
for (let childIndex = 0; childIndex < group.childRefs.length; childIndex += 1) {
|
|
98
|
+
if (groupLayout.childPositions[childIndex]) {
|
|
99
|
+
refPositionMap.set(group.childRefs[childIndex], groupLayout.childPositions[childIndex]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const materializedNodes = [];
|
|
105
|
+
for (const node of plan.nodes) {
|
|
106
|
+
if (deferredNodeRefs.has(node.ref)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const existingNodeId = existingNodeByRef.get(node.ref);
|
|
110
|
+
if (existingNodeId) {
|
|
111
|
+
nodeMap[node.ref] = existingNodeId;
|
|
112
|
+
const nextElement = buildNodeElement({
|
|
113
|
+
id: existingNodeId,
|
|
114
|
+
node,
|
|
115
|
+
sourceNodeId: node.dependsOn ? nodeMap[node.dependsOn] : undefined,
|
|
116
|
+
canvasElements: session.getElements(),
|
|
117
|
+
positionOverride: refPositionMap.get(node.ref),
|
|
118
|
+
planId: plan.planId,
|
|
119
|
+
});
|
|
120
|
+
await session.changeElements([
|
|
121
|
+
{
|
|
122
|
+
id: existingNodeId,
|
|
123
|
+
data: nextElement.data,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
materializedNodes.push({
|
|
127
|
+
ref: node.ref,
|
|
128
|
+
id: existingNodeId,
|
|
129
|
+
type: node.type,
|
|
130
|
+
action: 'reused',
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const nodeId = randomUUID().replace(/-/g, '');
|
|
135
|
+
nodeMap[node.ref] = nodeId;
|
|
136
|
+
const sourceNodeId = node.dependsOn ? nodeMap[node.dependsOn] : undefined;
|
|
137
|
+
const layoutInputElements = session.getElements();
|
|
138
|
+
const element = buildNodeElement({
|
|
139
|
+
id: nodeId,
|
|
140
|
+
node,
|
|
141
|
+
sourceNodeId,
|
|
142
|
+
canvasElements: layoutInputElements,
|
|
143
|
+
positionOverride: refPositionMap.get(node.ref),
|
|
144
|
+
planId: plan.planId,
|
|
145
|
+
});
|
|
146
|
+
await session.addElements([element]);
|
|
147
|
+
createdNodeIds.push(nodeId);
|
|
148
|
+
materializedNodes.push({
|
|
149
|
+
ref: node.ref,
|
|
150
|
+
id: nodeId,
|
|
151
|
+
type: node.type,
|
|
152
|
+
action: 'created',
|
|
153
|
+
position: element.position ?? null,
|
|
154
|
+
sourceNodeId: sourceNodeId ?? null,
|
|
155
|
+
layoutStrategy: element.__layoutDebug?.strategy ?? null,
|
|
156
|
+
});
|
|
157
|
+
if (node.type === 'input' &&
|
|
158
|
+
sourceNodeId &&
|
|
159
|
+
!explicitEdgeTargets.has(node.ref)) {
|
|
160
|
+
const implicitResult = await tryCreateImplicitInputSourceEdge(sourceNodeId, nodeId, session, existingEdgeKeys);
|
|
161
|
+
if (implicitResult.createdEdgeId) {
|
|
162
|
+
createdEdgeIds.push(implicitResult.createdEdgeId);
|
|
163
|
+
}
|
|
164
|
+
materializedNodes.push({
|
|
165
|
+
ref: node.ref,
|
|
166
|
+
id: nodeId,
|
|
167
|
+
action: 'implicit_source_to_input_edge',
|
|
168
|
+
edgeId: implicitResult.createdEdgeId ?? null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await traceWriter?.writeRound('node-materialization', {
|
|
173
|
+
createdNodeIds,
|
|
174
|
+
nodeMap,
|
|
175
|
+
deferredNodeRefs: Array.from(deferredNodeRefs),
|
|
176
|
+
operations: materializedNodes,
|
|
177
|
+
});
|
|
178
|
+
// Backfill nodeMap for raw canvas element IDs referenced in edges.
|
|
179
|
+
// Agents may reference existing canvas elements by their raw ID (not a plan ref).
|
|
180
|
+
// Without this, tryCreateEdgeFromSpec cannot resolve sourceId/targetId.
|
|
181
|
+
{
|
|
182
|
+
const allElements = session.getElements();
|
|
183
|
+
const elementIdSet = new Set(allElements.map((el) => el.id));
|
|
184
|
+
for (const edge of plan.edges) {
|
|
185
|
+
for (const ref of [edge.from, edge.to]) {
|
|
186
|
+
if (!nodeMap[ref] && elementIdSet.has(ref)) {
|
|
187
|
+
nodeMap[ref] = ref;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const edgeOperations = [];
|
|
193
|
+
for (const edge of plan.edges) {
|
|
194
|
+
if (deferredNodeRefs.has(edge.from) || deferredNodeRefs.has(edge.to)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (shouldSkipRedundantProcessToProcessEdge(edge, plan)) {
|
|
198
|
+
edgeOperations.push({
|
|
199
|
+
ref: edge.ref || `${edge.from}-${edge.to}`,
|
|
200
|
+
action: 'skipped_redundant_process_edge',
|
|
201
|
+
from: edge.from,
|
|
202
|
+
to: edge.to,
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const edgeResult = await tryCreateEdgeFromSpec({
|
|
207
|
+
edge,
|
|
208
|
+
plan,
|
|
209
|
+
nodeMap,
|
|
210
|
+
session,
|
|
211
|
+
existingEdgeKeys,
|
|
212
|
+
});
|
|
213
|
+
if (edgeResult.createdEdgeId) {
|
|
214
|
+
createdEdgeIds.push(edgeResult.createdEdgeId);
|
|
215
|
+
edgeOperations.push({
|
|
216
|
+
ref: edge.ref || `${edge.from}-${edge.to}`,
|
|
217
|
+
action: 'created',
|
|
218
|
+
edgeId: edgeResult.createdEdgeId,
|
|
219
|
+
from: edge.from,
|
|
220
|
+
to: edge.to,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
edgeOperations.push({
|
|
225
|
+
ref: edge.ref || `${edge.from}-${edge.to}`,
|
|
226
|
+
action: 'reused_or_skipped',
|
|
227
|
+
from: edge.from,
|
|
228
|
+
to: edge.to,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
await traceWriter?.writeRound('explicit-edge-materialization', {
|
|
233
|
+
createdEdgeIds,
|
|
234
|
+
operations: edgeOperations,
|
|
235
|
+
});
|
|
236
|
+
const executableBatches = buildExecutableExecutionBatches(plan, plan.nodes
|
|
237
|
+
.filter((node) => isExecutableNodeType(node.type))
|
|
238
|
+
.map((node) => node.ref)
|
|
239
|
+
.filter((ref) => !batchRefs.has(ref)));
|
|
240
|
+
if (executableBatches.length > 0) {
|
|
241
|
+
executionCompleted = true;
|
|
242
|
+
for (const batch of executableBatches) {
|
|
243
|
+
const batchNodeIds = batch
|
|
244
|
+
.map((ref) => nodeMap[ref])
|
|
245
|
+
.filter((id) => Boolean(id));
|
|
246
|
+
await traceWriter?.writeRound('execute-batch-start', {
|
|
247
|
+
refs: batch,
|
|
248
|
+
nodeIds: batchNodeIds,
|
|
249
|
+
});
|
|
250
|
+
if (batchNodeIds.length === 0)
|
|
251
|
+
continue;
|
|
252
|
+
await session.executeNodes(batchNodeIds);
|
|
253
|
+
// Wait with retry: some nodes (e.g. Refine with logo_design) can take
|
|
254
|
+
// much longer than 5 minutes. Retry up to 3 times (total ~15 min).
|
|
255
|
+
let batchCompleted = false;
|
|
256
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
257
|
+
batchCompleted = await session.waitForCompletion(batchNodeIds, 300_000);
|
|
258
|
+
if (batchCompleted)
|
|
259
|
+
break;
|
|
260
|
+
// Check if any node actually failed (status 400) vs still running
|
|
261
|
+
const allDone = batchNodeIds.every((id) => {
|
|
262
|
+
const el = session.getElement(id);
|
|
263
|
+
const status = el?.data?.status;
|
|
264
|
+
return status === 3 || status === 400;
|
|
265
|
+
});
|
|
266
|
+
if (allDone) {
|
|
267
|
+
batchCompleted = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
executedNodeIds.push(...batchNodeIds);
|
|
272
|
+
if (!batchCompleted) {
|
|
273
|
+
executionCompleted = false;
|
|
274
|
+
}
|
|
275
|
+
await traceWriter?.writeRound('execute-batch-finish', {
|
|
276
|
+
refs: batch,
|
|
277
|
+
nodeIds: batchNodeIds,
|
|
278
|
+
completed: batchCompleted,
|
|
279
|
+
nodeStates: batchNodeIds.map((id) => ({
|
|
280
|
+
id,
|
|
281
|
+
data: session.getElement(id)?.data ?? null,
|
|
282
|
+
})),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const batchResults = await executeBatchNodes({
|
|
287
|
+
plan,
|
|
288
|
+
session,
|
|
289
|
+
nodeMap,
|
|
290
|
+
createdNodeIds,
|
|
291
|
+
createdEdgeIds,
|
|
292
|
+
existingNodeByRef,
|
|
293
|
+
existingEdgeKeys,
|
|
294
|
+
traceWriter,
|
|
295
|
+
});
|
|
296
|
+
if (batchResults.executedNodeIds.length > 0) {
|
|
297
|
+
executedNodeIds = [...executedNodeIds, ...batchResults.executedNodeIds];
|
|
298
|
+
executionCompleted =
|
|
299
|
+
executionCompleted === false || batchResults.executionCompleted === false
|
|
300
|
+
? false
|
|
301
|
+
: executionCompleted === true || batchResults.executionCompleted === true
|
|
302
|
+
? true
|
|
303
|
+
: executionCompleted;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
createdNodeIds,
|
|
307
|
+
createdEdgeIds,
|
|
308
|
+
nodeMap,
|
|
309
|
+
executedNodeIds,
|
|
310
|
+
executionCompleted,
|
|
311
|
+
traceDirectory: traceWriter?.directory,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function isExecutableNodeType(type) {
|
|
315
|
+
return type === 'process' || type === 'refine' || type === 'crop';
|
|
316
|
+
}
|
|
317
|
+
function buildExecutableExecutionBatches(plan, executableRefs) {
|
|
318
|
+
const uniqueExecutableRefs = Array.from(new Set(executableRefs));
|
|
319
|
+
if (uniqueExecutableRefs.length === 0) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const nodeOrder = new Map();
|
|
323
|
+
plan.nodes.forEach((node, index) => nodeOrder.set(node.ref, index));
|
|
324
|
+
const incomingRefs = buildIncomingRefsMap(plan);
|
|
325
|
+
const executableRefSet = new Set(uniqueExecutableRefs);
|
|
326
|
+
const inDegree = new Map();
|
|
327
|
+
const dependents = new Map();
|
|
328
|
+
for (const ref of uniqueExecutableRefs) {
|
|
329
|
+
inDegree.set(ref, 0);
|
|
330
|
+
dependents.set(ref, new Set());
|
|
331
|
+
}
|
|
332
|
+
for (const ref of uniqueExecutableRefs) {
|
|
333
|
+
const upstreamExecutableRefs = collectUpstreamExecutableDependencies(ref, incomingRefs, executableRefSet);
|
|
334
|
+
inDegree.set(ref, upstreamExecutableRefs.size);
|
|
335
|
+
for (const dependencyRef of upstreamExecutableRefs) {
|
|
336
|
+
dependents.get(dependencyRef)?.add(ref);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const sortByPlanOrder = (refs) => refs.sort((a, b) => (nodeOrder.get(a) ?? Number.MAX_SAFE_INTEGER) -
|
|
340
|
+
(nodeOrder.get(b) ?? Number.MAX_SAFE_INTEGER));
|
|
341
|
+
let readyRefs = sortByPlanOrder(uniqueExecutableRefs.filter((ref) => (inDegree.get(ref) || 0) === 0));
|
|
342
|
+
const executedRefs = new Set();
|
|
343
|
+
const batches = [];
|
|
344
|
+
while (readyRefs.length > 0) {
|
|
345
|
+
const currentBatch = [...readyRefs];
|
|
346
|
+
batches.push(currentBatch);
|
|
347
|
+
readyRefs = [];
|
|
348
|
+
const nextReadyRefs = new Set();
|
|
349
|
+
for (const ref of currentBatch) {
|
|
350
|
+
executedRefs.add(ref);
|
|
351
|
+
const downstreamRefs = dependents.get(ref);
|
|
352
|
+
if (!downstreamRefs)
|
|
353
|
+
continue;
|
|
354
|
+
for (const downstreamRef of downstreamRefs) {
|
|
355
|
+
const nextInDegree = (inDegree.get(downstreamRef) || 0) - 1;
|
|
356
|
+
inDegree.set(downstreamRef, nextInDegree);
|
|
357
|
+
if (nextInDegree === 0 && !executedRefs.has(downstreamRef)) {
|
|
358
|
+
nextReadyRefs.add(downstreamRef);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
readyRefs = sortByPlanOrder(Array.from(nextReadyRefs));
|
|
363
|
+
}
|
|
364
|
+
if (executedRefs.size < uniqueExecutableRefs.length) {
|
|
365
|
+
const unresolvedRefs = sortByPlanOrder(uniqueExecutableRefs.filter((ref) => !executedRefs.has(ref)));
|
|
366
|
+
unresolvedRefs.forEach((ref) => batches.push([ref]));
|
|
367
|
+
}
|
|
368
|
+
return batches;
|
|
369
|
+
}
|
|
370
|
+
function buildIncomingRefsMap(plan) {
|
|
371
|
+
const incomingRefs = new Map();
|
|
372
|
+
for (const node of plan.nodes) {
|
|
373
|
+
incomingRefs.set(node.ref, new Set());
|
|
374
|
+
}
|
|
375
|
+
for (const edge of plan.edges) {
|
|
376
|
+
incomingRefs.get(edge.to)?.add(edge.from);
|
|
377
|
+
}
|
|
378
|
+
for (const node of plan.nodes) {
|
|
379
|
+
if (node.dependsOn) {
|
|
380
|
+
incomingRefs.get(node.ref)?.add(node.dependsOn);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return incomingRefs;
|
|
384
|
+
}
|
|
385
|
+
function collectUpstreamExecutableDependencies(targetRef, incomingRefs, executableRefSet) {
|
|
386
|
+
const dependencies = new Set();
|
|
387
|
+
const visitedRefs = new Set();
|
|
388
|
+
const queue = Array.from(incomingRefs.get(targetRef) || []);
|
|
389
|
+
while (queue.length > 0) {
|
|
390
|
+
const currentRef = queue.shift();
|
|
391
|
+
if (!currentRef || currentRef === targetRef || visitedRefs.has(currentRef)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
visitedRefs.add(currentRef);
|
|
395
|
+
if (executableRefSet.has(currentRef)) {
|
|
396
|
+
dependencies.add(currentRef);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const parentRefs = incomingRefs.get(currentRef);
|
|
400
|
+
if (!parentRefs || parentRefs.size === 0)
|
|
401
|
+
continue;
|
|
402
|
+
for (const parentRef of parentRefs) {
|
|
403
|
+
if (!visitedRefs.has(parentRef)) {
|
|
404
|
+
queue.push(parentRef);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return dependencies;
|
|
409
|
+
}
|
|
410
|
+
async function executeBatchNodes(args) {
|
|
411
|
+
const { plan, session, nodeMap, createdNodeIds, createdEdgeIds, existingNodeByRef, existingEdgeKeys, traceWriter, } = args;
|
|
412
|
+
const executedNodeIds = [];
|
|
413
|
+
let executionCompleted = null;
|
|
414
|
+
const selectorRefs = new Set(plan.nodes
|
|
415
|
+
.filter((node) => node.type === 'selector')
|
|
416
|
+
.map((node) => node.ref));
|
|
417
|
+
for (const batchNode of plan.nodes.filter((node) => node.type === 'batch')) {
|
|
418
|
+
const sourceRef = batchNode.dependsOn;
|
|
419
|
+
if (!sourceRef) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const sourceId = nodeMap[sourceRef];
|
|
423
|
+
const sourceElement = sourceId ? session.getElement(sourceId) : undefined;
|
|
424
|
+
if (!sourceId || !sourceElement) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const outputs = extractBatchOutputs(sourceElement);
|
|
428
|
+
if (outputs.length === 0) {
|
|
429
|
+
await traceWriter?.writeRound('batch-no-outputs', {
|
|
430
|
+
batchRef: batchNode.ref,
|
|
431
|
+
sourceRef,
|
|
432
|
+
sourceId,
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const downstreamEdge = plan.edges.find((edge) => edge.from === batchNode.ref);
|
|
437
|
+
const downstreamSelector = downstreamEdge && selectorRefs.has(downstreamEdge.to)
|
|
438
|
+
? plan.nodes.find((node) => node.ref === downstreamEdge.to)
|
|
439
|
+
: undefined;
|
|
440
|
+
const models = Array.isArray(batchNode.data.models) && batchNode.data.models.length > 0
|
|
441
|
+
? batchNode.data.models
|
|
442
|
+
: [{ model: batchNode.data.current_model || batchNode.data.model || 'default' }];
|
|
443
|
+
let selectorId;
|
|
444
|
+
const batchProcessIds = [];
|
|
445
|
+
await traceWriter?.writeRound('batch-prepare', {
|
|
446
|
+
batchRef: batchNode.ref,
|
|
447
|
+
sourceRef,
|
|
448
|
+
sourceId,
|
|
449
|
+
outputCount: outputs.length,
|
|
450
|
+
downstreamSelectorRef: downstreamSelector?.ref ?? null,
|
|
451
|
+
models,
|
|
452
|
+
});
|
|
453
|
+
// Pre-calculate layout positions for all batch-expanded nodes.
|
|
454
|
+
// Each output produces 1 input + N process nodes (one per model).
|
|
455
|
+
const batchPositionMap = new Map();
|
|
456
|
+
{
|
|
457
|
+
const groupSpecs = outputs.map(() => ({
|
|
458
|
+
childCount: models.length,
|
|
459
|
+
childType: 'process',
|
|
460
|
+
layoutDirection: 'horizontal',
|
|
461
|
+
}));
|
|
462
|
+
const batchLayout = calculateBatchGroupLayout(session.getElements(), sourceId, groupSpecs);
|
|
463
|
+
for (let outputIdx = 0; outputIdx < outputs.length; outputIdx += 1) {
|
|
464
|
+
const groupResult = batchLayout.groups[outputIdx];
|
|
465
|
+
if (!groupResult)
|
|
466
|
+
continue;
|
|
467
|
+
const inputRef = `${batchNode.ref}_input_${outputs[outputIdx].index}`;
|
|
468
|
+
batchPositionMap.set(inputRef, groupResult.inputPosition);
|
|
469
|
+
let modelIdx = 0;
|
|
470
|
+
for (const modelSpec of models) {
|
|
471
|
+
const processRef = typeof modelSpec === 'string'
|
|
472
|
+
? `${batchNode.ref}_process_${outputs[outputIdx].index}_${sanitizeRefFragment(modelSpec)}`
|
|
473
|
+
: `${batchNode.ref}_process_${outputs[outputIdx].index}_${sanitizeRefFragment(modelSpec.model)}`;
|
|
474
|
+
if (groupResult.childPositions[modelIdx]) {
|
|
475
|
+
batchPositionMap.set(processRef, groupResult.childPositions[modelIdx]);
|
|
476
|
+
}
|
|
477
|
+
modelIdx += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (const output of outputs) {
|
|
482
|
+
const inputRef = `${batchNode.ref}_input_${output.index}`;
|
|
483
|
+
const existingInputId = existingNodeByRef.get(inputRef) ||
|
|
484
|
+
findExistingBatchInputId(session.getElements(), sourceId, output.index);
|
|
485
|
+
const inputId = existingInputId || randomUUID().replace(/-/g, '');
|
|
486
|
+
nodeMap[inputRef] = inputId;
|
|
487
|
+
const inputNode = buildNodeElement({
|
|
488
|
+
id: inputId,
|
|
489
|
+
node: {
|
|
490
|
+
ref: inputRef,
|
|
491
|
+
type: 'input',
|
|
492
|
+
data: {
|
|
493
|
+
prompt: output.prompt,
|
|
494
|
+
},
|
|
495
|
+
dependsOn: sourceRef,
|
|
496
|
+
},
|
|
497
|
+
sourceNodeId: sourceId,
|
|
498
|
+
canvasElements: session.getElements(),
|
|
499
|
+
planId: plan.planId,
|
|
500
|
+
positionOverride: batchPositionMap.get(inputRef),
|
|
501
|
+
});
|
|
502
|
+
if (existingInputId) {
|
|
503
|
+
await session.changeElements([{ id: inputId, data: inputNode.data }]);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
await session.addElements([inputNode]);
|
|
507
|
+
createdNodeIds.push(inputId);
|
|
508
|
+
}
|
|
509
|
+
const inputTarget = ensureInputTargetHandle(inputNode, sourceElement, sourceId, output.index);
|
|
510
|
+
await session.changeElements([inputTarget.change]);
|
|
511
|
+
const sourceToInput = await tryCreateEdgeFromSpec({
|
|
512
|
+
edge: {
|
|
513
|
+
ref: `${batchNode.ref}:source:${output.index}`,
|
|
514
|
+
from: sourceRef,
|
|
515
|
+
to: inputRef,
|
|
516
|
+
fromHandle: output.index,
|
|
517
|
+
toHandle: inputTarget.handle,
|
|
518
|
+
},
|
|
519
|
+
plan,
|
|
520
|
+
nodeMap,
|
|
521
|
+
session,
|
|
522
|
+
existingEdgeKeys,
|
|
523
|
+
});
|
|
524
|
+
if (sourceToInput.createdEdgeId) {
|
|
525
|
+
createdEdgeIds.push(sourceToInput.createdEdgeId);
|
|
526
|
+
}
|
|
527
|
+
const existingProcessIds = findExistingBatchProcessIds(session.getElements(), inputId);
|
|
528
|
+
let modelIndex = 0;
|
|
529
|
+
for (const modelSpec of models) {
|
|
530
|
+
const processRef = typeof modelSpec === 'string'
|
|
531
|
+
? `${batchNode.ref}_process_${output.index}_${sanitizeRefFragment(modelSpec)}`
|
|
532
|
+
: `${batchNode.ref}_process_${output.index}_${sanitizeRefFragment(modelSpec.model)}`;
|
|
533
|
+
const processId = existingNodeByRef.get(processRef) ||
|
|
534
|
+
existingProcessIds[modelIndex] ||
|
|
535
|
+
randomUUID().replace(/-/g, '');
|
|
536
|
+
nodeMap[processRef] = processId;
|
|
537
|
+
const processNode = buildNodeElement({
|
|
538
|
+
id: processId,
|
|
539
|
+
node: {
|
|
540
|
+
ref: processRef,
|
|
541
|
+
type: 'process',
|
|
542
|
+
data: {
|
|
543
|
+
...batchNode.data,
|
|
544
|
+
prompt: output.prompt,
|
|
545
|
+
model: typeof modelSpec === 'string'
|
|
546
|
+
? modelSpec
|
|
547
|
+
: modelSpec.model,
|
|
548
|
+
current_model: typeof modelSpec === 'string'
|
|
549
|
+
? modelSpec
|
|
550
|
+
: modelSpec.model,
|
|
551
|
+
ratio: typeof modelSpec === 'object' && modelSpec.ratio
|
|
552
|
+
? modelSpec.ratio
|
|
553
|
+
: batchNode.data.ratio,
|
|
554
|
+
resolution: typeof modelSpec === 'object' && modelSpec.resolution
|
|
555
|
+
? modelSpec.resolution
|
|
556
|
+
: batchNode.data.resolution,
|
|
557
|
+
count: typeof modelSpec === 'object' && modelSpec.count
|
|
558
|
+
? modelSpec.count
|
|
559
|
+
: batchNode.data.count,
|
|
560
|
+
},
|
|
561
|
+
dependsOn: inputRef,
|
|
562
|
+
},
|
|
563
|
+
sourceNodeId: inputId,
|
|
564
|
+
canvasElements: session.getElements(),
|
|
565
|
+
planId: plan.planId,
|
|
566
|
+
positionOverride: batchPositionMap.get(processRef),
|
|
567
|
+
});
|
|
568
|
+
if (existingNodeByRef.has(processRef) || existingProcessIds[modelIndex]) {
|
|
569
|
+
await session.changeElements([
|
|
570
|
+
{
|
|
571
|
+
id: processId,
|
|
572
|
+
data: {
|
|
573
|
+
...processNode.data,
|
|
574
|
+
status: 0,
|
|
575
|
+
executed: false,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
await session.addElements([processNode]);
|
|
582
|
+
createdNodeIds.push(processId);
|
|
583
|
+
}
|
|
584
|
+
batchProcessIds.push(processId);
|
|
585
|
+
const inputToProcess = await tryCreateEdgeFromSpec({
|
|
586
|
+
edge: {
|
|
587
|
+
ref: `${inputRef}->${processRef}`,
|
|
588
|
+
from: inputRef,
|
|
589
|
+
to: processRef,
|
|
590
|
+
},
|
|
591
|
+
plan,
|
|
592
|
+
nodeMap,
|
|
593
|
+
session,
|
|
594
|
+
existingEdgeKeys,
|
|
595
|
+
});
|
|
596
|
+
if (inputToProcess.createdEdgeId) {
|
|
597
|
+
createdEdgeIds.push(inputToProcess.createdEdgeId);
|
|
598
|
+
}
|
|
599
|
+
modelIndex += 1;
|
|
600
|
+
}
|
|
601
|
+
await traceWriter?.writeRound('batch-output-materialized', {
|
|
602
|
+
batchRef: batchNode.ref,
|
|
603
|
+
outputIndex: output.index,
|
|
604
|
+
inputRef,
|
|
605
|
+
inputId,
|
|
606
|
+
processIds: [...batchProcessIds],
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
if (batchProcessIds.length > 0) {
|
|
610
|
+
await traceWriter?.writeRound('batch-run-start', {
|
|
611
|
+
batchRef: batchNode.ref,
|
|
612
|
+
batchProcessIds,
|
|
613
|
+
});
|
|
614
|
+
await session.executeNodes(batchProcessIds);
|
|
615
|
+
let batchCompleted = false;
|
|
616
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
617
|
+
batchCompleted = await session.waitForCompletion(batchProcessIds, 300_000);
|
|
618
|
+
if (batchCompleted)
|
|
619
|
+
break;
|
|
620
|
+
const allDone = batchProcessIds.every((id) => {
|
|
621
|
+
const el = session.getElement(id);
|
|
622
|
+
const status = el?.data?.status;
|
|
623
|
+
return status === 3 || status === 400;
|
|
624
|
+
});
|
|
625
|
+
if (allDone) {
|
|
626
|
+
batchCompleted = true;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
executedNodeIds.push(...batchProcessIds);
|
|
631
|
+
executionCompleted =
|
|
632
|
+
executionCompleted === false || batchCompleted === false
|
|
633
|
+
? false
|
|
634
|
+
: true;
|
|
635
|
+
await traceWriter?.writeRound('batch-run-finish', {
|
|
636
|
+
batchRef: batchNode.ref,
|
|
637
|
+
batchProcessIds,
|
|
638
|
+
completed: batchCompleted,
|
|
639
|
+
outputs: batchProcessIds.map((id) => ({
|
|
640
|
+
id,
|
|
641
|
+
medias: collectProcessOutputMedias(session.getElement(id)),
|
|
642
|
+
})),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (selectorId === undefined && downstreamSelector && batchProcessIds.length > 0) {
|
|
646
|
+
// All batch processes are done — the Selector's upstream is now ready.
|
|
647
|
+
selectorId =
|
|
648
|
+
existingNodeByRef.get(downstreamSelector.ref) ||
|
|
649
|
+
randomUUID().replace(/-/g, '');
|
|
650
|
+
nodeMap[downstreamSelector.ref] = selectorId;
|
|
651
|
+
// Position selector to the right of the rightmost batch process node
|
|
652
|
+
const batchProcessElements = batchProcessIds
|
|
653
|
+
.map((id) => session.getElement(id))
|
|
654
|
+
.filter((el) => el !== undefined);
|
|
655
|
+
const rightmostX = batchProcessElements.reduce((max, el) => {
|
|
656
|
+
const x = (el.position?.x ?? 0) + (el.measured?.width ?? 260);
|
|
657
|
+
return x > max ? x : max;
|
|
658
|
+
}, 0);
|
|
659
|
+
const avgY = batchProcessElements.length > 0
|
|
660
|
+
? batchProcessElements.reduce((sum, el) => sum + (el.position?.y ?? 0), 0) /
|
|
661
|
+
batchProcessElements.length
|
|
662
|
+
: 0;
|
|
663
|
+
const selectorElement = buildNodeElement({
|
|
664
|
+
id: selectorId,
|
|
665
|
+
node: downstreamSelector,
|
|
666
|
+
sourceNodeId: sourceId,
|
|
667
|
+
canvasElements: session.getElements(),
|
|
668
|
+
positionOverride: { x: rightmostX + 100, y: avgY },
|
|
669
|
+
planId: plan.planId,
|
|
670
|
+
});
|
|
671
|
+
if (existingNodeByRef.has(downstreamSelector.ref)) {
|
|
672
|
+
await session.changeElements([{ id: selectorId, data: selectorElement.data }]);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
await session.addElements([selectorElement]);
|
|
676
|
+
createdNodeIds.push(selectorId);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (downstreamSelector && nodeMap[downstreamSelector.ref]) {
|
|
680
|
+
selectorId = nodeMap[downstreamSelector.ref];
|
|
681
|
+
}
|
|
682
|
+
if (selectorId && batchProcessIds.length > 0) {
|
|
683
|
+
const selectorElement = session.getElement(selectorId);
|
|
684
|
+
if (selectorElement) {
|
|
685
|
+
for (const processId of batchProcessIds) {
|
|
686
|
+
const processElement = session.getElement(processId);
|
|
687
|
+
const outputMedias = collectProcessOutputMedias(processElement);
|
|
688
|
+
// If process has real output medias, wire them
|
|
689
|
+
if (outputMedias.length > 0) {
|
|
690
|
+
for (const outputMedia of outputMedias) {
|
|
691
|
+
const fromHandle = typeof outputMedia.id === 'string' ? outputMedia.id : undefined;
|
|
692
|
+
const selectorTarget = ensureSelectorTargetHandle(selectorElement, processElement || selectorElement, processId, fromHandle);
|
|
693
|
+
await session.changeElements([selectorTarget.change]);
|
|
694
|
+
// Create edge directly from process to selector using real IDs
|
|
695
|
+
const edgeId = randomUUID().replace(/-/g, '');
|
|
696
|
+
const edgeKey = buildEdgeKey(processId, selectorId, fromHandle, selectorTarget.handle);
|
|
697
|
+
if (!existingEdgeKeys.has(edgeKey)) {
|
|
698
|
+
const edgeElement = buildEdgeElement({
|
|
699
|
+
id: edgeId,
|
|
700
|
+
edge: {
|
|
701
|
+
from: processId,
|
|
702
|
+
to: selectorId,
|
|
703
|
+
fromHandle,
|
|
704
|
+
toHandle: selectorTarget.handle,
|
|
705
|
+
},
|
|
706
|
+
sourceId: processId,
|
|
707
|
+
targetId: selectorId,
|
|
708
|
+
});
|
|
709
|
+
await session.addElements([edgeElement]);
|
|
710
|
+
existingEdgeKeys.add(edgeKey);
|
|
711
|
+
createdEdgeIds.push(edgeId);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
// No real outputs yet — create placeholder handle
|
|
717
|
+
const selectorTarget = ensureSelectorTargetHandle(selectorElement, processElement || selectorElement, processId, undefined);
|
|
718
|
+
await session.changeElements([selectorTarget.change]);
|
|
719
|
+
const edgeKey = buildEdgeKey(processId, selectorId, undefined, selectorTarget.handle);
|
|
720
|
+
if (!existingEdgeKeys.has(edgeKey)) {
|
|
721
|
+
const edgeId = randomUUID().replace(/-/g, '');
|
|
722
|
+
const edgeElement = buildEdgeElement({
|
|
723
|
+
id: edgeId,
|
|
724
|
+
edge: {
|
|
725
|
+
from: processId,
|
|
726
|
+
to: selectorId,
|
|
727
|
+
toHandle: selectorTarget.handle,
|
|
728
|
+
},
|
|
729
|
+
sourceId: processId,
|
|
730
|
+
targetId: selectorId,
|
|
731
|
+
});
|
|
732
|
+
await session.addElements([edgeElement]);
|
|
733
|
+
existingEdgeKeys.add(edgeKey);
|
|
734
|
+
createdEdgeIds.push(edgeId);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
await traceWriter?.writeRound('batch-selector-updated', {
|
|
740
|
+
batchRef: batchNode.ref,
|
|
741
|
+
selectorId,
|
|
742
|
+
selectorState: session.getElement(selectorId)?.data ?? null,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return { executedNodeIds, executionCompleted };
|
|
747
|
+
}
|
|
748
|
+
function extractBatchOutputs(sourceElement) {
|
|
749
|
+
const sourceData = asRecord(sourceElement.data);
|
|
750
|
+
const multiOutputs = asRecord(sourceData.multi_outputs);
|
|
751
|
+
const outputKeys = Object.keys(multiOutputs).sort();
|
|
752
|
+
if (outputKeys.length > 0) {
|
|
753
|
+
return outputKeys.map((key) => {
|
|
754
|
+
const output = asRecord(multiOutputs[key]);
|
|
755
|
+
return {
|
|
756
|
+
index: key,
|
|
757
|
+
prompt: typeof output.prompt === 'string' ? output.prompt : '',
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (typeof sourceData.output_text === 'string' && sourceData.output_text.trim()) {
|
|
762
|
+
const separator = typeof sourceData.separator === 'string' && sourceData.separator
|
|
763
|
+
? sourceData.separator
|
|
764
|
+
: '#@';
|
|
765
|
+
return sourceData.output_text
|
|
766
|
+
.split(separator)
|
|
767
|
+
.map((part) => part.trim())
|
|
768
|
+
.filter(Boolean)
|
|
769
|
+
.map((prompt, index) => ({
|
|
770
|
+
index: String(index),
|
|
771
|
+
prompt,
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
function collectProcessOutputMedias(processElement) {
|
|
777
|
+
if (!processElement?.data) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
const processData = asRecord(processElement.data);
|
|
781
|
+
if (Array.isArray(processData.medias)) {
|
|
782
|
+
return processData.medias
|
|
783
|
+
.map((media) => asRecord(media))
|
|
784
|
+
.filter((media) => typeof media.url === 'string');
|
|
785
|
+
}
|
|
786
|
+
const medias = asRecord(processData.medias);
|
|
787
|
+
const values = Object.values(medias)
|
|
788
|
+
.map((media) => asRecord(media))
|
|
789
|
+
.filter((media) => typeof media.url === 'string');
|
|
790
|
+
if (values.length > 0) {
|
|
791
|
+
return values;
|
|
792
|
+
}
|
|
793
|
+
if (typeof processData.url === 'string' && processData.url) {
|
|
794
|
+
return [
|
|
795
|
+
{
|
|
796
|
+
id: processElement.id,
|
|
797
|
+
name: typeof processData.name === 'string' ? processData.name : 'image',
|
|
798
|
+
type: typeof processData.type === 'string' ? processData.type : 'image',
|
|
799
|
+
url: processData.url,
|
|
800
|
+
width: typeof processData.width === 'number' ? processData.width : 1024,
|
|
801
|
+
height: typeof processData.height === 'number' ? processData.height : 1024,
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
return [];
|
|
806
|
+
}
|
|
807
|
+
function resolveDefaultSourceHandle(sourceElement) {
|
|
808
|
+
if (sourceElement.type === CANVAS_NODE_TYPES.PROCESS ||
|
|
809
|
+
sourceElement.type === CANVAS_NODE_TYPES.CROP ||
|
|
810
|
+
sourceElement.type === CANVAS_NODE_TYPES.IMAGE ||
|
|
811
|
+
sourceElement.type === CANVAS_NODE_TYPES.VIDEO ||
|
|
812
|
+
sourceElement.type === CANVAS_NODE_TYPES.RESULT) {
|
|
813
|
+
const media = resolveSourceMedia(sourceElement);
|
|
814
|
+
return typeof media?.id === 'string' ? media.id : undefined;
|
|
815
|
+
}
|
|
816
|
+
if (sourceElement.type === CANVAS_NODE_TYPES.REFINE) {
|
|
817
|
+
const sourceData = asRecord(sourceElement.data);
|
|
818
|
+
const outputs = asRecord(sourceData.multi_outputs);
|
|
819
|
+
const keys = Object.keys(outputs);
|
|
820
|
+
return keys.length > 0 ? keys[0] : undefined;
|
|
821
|
+
}
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
function normalizeSourceHandle(sourceElement, fromHandle) {
|
|
825
|
+
if (!fromHandle) {
|
|
826
|
+
return fromHandle;
|
|
827
|
+
}
|
|
828
|
+
if (sourceElement.type === CANVAS_NODE_TYPES.SELECTOR) {
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
return fromHandle;
|
|
832
|
+
}
|
|
833
|
+
function shouldSkipRedundantProcessToProcessEdge(edge, plan) {
|
|
834
|
+
const sourceNode = plan.nodes.find((node) => node.ref === edge.from);
|
|
835
|
+
const targetNode = plan.nodes.find((node) => node.ref === edge.to);
|
|
836
|
+
if (sourceNode?.type !== 'process' || targetNode?.type !== 'process') {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
return plan.edges.some((firstHop) => {
|
|
840
|
+
if (firstHop.from !== edge.from || firstHop.to === edge.to) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
const middleNode = plan.nodes.find((node) => node.ref === firstHop.to);
|
|
844
|
+
if (middleNode?.type !== 'input') {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
return plan.edges.some((secondHop) => secondHop.from === middleNode.ref && secondHop.to === edge.to);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
async function tryCreateEdgeFromSpec(args) {
|
|
851
|
+
const { edge, nodeMap, session, existingEdgeKeys } = args;
|
|
852
|
+
const sourceId = nodeMap[edge.from];
|
|
853
|
+
const targetId = nodeMap[edge.to];
|
|
854
|
+
if (!sourceId || !targetId) {
|
|
855
|
+
return {};
|
|
856
|
+
}
|
|
857
|
+
const sourceElement = session.getElement(sourceId);
|
|
858
|
+
const targetElement = session.getElement(targetId);
|
|
859
|
+
if (!sourceElement || !targetElement) {
|
|
860
|
+
return {};
|
|
861
|
+
}
|
|
862
|
+
let fromHandle = normalizeSourceHandle(sourceElement, edge.fromHandle ?? resolveDefaultSourceHandle(sourceElement));
|
|
863
|
+
let toHandle = edge.toHandle;
|
|
864
|
+
let targetUpdateChange = null;
|
|
865
|
+
if (targetElement.type === CANVAS_NODE_TYPES.SELECTOR && !toHandle) {
|
|
866
|
+
const selectorTarget = ensureSelectorTargetHandle(targetElement, sourceElement, sourceId, fromHandle);
|
|
867
|
+
toHandle = selectorTarget.handle;
|
|
868
|
+
targetUpdateChange = selectorTarget.change;
|
|
869
|
+
}
|
|
870
|
+
if (targetElement.type === CANVAS_NODE_TYPES.INPUT && !toHandle) {
|
|
871
|
+
const inputTarget = ensureInputTargetHandle(targetElement, sourceElement, sourceId, fromHandle);
|
|
872
|
+
toHandle = inputTarget.handle;
|
|
873
|
+
targetUpdateChange = inputTarget.change;
|
|
874
|
+
}
|
|
875
|
+
const edgeKey = buildEdgeKey(sourceId, targetId, fromHandle, toHandle);
|
|
876
|
+
if (existingEdgeKeys.has(edgeKey)) {
|
|
877
|
+
return {};
|
|
878
|
+
}
|
|
879
|
+
if (targetUpdateChange) {
|
|
880
|
+
await session.changeElements([targetUpdateChange]);
|
|
881
|
+
}
|
|
882
|
+
const edgeId = randomUUID().replace(/-/g, '');
|
|
883
|
+
const edgeElement = buildEdgeElement({
|
|
884
|
+
id: edgeId,
|
|
885
|
+
edge: {
|
|
886
|
+
...edge,
|
|
887
|
+
fromHandle,
|
|
888
|
+
toHandle,
|
|
889
|
+
},
|
|
890
|
+
sourceId,
|
|
891
|
+
targetId,
|
|
892
|
+
});
|
|
893
|
+
await session.addElements([edgeElement]);
|
|
894
|
+
existingEdgeKeys.add(edgeKey);
|
|
895
|
+
return { createdEdgeId: edgeId };
|
|
896
|
+
}
|
|
897
|
+
async function tryCreateImplicitInputSourceEdge(sourceId, targetId, session, existingEdgeKeys) {
|
|
898
|
+
const sourceElement = session.getElement(sourceId);
|
|
899
|
+
const targetElement = session.getElement(targetId);
|
|
900
|
+
if (!sourceElement || !targetElement) {
|
|
901
|
+
return {};
|
|
902
|
+
}
|
|
903
|
+
let fromHandle = normalizeSourceHandle(sourceElement, resolveDefaultSourceHandle(sourceElement));
|
|
904
|
+
const inputTarget = ensureInputTargetHandle(targetElement, sourceElement, sourceId, fromHandle);
|
|
905
|
+
fromHandle = normalizeSourceHandle(sourceElement, fromHandle);
|
|
906
|
+
const edgeKey = buildEdgeKey(sourceId, targetId, fromHandle, inputTarget.handle);
|
|
907
|
+
if (existingEdgeKeys.has(edgeKey)) {
|
|
908
|
+
return {};
|
|
909
|
+
}
|
|
910
|
+
await session.changeElements([inputTarget.change]);
|
|
911
|
+
const edgeId = randomUUID().replace(/-/g, '');
|
|
912
|
+
const edgeElement = buildEdgeElement({
|
|
913
|
+
id: edgeId,
|
|
914
|
+
edge: {
|
|
915
|
+
from: sourceId,
|
|
916
|
+
to: targetId,
|
|
917
|
+
fromHandle,
|
|
918
|
+
toHandle: inputTarget.handle,
|
|
919
|
+
},
|
|
920
|
+
sourceId,
|
|
921
|
+
targetId,
|
|
922
|
+
});
|
|
923
|
+
await session.addElements([edgeElement]);
|
|
924
|
+
existingEdgeKeys.add(edgeKey);
|
|
925
|
+
return { createdEdgeId: edgeId };
|
|
926
|
+
}
|
|
927
|
+
function ensureSelectorTargetHandle(targetElement, sourceElement, sourceId, fromHandle) {
|
|
928
|
+
const targetData = asRecord(targetElement.data);
|
|
929
|
+
const currentMedias = asRecord(targetData.medias);
|
|
930
|
+
const existingHandle = findExistingSelectorHandle(currentMedias, sourceId, fromHandle);
|
|
931
|
+
if (existingHandle) {
|
|
932
|
+
return {
|
|
933
|
+
handle: existingHandle,
|
|
934
|
+
change: {
|
|
935
|
+
id: targetElement.id,
|
|
936
|
+
data: {
|
|
937
|
+
medias: currentMedias,
|
|
938
|
+
selectedId: typeof targetData.selectedId === 'string' && targetData.selectedId.length > 0
|
|
939
|
+
? targetData.selectedId
|
|
940
|
+
: existingHandle,
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const handle = randomUUID().replace(/-/g, '');
|
|
946
|
+
const sourceMedia = resolveSourceMedia(sourceElement, fromHandle);
|
|
947
|
+
const mediaType = sourceMedia?.type === 'video' ? 'video' : 'image';
|
|
948
|
+
currentMedias[handle] = {
|
|
949
|
+
id: handle,
|
|
950
|
+
name: sourceMedia?.name || mediaType,
|
|
951
|
+
type: mediaType,
|
|
952
|
+
url: sourceMedia?.url || '',
|
|
953
|
+
width: sourceMedia?.width || 1024,
|
|
954
|
+
height: sourceMedia?.height || 1024,
|
|
955
|
+
source: sourceId,
|
|
956
|
+
};
|
|
957
|
+
return {
|
|
958
|
+
handle,
|
|
959
|
+
change: {
|
|
960
|
+
id: targetElement.id,
|
|
961
|
+
data: {
|
|
962
|
+
medias: currentMedias,
|
|
963
|
+
selectedId: typeof targetData.selectedId === 'string' && targetData.selectedId.length > 0
|
|
964
|
+
? targetData.selectedId
|
|
965
|
+
: handle,
|
|
966
|
+
},
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function ensureInputTargetHandle(targetElement, sourceElement, sourceId, fromHandle) {
|
|
971
|
+
const targetData = asRecord(targetElement.data);
|
|
972
|
+
if (sourceElement.type === CANVAS_NODE_TYPES.REFINE) {
|
|
973
|
+
const currentAddons = asRecord(targetData.addons);
|
|
974
|
+
const existingAddonHandle = findExistingAddonHandle(currentAddons, sourceId, fromHandle);
|
|
975
|
+
if (existingAddonHandle) {
|
|
976
|
+
currentAddons[existingAddonHandle] = {
|
|
977
|
+
...asRecord(currentAddons[existingAddonHandle]),
|
|
978
|
+
prompt: resolveSourcePrompt(sourceElement, fromHandle),
|
|
979
|
+
};
|
|
980
|
+
return {
|
|
981
|
+
handle: existingAddonHandle,
|
|
982
|
+
change: {
|
|
983
|
+
id: targetElement.id,
|
|
984
|
+
data: {
|
|
985
|
+
addons: currentAddons,
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const handle = randomUUID().replace(/-/g, '');
|
|
991
|
+
currentAddons[handle] = {
|
|
992
|
+
id: handle,
|
|
993
|
+
index: Object.keys(currentAddons).length + 1,
|
|
994
|
+
prompt: resolveSourcePrompt(sourceElement, fromHandle),
|
|
995
|
+
source: sourceId,
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
handle,
|
|
999
|
+
change: {
|
|
1000
|
+
id: targetElement.id,
|
|
1001
|
+
data: {
|
|
1002
|
+
addons: currentAddons,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const sourceMedia = resolveSourceMedia(sourceElement, fromHandle);
|
|
1008
|
+
const mediaType = sourceMedia?.type === 'video' ? 'video' : 'image';
|
|
1009
|
+
const currentMedias = asRecord(targetData.medias);
|
|
1010
|
+
const existingMediaHandle = findExistingInputMediaHandle(currentMedias, sourceId, fromHandle);
|
|
1011
|
+
if (existingMediaHandle) {
|
|
1012
|
+
currentMedias[existingMediaHandle] = {
|
|
1013
|
+
...asRecord(currentMedias[existingMediaHandle]),
|
|
1014
|
+
name: sourceMedia?.name || mediaType,
|
|
1015
|
+
type: sourceMedia?.type || mediaType,
|
|
1016
|
+
url: sourceMedia?.url || '',
|
|
1017
|
+
width: sourceMedia?.width || 1024,
|
|
1018
|
+
height: sourceMedia?.height || 1024,
|
|
1019
|
+
source: sourceId,
|
|
1020
|
+
sourceHandle: fromHandle || existingMediaHandle,
|
|
1021
|
+
};
|
|
1022
|
+
return {
|
|
1023
|
+
handle: existingMediaHandle,
|
|
1024
|
+
change: {
|
|
1025
|
+
id: targetElement.id,
|
|
1026
|
+
data: {
|
|
1027
|
+
medias: currentMedias,
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
const handle = randomUUID().replace(/-/g, '');
|
|
1033
|
+
currentMedias[handle] = {
|
|
1034
|
+
id: handle,
|
|
1035
|
+
name: sourceMedia?.name || mediaType,
|
|
1036
|
+
type: sourceMedia?.type || mediaType,
|
|
1037
|
+
url: sourceMedia?.url || '',
|
|
1038
|
+
width: sourceMedia?.width || 1024,
|
|
1039
|
+
height: sourceMedia?.height || 1024,
|
|
1040
|
+
source: sourceId,
|
|
1041
|
+
sourceHandle: fromHandle || handle,
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
handle,
|
|
1045
|
+
change: {
|
|
1046
|
+
id: targetElement.id,
|
|
1047
|
+
data: {
|
|
1048
|
+
medias: currentMedias,
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function resolveSourcePrompt(sourceElement, fromHandle) {
|
|
1054
|
+
const sourceData = asRecord(sourceElement.data);
|
|
1055
|
+
if (sourceElement.type === CANVAS_NODE_TYPES.REFINE) {
|
|
1056
|
+
const outputs = asRecord(sourceData.multi_outputs);
|
|
1057
|
+
const output = fromHandle ? asRecord(outputs[fromHandle]) : asRecord(outputs[Object.keys(outputs)[0]]);
|
|
1058
|
+
if (typeof output.prompt === 'string') {
|
|
1059
|
+
return output.prompt;
|
|
1060
|
+
}
|
|
1061
|
+
if (typeof sourceData.output_text === 'string') {
|
|
1062
|
+
return sourceData.output_text;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof sourceData.prompt === 'string') {
|
|
1066
|
+
return sourceData.prompt;
|
|
1067
|
+
}
|
|
1068
|
+
return '';
|
|
1069
|
+
}
|
|
1070
|
+
function resolveSourceMedia(sourceElement, fromHandle) {
|
|
1071
|
+
const sourceData = asRecord(sourceElement.data);
|
|
1072
|
+
const mediasValue = sourceData.medias;
|
|
1073
|
+
if (Array.isArray(mediasValue)) {
|
|
1074
|
+
if (fromHandle) {
|
|
1075
|
+
const matched = mediasValue.find((media) => asRecord(media).id === fromHandle);
|
|
1076
|
+
if (matched) {
|
|
1077
|
+
return asRecord(matched);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return mediasValue.length > 0 ? asRecord(mediasValue[0]) : null;
|
|
1081
|
+
}
|
|
1082
|
+
const medias = asRecord(mediasValue);
|
|
1083
|
+
if (fromHandle && isRecord(medias[fromHandle])) {
|
|
1084
|
+
return asRecord(medias[fromHandle]);
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof sourceData.selectedId === 'string' && isRecord(medias[sourceData.selectedId])) {
|
|
1087
|
+
return asRecord(medias[sourceData.selectedId]);
|
|
1088
|
+
}
|
|
1089
|
+
const keys = Object.keys(medias);
|
|
1090
|
+
if (keys.length > 0 && isRecord(medias[keys[0]])) {
|
|
1091
|
+
return asRecord(medias[keys[0]]);
|
|
1092
|
+
}
|
|
1093
|
+
if (typeof sourceData.url === 'string' && sourceData.url.length > 0) {
|
|
1094
|
+
return {
|
|
1095
|
+
id: fromHandle || randomUUID().replace(/-/g, ''),
|
|
1096
|
+
url: sourceData.url,
|
|
1097
|
+
type: sourceElement.type === CANVAS_NODE_TYPES.VIDEO ? 'video' : 'image',
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
function asRecord(value) {
|
|
1103
|
+
return isRecord(value) ? value : {};
|
|
1104
|
+
}
|
|
1105
|
+
function isRecord(value) {
|
|
1106
|
+
return typeof value === 'object' && value !== null;
|
|
1107
|
+
}
|
|
1108
|
+
function buildExistingNodeRefMap(elements, planId) {
|
|
1109
|
+
const map = new Map();
|
|
1110
|
+
for (const element of elements) {
|
|
1111
|
+
const data = asRecord(element.data);
|
|
1112
|
+
const ref = typeof data.plan_ref === 'string' ? data.plan_ref : undefined;
|
|
1113
|
+
if (!ref)
|
|
1114
|
+
continue;
|
|
1115
|
+
// When planId is provided, only match nodes from the same plan.
|
|
1116
|
+
// This prevents cross-plan ref collisions (e.g. both plans using N1, N2...).
|
|
1117
|
+
if (planId && typeof data.plan_id === 'string' && data.plan_id !== planId) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
map.set(ref, element.id);
|
|
1121
|
+
}
|
|
1122
|
+
return map;
|
|
1123
|
+
}
|
|
1124
|
+
function buildExistingEdgeKeySet(elements) {
|
|
1125
|
+
const set = new Set();
|
|
1126
|
+
for (const element of elements) {
|
|
1127
|
+
const edge = element;
|
|
1128
|
+
if (element.type !== CANVAS_NODE_TYPES.EDGE || !edge.source || !edge.target) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
set.add(buildEdgeKey(edge.source, edge.target, edge.sourceHandle, edge.targetHandle));
|
|
1132
|
+
}
|
|
1133
|
+
return set;
|
|
1134
|
+
}
|
|
1135
|
+
function buildEdgeKey(sourceId, targetId, fromHandle, toHandle) {
|
|
1136
|
+
return [sourceId, targetId, fromHandle || '', toHandle || ''].join('::');
|
|
1137
|
+
}
|
|
1138
|
+
function findExistingBatchInputId(elements, sourceId, outputIndex) {
|
|
1139
|
+
for (const element of elements) {
|
|
1140
|
+
if (element.type !== CANVAS_NODE_TYPES.INPUT) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const data = asRecord(element.data);
|
|
1144
|
+
const addons = asRecord(data.addons);
|
|
1145
|
+
const addon = Object.values(addons).map(asRecord)[0];
|
|
1146
|
+
if (!addon) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (String(addon.index ?? '') !== String(Number(outputIndex) + 1)) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const hasSourceEdge = elements.some((candidate) => {
|
|
1153
|
+
const edge = candidate;
|
|
1154
|
+
return (candidate.type === CANVAS_NODE_TYPES.EDGE &&
|
|
1155
|
+
edge.source === sourceId &&
|
|
1156
|
+
edge.target === element.id);
|
|
1157
|
+
});
|
|
1158
|
+
if (hasSourceEdge) {
|
|
1159
|
+
return element.id;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return undefined;
|
|
1163
|
+
}
|
|
1164
|
+
function findExistingBatchProcessIds(elements, inputId) {
|
|
1165
|
+
return elements
|
|
1166
|
+
.filter((element) => {
|
|
1167
|
+
const edge = element;
|
|
1168
|
+
return (element.type === CANVAS_NODE_TYPES.EDGE &&
|
|
1169
|
+
edge.source === inputId &&
|
|
1170
|
+
typeof edge.target === 'string');
|
|
1171
|
+
})
|
|
1172
|
+
.map((edge) => edge.target)
|
|
1173
|
+
.filter((targetId) => {
|
|
1174
|
+
const target = elements.find((element) => element.id === targetId);
|
|
1175
|
+
return Boolean(target &&
|
|
1176
|
+
(target.type === CANVAS_NODE_TYPES.PROCESS ||
|
|
1177
|
+
target.type === CANVAS_NODE_TYPES.REFINE ||
|
|
1178
|
+
target.type === CANVAS_NODE_TYPES.CROP));
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
function getExpectedProcessOutputCount(processElement) {
|
|
1182
|
+
if (!processElement) {
|
|
1183
|
+
return 1;
|
|
1184
|
+
}
|
|
1185
|
+
const processData = asRecord(processElement.data);
|
|
1186
|
+
if (Array.isArray(processData.medias) && processData.medias.length > 0) {
|
|
1187
|
+
return processData.medias.length;
|
|
1188
|
+
}
|
|
1189
|
+
const medias = asRecord(processData.medias);
|
|
1190
|
+
if (Object.keys(medias).length > 0) {
|
|
1191
|
+
return Object.keys(medias).length;
|
|
1192
|
+
}
|
|
1193
|
+
if (typeof processData.count === 'number' && processData.count > 1) {
|
|
1194
|
+
return processData.count;
|
|
1195
|
+
}
|
|
1196
|
+
return 1;
|
|
1197
|
+
}
|
|
1198
|
+
function sanitizeRefFragment(value) {
|
|
1199
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, '_').slice(0, 40) || 'model';
|
|
1200
|
+
}
|
|
1201
|
+
function findExistingSelectorHandle(medias, sourceId, fromHandle) {
|
|
1202
|
+
return Object.entries(medias).find(([, media]) => {
|
|
1203
|
+
const record = asRecord(media);
|
|
1204
|
+
return (record.source === sourceId &&
|
|
1205
|
+
(!fromHandle || record.sourceHandle === fromHandle));
|
|
1206
|
+
})?.[0];
|
|
1207
|
+
}
|
|
1208
|
+
function findExistingAddonHandle(addons, sourceId, fromHandle) {
|
|
1209
|
+
return Object.entries(addons).find(([, addon]) => {
|
|
1210
|
+
const record = asRecord(addon);
|
|
1211
|
+
return (record.source === sourceId &&
|
|
1212
|
+
(!fromHandle || record.sourceHandle === fromHandle || record.id === fromHandle));
|
|
1213
|
+
})?.[0];
|
|
1214
|
+
}
|
|
1215
|
+
function findExistingInputMediaHandle(medias, sourceId, fromHandle) {
|
|
1216
|
+
return Object.entries(medias).find(([, media]) => {
|
|
1217
|
+
const record = asRecord(media);
|
|
1218
|
+
return (record.source === sourceId &&
|
|
1219
|
+
(!fromHandle || record.sourceHandle === fromHandle));
|
|
1220
|
+
})?.[0];
|
|
1221
|
+
}
|