@openclaw/comfy-provider 2026.11.3 → 2026.11.5-beta.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"entries":[{"output":"index.js","source":"extensions/comfy/index.ts"},{"output":"workflow-http.js","source":"extensions/comfy/workflow-http.ts"},{"output":"workflow-runtime.js","source":"extensions/comfy/workflow-runtime.ts"}],"chunks":[{"output":"index.js","sha256":"c403ae5912cedc989f2fb8a1b08b67fb0865e1e5fca5cc635a01d7383e3258de","facadeSource":"extensions/comfy/index.ts","sources":["extensions/comfy/index.ts"]},{"output":"workflow-http.js","sha256":"82c1403fa34781b80c8cede9aabc7e8e1f88d9d8ee6cc26984aa7ef68cb4cec4","facadeSource":"extensions/comfy/workflow-http.ts","sources":["extensions/comfy/workflow-http.ts"]},{"output":"workflow-runtime.js","sha256":"a9883b89df220759026445689df0e6f6f1b92436a9e036b3d74a1afcd8397162","facadeSource":"extensions/comfy/workflow-runtime.ts","sources":["extensions/comfy/workflow-runtime.ts"]}],"unrenderedSources":[]}
package/dist/index.js CHANGED
@@ -1,10 +1,45 @@
1
- import { buildComfyImageGenerationProvider } from "./image-generation-provider.js";
2
- import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js";
3
- import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js";
1
+ import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, isComfyCapabilityConfiguredAsync, runComfyWorkflow } from "./workflow-runtime.js";
4
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
5
3
  import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
6
4
  //#region extensions/comfy/index.ts
7
5
  const PROVIDER_ID = "comfy";
6
+ const COMFY_MAX_INPUT_IMAGES = 1;
7
+ function comfyProviderDefaults(capability) {
8
+ return {
9
+ id: PROVIDER_ID,
10
+ label: "ComfyUI",
11
+ defaultModel: DEFAULT_COMFY_MODEL,
12
+ models: [DEFAULT_COMFY_MODEL],
13
+ isConfigured: ({ cfg, agentDir }) => isComfyCapabilityConfigured({
14
+ cfg,
15
+ agentDir,
16
+ capability
17
+ }),
18
+ isConfiguredAsync: ({ cfg, agentDir }) => isComfyCapabilityConfiguredAsync({
19
+ cfg,
20
+ agentDir,
21
+ capability
22
+ })
23
+ };
24
+ }
25
+ function resolveInputImage(inputImage) {
26
+ if (!inputImage) return;
27
+ if (!inputImage.buffer) throw new Error("Comfy music generation requires loaded reference image bytes.");
28
+ return {
29
+ buffer: inputImage.buffer,
30
+ mimeType: inputImage.mimeType ?? "image/png",
31
+ fileName: inputImage.fileName
32
+ };
33
+ }
34
+ function toComfyInputImage(inputImage) {
35
+ if (!inputImage) return;
36
+ if (!inputImage.buffer || !inputImage.mimeType) throw new Error("Comfy video generation requires a local reference image file");
37
+ return {
38
+ buffer: inputImage.buffer,
39
+ mimeType: inputImage.mimeType,
40
+ fileName: inputImage.fileName
41
+ };
42
+ }
8
43
  var comfy_default = definePluginEntry({
9
44
  id: PROVIDER_ID,
10
45
  name: "ComfyUI Provider",
@@ -35,9 +70,106 @@ var comfy_default = definePluginEntry({
35
70
  }
36
71
  })]
37
72
  });
38
- api.registerImageGenerationProvider(buildComfyImageGenerationProvider());
39
- api.registerMusicGenerationProvider(buildComfyMusicGenerationProvider());
40
- api.registerVideoGenerationProvider(buildComfyVideoGenerationProvider());
73
+ api.registerImageGenerationProvider({
74
+ ...comfyProviderDefaults("image"),
75
+ capabilities: {
76
+ generate: {
77
+ maxCount: 1,
78
+ supportsSize: false,
79
+ supportsAspectRatio: false,
80
+ supportsResolution: false
81
+ },
82
+ edit: {
83
+ enabled: true,
84
+ maxCount: 1,
85
+ maxInputImages: 1,
86
+ supportsSize: false,
87
+ supportsAspectRatio: false,
88
+ supportsResolution: false
89
+ }
90
+ },
91
+ async generateImage(req) {
92
+ if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy image generation currently supports at most one reference image");
93
+ const result = await runComfyWorkflow({
94
+ ...req,
95
+ capability: "image",
96
+ inputImage: req.inputImages?.[0]
97
+ });
98
+ return {
99
+ images: result.assets,
100
+ model: result.model,
101
+ metadata: result.metadata
102
+ };
103
+ }
104
+ });
105
+ api.registerMusicGenerationProvider({
106
+ ...comfyProviderDefaults("music"),
107
+ capabilities: {
108
+ generate: {},
109
+ edit: {
110
+ enabled: true,
111
+ maxInputImages: COMFY_MAX_INPUT_IMAGES
112
+ }
113
+ },
114
+ async generateMusic(req) {
115
+ if ((req.inputImages?.length ?? 0) > COMFY_MAX_INPUT_IMAGES) throw new Error(`Comfy music generation supports at most ${COMFY_MAX_INPUT_IMAGES} reference image.`);
116
+ const result = await runComfyWorkflow({
117
+ ...req,
118
+ capability: "music",
119
+ inputImage: resolveInputImage(req.inputImages?.[0])
120
+ });
121
+ return {
122
+ tracks: result.assets.map(({ buffer, mimeType, fileName }) => ({
123
+ buffer,
124
+ mimeType,
125
+ fileName
126
+ })),
127
+ model: result.model,
128
+ metadata: {
129
+ ...result.metadata,
130
+ inputImageCount: req.inputImages?.length ?? 0
131
+ }
132
+ };
133
+ }
134
+ });
135
+ api.registerVideoGenerationProvider({
136
+ ...comfyProviderDefaults("video"),
137
+ capabilities: {
138
+ generate: {
139
+ maxVideos: 1,
140
+ supportsSize: false,
141
+ supportsAspectRatio: false,
142
+ supportsResolution: false,
143
+ supportsAudio: false,
144
+ supportsWatermark: false
145
+ },
146
+ imageToVideo: {
147
+ enabled: true,
148
+ maxVideos: 1,
149
+ maxInputImages: 1,
150
+ supportsSize: false,
151
+ supportsAspectRatio: false,
152
+ supportsResolution: false,
153
+ supportsAudio: false,
154
+ supportsWatermark: false
155
+ },
156
+ videoToVideo: { enabled: false }
157
+ },
158
+ async generateVideo(req) {
159
+ if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy video generation currently supports at most one reference image");
160
+ if ((req.inputVideos?.length ?? 0) > 0) throw new Error("Comfy video generation does not support input videos");
161
+ const result = await runComfyWorkflow({
162
+ ...req,
163
+ capability: "video",
164
+ inputImage: toComfyInputImage(req.inputImages?.[0])
165
+ });
166
+ return {
167
+ videos: result.assets,
168
+ model: result.model,
169
+ metadata: result.metadata
170
+ };
171
+ }
172
+ });
41
173
  }
42
174
  });
43
175
  //#endregion
@@ -0,0 +1,38 @@
1
+ import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
2
+ import { assertOkOrThrowHttpError, readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
3
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
4
+ //#region extensions/comfy/workflow-http.ts
5
+ async function readJsonResponse(params) {
6
+ const { errorPrefix, deadline, timeoutMs, ...request } = params;
7
+ const remainingMs = () => resolveComfyRemainingMs(deadline, timeoutMs);
8
+ const { response, release } = await fetchWithSsrFGuard({
9
+ ...request,
10
+ timeoutMs: remainingMs()
11
+ });
12
+ try {
13
+ const requestHeaders = params.init?.headers;
14
+ await assertOkOrThrowHttpError(response, errorPrefix, {
15
+ requestHeaders,
16
+ bodyTimeoutMs: remainingMs,
17
+ onBodyTimeout: () => createComfyTimeoutError(timeoutMs)
18
+ });
19
+ return await readProviderJsonResponse(response, errorPrefix, {
20
+ requestHeaders,
21
+ timeoutMs: remainingMs,
22
+ onTimeout: () => createComfyTimeoutError(timeoutMs)
23
+ });
24
+ } finally {
25
+ await release();
26
+ }
27
+ }
28
+ function createComfyTimeoutError(timeoutMs) {
29
+ return /* @__PURE__ */ new Error(`Comfy workflow did not finish within ${Math.ceil(timeoutMs / 1e3)}s`);
30
+ }
31
+ function resolveComfyRemainingMs(deadline, timeoutMs, defaultTimeoutMs = timeoutMs) {
32
+ const defaultMs = resolvePositiveTimerTimeoutMs(defaultTimeoutMs, 1);
33
+ const remainingMs = deadline - performance.now();
34
+ if (remainingMs <= 0) throw createComfyTimeoutError(timeoutMs);
35
+ return Math.max(1, Math.min(defaultMs, remainingMs));
36
+ }
37
+ //#endregion
38
+ export { createComfyTimeoutError, readJsonResponse, resolveComfyRemainingMs };
@@ -1,4 +1,4 @@
1
- import { isTerminalComfyHistory } from "./workflow-history.js";
1
+ import { createComfyTimeoutError, readJsonResponse, resolveComfyRemainingMs } from "./workflow-http.js";
2
2
  import { randomInt } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import { bufferToBlobPart } from "openclaw/plugin-sdk/blob-runtime";
@@ -7,13 +7,22 @@ import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
7
7
  import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
8
8
  import { isProviderApiKeyConfigured, isProviderApiKeyConfiguredAsync } from "openclaw/plugin-sdk/provider-auth";
9
9
  import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
10
- import { assertOkOrThrowHttpError, readProviderBinaryResponse, readProviderJsonResponse, redactProviderResponseErrorText, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
10
+ import { assertOkOrThrowHttpError, readProviderBinaryResponse, redactProviderResponseErrorText, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
11
11
  import { normalizeSecretInputString, resolveConfiguredSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
12
12
  import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
13
13
  import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
14
14
  import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
15
15
  import { resolveUserPath, sleep } from "openclaw/plugin-sdk/text-utility-runtime";
16
16
  //#region extensions/comfy/workflow-runtime.ts
17
+ function isTerminalComfyHistory(entry, headers) {
18
+ if (entry.status?.status_str === "error") {
19
+ const messages = entry.status.messages;
20
+ const errorDetails = (Array.isArray(messages) ? messages.findLast((message) => Array.isArray(message) && message[0] === "execution_error") : void 0)?.[1];
21
+ const detail = isRecord(errorDetails) ? normalizeOptionalString(errorDetails.exception_message) : void 0;
22
+ throw new Error(`Comfy workflow failed${detail ? `: ${redactProviderResponseErrorText(detail, headers)}` : ""}`);
23
+ }
24
+ return entry.status?.completed === true || Boolean(entry.outputs && Object.keys(entry.outputs).length);
25
+ }
17
26
  const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
18
27
  const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
19
28
  const DEFAULT_POLL_INTERVAL_MS = 1500;
@@ -131,40 +140,6 @@ function resolveComfyNetworkPolicy(params) {
131
140
  function isSingleLabelServiceHostname(hostname) {
132
141
  return /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/u.test(hostname);
133
142
  }
134
- async function readJsonResponse(params) {
135
- const { errorPrefix, ...request } = params;
136
- const { response, release } = await fetchWithSsrFGuard(request);
137
- try {
138
- const requestHeaders = params.init?.headers;
139
- await assertOkOrThrowHttpError(response, errorPrefix, { requestHeaders });
140
- return await readProviderJsonResponse(response, errorPrefix, { requestHeaders });
141
- } finally {
142
- await release();
143
- }
144
- }
145
- async function uploadInputImage(connection, image, capability) {
146
- const { baseUrl, mode, headers: requestHeaders, ...request } = connection;
147
- const form = new FormData();
148
- form.set("image", new Blob([bufferToBlobPart(image.buffer)], { type: image.mimeType }), normalizeOptionalString(image.fileName) || `input.${extensionForMime(image.mimeType)?.slice(1) || "bin"}`);
149
- form.set("type", "input");
150
- form.set("overwrite", "true");
151
- const headers = new Headers(requestHeaders);
152
- headers.delete("Content-Type");
153
- const payload = await readJsonResponse({
154
- ...request,
155
- url: `${baseUrl}${mode === "cloud" ? "/api/upload/image" : "/upload/image"}`,
156
- init: {
157
- method: "POST",
158
- headers,
159
- body: form
160
- },
161
- auditContext: `comfy-${capability}-upload`,
162
- errorPrefix: "Comfy image upload failed"
163
- });
164
- const uploadedName = normalizeOptionalString(payload.filename) || normalizeOptionalString(payload.name);
165
- if (!uploadedName) throw new Error("Comfy image upload response missing filename");
166
- return uploadedName;
167
- }
168
143
  function extractHistoryEntry(history, promptId) {
169
144
  if (!isRecord(history)) return null;
170
145
  const directOutputs = history.outputs;
@@ -173,108 +148,6 @@ function extractHistoryEntry(history, promptId) {
173
148
  if (isRecord(nested)) return nested;
174
149
  return null;
175
150
  }
176
- async function waitForComfyHistory(params, promptId, pollIntervalMs) {
177
- const { baseUrl, headers: requestHeaders, mode, ...request } = params;
178
- const headers = new Headers(requestHeaders);
179
- const deadline = performance.now() + params.timeoutMs;
180
- const read = (path, kind, timeoutMs) => readJsonResponse({
181
- ...request,
182
- url: `${baseUrl}${path}`,
183
- init: {
184
- method: "GET",
185
- headers
186
- },
187
- timeoutMs,
188
- auditContext: `comfy-${kind}`,
189
- errorPrefix: `Comfy ${kind} lookup failed`
190
- });
191
- for (;;) {
192
- const requestTimeoutMs = resolveComfyRemainingMs(deadline, params.timeoutMs);
193
- if (mode === "cloud") {
194
- const status = await read(`/api/job/${promptId}/status`, "status", requestTimeoutMs);
195
- if (status.status === "completed") return await read(`/api/history_v2/${promptId}`, "history", params.timeoutMs);
196
- if (status.status === "failed" || status.status === "cancelled") {
197
- const detail = redactProviderResponseErrorText(status.error ?? status.message ?? promptId, headers);
198
- throw new Error(`Comfy workflow ${status.status}: ${detail}`);
199
- }
200
- } else {
201
- const entry = extractHistoryEntry(await read(`/history/${promptId}`, "history", requestTimeoutMs), promptId);
202
- if (entry && isTerminalComfyHistory(entry, params.headers)) return entry;
203
- }
204
- const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, pollIntervalMs);
205
- await sleep(pollDelayMs);
206
- }
207
- }
208
- function resolveComfyRemainingMs(deadline, timeoutMs, defaultTimeoutMs = timeoutMs) {
209
- const defaultMs = resolvePositiveTimerTimeoutMs(defaultTimeoutMs, 1);
210
- const remainingMs = deadline - performance.now();
211
- if (remainingMs <= 0) throw new Error(`Comfy workflow did not finish within ${Math.ceil(timeoutMs / 1e3)}s`);
212
- return Math.max(1, Math.min(defaultMs, remainingMs));
213
- }
214
- function collectOutputFiles(params) {
215
- const outputs = params.history.outputs;
216
- if (!outputs) return [];
217
- const nodeIds = params.outputNodeId ? [params.outputNodeId] : Object.keys(outputs);
218
- const files = [];
219
- for (const nodeId of nodeIds) {
220
- const entry = outputs[nodeId];
221
- if (!entry) continue;
222
- for (const kind of params.outputKinds) {
223
- const bucket = entry[kind];
224
- if (!Array.isArray(bucket)) continue;
225
- for (const file of bucket) {
226
- if (params.capability === "video" && kind === "images") {
227
- const fileName = normalizeOptionalString(file.filename) || normalizeOptionalString(file.name);
228
- if (!fileName || !/\.(?:mp4|webm)$/i.test(fileName)) continue;
229
- }
230
- files.push({
231
- nodeId,
232
- file
233
- });
234
- }
235
- }
236
- }
237
- return files;
238
- }
239
- async function downloadOutputFile(params, file, capability, maxBytes) {
240
- const { baseUrl, headers: requestHeaders, mode, ...request } = params;
241
- const headers = new Headers(requestHeaders);
242
- const fileName = normalizeOptionalString(file.filename) || normalizeOptionalString(file.name);
243
- if (!fileName) throw new Error("Comfy output entry missing filename");
244
- const query = new URLSearchParams({
245
- filename: fileName,
246
- subfolder: normalizeOptionalString(file.subfolder) ?? "",
247
- type: normalizeOptionalString(file.type) ?? "output"
248
- });
249
- const viewPath = mode === "cloud" ? "/api/view" : "/view";
250
- const auditContext = `comfy-${capability}-download`;
251
- const firstResponse = await fetchWithSsrFGuard({
252
- ...request,
253
- url: `${baseUrl}${viewPath}?${query.toString()}`,
254
- init: {
255
- method: "GET",
256
- headers
257
- },
258
- auditContext
259
- });
260
- try {
261
- await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: headers });
262
- const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
263
- const downloadLabel = `Comfy ${capability} output download`;
264
- return {
265
- buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel, capability, {
266
- maxBytes,
267
- chunkTimeoutMs: params.timeoutMs,
268
- onOverflow: ({ maxBytes: limit }) => /* @__PURE__ */ new Error(`${downloadLabel} exceeds ${limit} bytes`),
269
- onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`${downloadLabel} stalled after ${chunkTimeoutMs}ms`)
270
- }),
271
- mimeType,
272
- fileName
273
- };
274
- } finally {
275
- await firstResponse.release();
276
- }
277
- }
278
151
  function hasUnavailableComfyHeaderSecret(value, cfg) {
279
152
  if (!isRecord(value)) return false;
280
153
  return Object.entries(value).some(([name, headerValue]) => {
@@ -334,7 +207,7 @@ async function runComfyWorkflow(params) {
334
207
  const seedInputName = normalizeOptionalString(capabilityConfig.seedInputName) ?? "seed";
335
208
  const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
336
209
  const pollIntervalMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "pollIntervalMs"), DEFAULT_POLL_INTERVAL_MS);
337
- const timeoutMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "timeoutMs") ?? params.timeoutMs, DEFAULT_TIMEOUT_MS);
210
+ const timeoutMs = resolvePositiveTimerTimeoutMs(params.timeoutMs ?? readConfigInteger(capabilityConfig, "timeoutMs"), DEFAULT_TIMEOUT_MS);
338
211
  const providerModel = normalizeOptionalString(params.model) || "workflow";
339
212
  setWorkflowInput(workflow, promptNodeId, promptInputName, params.prompt);
340
213
  if (seedNodeId) setWorkflowInput(workflow, seedNodeId, seedInputName, randomInt(RANDOM_SEED_EXCLUSIVE_MAX));
@@ -366,17 +239,33 @@ async function runComfyWorkflow(params) {
366
239
  explicitAllowPrivateNetwork,
367
240
  mode
368
241
  });
369
- const connection = {
370
- baseUrl,
371
- headers,
372
- timeoutMs,
373
- policy: networkPolicy,
374
- dispatcherPolicy,
375
- mode
376
- };
242
+ const deadline = performance.now() + timeoutMs;
243
+ const remainingMs = () => resolveComfyRemainingMs(deadline, timeoutMs);
377
244
  if (params.inputImage) {
378
245
  if (!inputImageNodeId) throw new Error("Comfy edit requests require plugins.entries.comfy.config.<capability>.inputImageNodeId to be configured");
379
- setWorkflowInput(workflow, inputImageNodeId, inputImageInputName, await uploadInputImage(connection, params.inputImage, params.capability));
246
+ const form = new FormData();
247
+ form.set("image", new Blob([bufferToBlobPart(params.inputImage.buffer)], { type: params.inputImage.mimeType }), normalizeOptionalString(params.inputImage.fileName) || `input.${extensionForMime(params.inputImage.mimeType)?.slice(1) || "bin"}`);
248
+ form.set("type", "input");
249
+ form.set("overwrite", "true");
250
+ const uploadHeaders = new Headers(headers);
251
+ uploadHeaders.delete("Content-Type");
252
+ const payload = await readJsonResponse({
253
+ deadline,
254
+ timeoutMs,
255
+ policy: networkPolicy,
256
+ dispatcherPolicy,
257
+ url: `${baseUrl}${mode === "cloud" ? "/api/upload/image" : "/upload/image"}`,
258
+ init: {
259
+ method: "POST",
260
+ headers: uploadHeaders,
261
+ body: form
262
+ },
263
+ auditContext: `comfy-${params.capability}-upload`,
264
+ errorPrefix: "Comfy image upload failed"
265
+ });
266
+ const uploadedName = normalizeOptionalString(payload.filename) || normalizeOptionalString(payload.name);
267
+ if (!uploadedName) throw new Error("Comfy image upload response missing filename");
268
+ setWorkflowInput(workflow, inputImageNodeId, inputImageInputName, uploadedName);
380
269
  }
381
270
  const submitPayload = {
382
271
  prompt: workflow,
@@ -389,6 +278,7 @@ async function runComfyWorkflow(params) {
389
278
  headers,
390
279
  body: JSON.stringify(submitPayload)
391
280
  },
281
+ deadline,
392
282
  timeoutMs,
393
283
  policy: networkPolicy,
394
284
  dispatcherPolicy,
@@ -397,24 +287,117 @@ async function runComfyWorkflow(params) {
397
287
  });
398
288
  const promptId = normalizeOptionalString(promptResponse.prompt_id);
399
289
  if (!promptId) throw new Error("Comfy workflow submit response missing prompt_id");
400
- const historyEntry = extractHistoryEntry(await waitForComfyHistory(connection, promptId, pollIntervalMs), promptId);
290
+ const waitForHistory = async () => {
291
+ const read = (path, kind) => readJsonResponse({
292
+ policy: networkPolicy,
293
+ dispatcherPolicy,
294
+ url: `${baseUrl}${path}`,
295
+ init: {
296
+ method: "GET",
297
+ headers
298
+ },
299
+ deadline,
300
+ timeoutMs,
301
+ auditContext: `comfy-${kind}`,
302
+ errorPrefix: `Comfy ${kind} lookup failed`
303
+ });
304
+ for (;;) {
305
+ if (mode === "cloud") {
306
+ const status = await read(`/api/job/${promptId}/status`, "status");
307
+ if (status.status === "completed") return await read(`/api/history_v2/${promptId}`, "history");
308
+ if (status.status === "failed" || status.status === "cancelled") {
309
+ const detail = redactProviderResponseErrorText(status.error ?? status.message ?? promptId, headers);
310
+ throw new Error(`Comfy workflow ${status.status}: ${detail}`);
311
+ }
312
+ } else {
313
+ const entry = extractHistoryEntry(await read(`/history/${promptId}`, "history"), promptId);
314
+ if (entry && isTerminalComfyHistory(entry, headers)) return entry;
315
+ }
316
+ const pollDelayMs = resolveComfyRemainingMs(deadline, timeoutMs, pollIntervalMs);
317
+ await sleep(pollDelayMs);
318
+ }
319
+ };
320
+ const historyEntry = extractHistoryEntry(await waitForHistory(), promptId);
401
321
  if (!historyEntry) throw new Error(`Comfy history response missing outputs for prompt ${promptId}`);
402
- const outputFiles = collectOutputFiles({
403
- history: historyEntry,
404
- outputNodeId,
405
- outputKinds: params.capability === "music" ? ["audio"] : params.capability === "video" ? [
406
- "images",
407
- "gifs",
408
- "videos"
409
- ] : ["images"],
410
- capability: params.capability
411
- });
322
+ const outputKinds = params.capability === "music" ? ["audio"] : params.capability === "video" ? [
323
+ "images",
324
+ "gifs",
325
+ "videos"
326
+ ] : ["images"];
327
+ const outputFiles = [];
328
+ const outputs = historyEntry.outputs;
329
+ if (outputs) {
330
+ const nodeIds = outputNodeId ? [outputNodeId] : Object.keys(outputs);
331
+ for (const nodeId of nodeIds) {
332
+ const entry = outputs[nodeId];
333
+ if (!entry) continue;
334
+ for (const kind of outputKinds) {
335
+ const bucket = entry[kind];
336
+ if (!Array.isArray(bucket)) continue;
337
+ for (const file of bucket) {
338
+ if (params.capability === "video" && kind === "images") {
339
+ const fileName = normalizeOptionalString(file.filename) || normalizeOptionalString(file.name);
340
+ if (!fileName || !/\.(?:mp4|webm)$/i.test(fileName)) continue;
341
+ }
342
+ outputFiles.push({
343
+ nodeId,
344
+ file
345
+ });
346
+ }
347
+ }
348
+ }
349
+ }
412
350
  if (outputFiles.length === 0) throw new Error(`Comfy workflow ${promptId} completed without ${params.capability} outputs`);
351
+ const downloadOutputFile = async (file, maxBytes) => {
352
+ const fileName = normalizeOptionalString(file.filename) || normalizeOptionalString(file.name);
353
+ if (!fileName) throw new Error("Comfy output entry missing filename");
354
+ const query = new URLSearchParams({
355
+ filename: fileName,
356
+ subfolder: normalizeOptionalString(file.subfolder) ?? "",
357
+ type: normalizeOptionalString(file.type) ?? "output"
358
+ });
359
+ const viewPath = mode === "cloud" ? "/api/view" : "/view";
360
+ const auditContext = `comfy-${params.capability}-download`;
361
+ const firstResponse = await fetchWithSsrFGuard({
362
+ timeoutMs: remainingMs(),
363
+ policy: networkPolicy,
364
+ dispatcherPolicy,
365
+ url: `${baseUrl}${viewPath}?${query.toString()}`,
366
+ init: {
367
+ method: "GET",
368
+ headers
369
+ },
370
+ auditContext
371
+ });
372
+ try {
373
+ await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", {
374
+ requestHeaders: headers,
375
+ bodyTimeoutMs: remainingMs,
376
+ onBodyTimeout: () => createComfyTimeoutError(timeoutMs)
377
+ });
378
+ const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
379
+ const downloadLabel = `Comfy ${params.capability} output download`;
380
+ return {
381
+ buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel, params.capability, {
382
+ maxBytes,
383
+ chunkTimeoutMs: remainingMs(),
384
+ timeoutMs: remainingMs,
385
+ onTimeout: () => createComfyTimeoutError(timeoutMs),
386
+ onOverflow: ({ maxBytes: limit }) => /* @__PURE__ */ new Error(`${downloadLabel} exceeds ${limit} bytes`),
387
+ onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`${downloadLabel} stalled after ${chunkTimeoutMs}ms`)
388
+ }),
389
+ mimeType,
390
+ fileName
391
+ };
392
+ } finally {
393
+ await firstResponse.release();
394
+ }
395
+ };
413
396
  const assets = [];
414
397
  const outputKind = params.capability === "music" ? "audio" : params.capability;
415
398
  const maxOutputBytes = outputKind === "image" ? resolveGeneratedImageDownloadMaxBytes(params.cfg) : resolveGeneratedMediaMaxBytes(params.cfg, outputKind);
416
399
  for (const output of outputFiles) {
417
- const downloaded = await downloadOutputFile(connection, output.file, params.capability, maxOutputBytes);
400
+ const downloaded = await downloadOutputFile(output.file, maxOutputBytes);
418
401
  assets.push({
419
402
  ...downloaded,
420
403
  metadata: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/comfy-provider",
3
- "version": "2026.11.3",
3
+ "version": "2026.11.5-beta.2",
4
4
  "description": "OpenClaw ComfyUI provider plugin.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.7.2"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.11.3"
21
+ "pluginApi": ">=2026.11.5-beta.2"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.11.3",
24
+ "openclawVersion": "2026.11.5-beta.2",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -40,7 +40,7 @@
40
40
  "assets/activity.svg"
41
41
  ],
42
42
  "peerDependencies": {
43
- "openclaw": ">=2026.11.3"
43
+ "openclaw": ">=2026.11.5-beta.2"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {
@@ -1,51 +0,0 @@
1
- import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, isComfyCapabilityConfiguredAsync, runComfyWorkflow } from "./workflow-runtime.js";
2
- //#region extensions/comfy/image-generation-provider.ts
3
- function buildComfyImageGenerationProvider() {
4
- return {
5
- id: "comfy",
6
- label: "ComfyUI",
7
- defaultModel: DEFAULT_COMFY_MODEL,
8
- models: [DEFAULT_COMFY_MODEL],
9
- isConfigured: ({ cfg, agentDir }) => isComfyCapabilityConfigured({
10
- cfg,
11
- agentDir,
12
- capability: "image"
13
- }),
14
- isConfiguredAsync: ({ cfg, agentDir }) => isComfyCapabilityConfiguredAsync({
15
- cfg,
16
- agentDir,
17
- capability: "image"
18
- }),
19
- capabilities: {
20
- generate: {
21
- maxCount: 1,
22
- supportsSize: false,
23
- supportsAspectRatio: false,
24
- supportsResolution: false
25
- },
26
- edit: {
27
- enabled: true,
28
- maxCount: 1,
29
- maxInputImages: 1,
30
- supportsSize: false,
31
- supportsAspectRatio: false,
32
- supportsResolution: false
33
- }
34
- },
35
- async generateImage(req) {
36
- if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy image generation currently supports at most one reference image");
37
- const result = await runComfyWorkflow({
38
- ...req,
39
- capability: "image",
40
- inputImage: req.inputImages?.[0]
41
- });
42
- return {
43
- images: result.assets,
44
- model: result.model,
45
- metadata: result.metadata
46
- };
47
- }
48
- };
49
- }
50
- //#endregion
51
- export { buildComfyImageGenerationProvider };
@@ -1,59 +0,0 @@
1
- import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, isComfyCapabilityConfiguredAsync, runComfyWorkflow } from "./workflow-runtime.js";
2
- //#region extensions/comfy/music-generation-provider.ts
3
- const COMFY_MAX_INPUT_IMAGES = 1;
4
- function resolveInputImage(inputImage) {
5
- if (!inputImage) return;
6
- if (!inputImage.buffer) throw new Error("Comfy music generation requires loaded reference image bytes.");
7
- return {
8
- buffer: inputImage.buffer,
9
- mimeType: inputImage.mimeType ?? "image/png",
10
- fileName: inputImage.fileName
11
- };
12
- }
13
- function buildComfyMusicGenerationProvider() {
14
- return {
15
- id: "comfy",
16
- label: "ComfyUI",
17
- defaultModel: DEFAULT_COMFY_MODEL,
18
- models: [DEFAULT_COMFY_MODEL],
19
- isConfigured: ({ cfg, agentDir }) => isComfyCapabilityConfigured({
20
- cfg,
21
- agentDir,
22
- capability: "music"
23
- }),
24
- isConfiguredAsync: ({ cfg, agentDir }) => isComfyCapabilityConfiguredAsync({
25
- cfg,
26
- agentDir,
27
- capability: "music"
28
- }),
29
- capabilities: {
30
- generate: {},
31
- edit: {
32
- enabled: true,
33
- maxInputImages: COMFY_MAX_INPUT_IMAGES
34
- }
35
- },
36
- async generateMusic(req) {
37
- if ((req.inputImages?.length ?? 0) > COMFY_MAX_INPUT_IMAGES) throw new Error(`Comfy music generation supports at most ${COMFY_MAX_INPUT_IMAGES} reference image.`);
38
- const result = await runComfyWorkflow({
39
- ...req,
40
- capability: "music",
41
- inputImage: resolveInputImage(req.inputImages?.[0])
42
- });
43
- return {
44
- tracks: result.assets.map(({ buffer, mimeType, fileName }) => ({
45
- buffer,
46
- mimeType,
47
- fileName
48
- })),
49
- model: result.model,
50
- metadata: {
51
- ...result.metadata,
52
- inputImageCount: req.inputImages?.length ?? 0
53
- }
54
- };
55
- }
56
- };
57
- }
58
- //#endregion
59
- export { buildComfyMusicGenerationProvider };
@@ -1,66 +0,0 @@
1
- import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, isComfyCapabilityConfiguredAsync, runComfyWorkflow } from "./workflow-runtime.js";
2
- //#region extensions/comfy/video-generation-provider.ts
3
- function toComfyInputImage(inputImage) {
4
- if (!inputImage) return;
5
- if (!inputImage.buffer || !inputImage.mimeType) throw new Error("Comfy video generation requires a local reference image file");
6
- return {
7
- buffer: inputImage.buffer,
8
- mimeType: inputImage.mimeType,
9
- fileName: inputImage.fileName
10
- };
11
- }
12
- function buildComfyVideoGenerationProvider() {
13
- return {
14
- id: "comfy",
15
- label: "ComfyUI",
16
- defaultModel: DEFAULT_COMFY_MODEL,
17
- models: [DEFAULT_COMFY_MODEL],
18
- isConfigured: ({ cfg, agentDir }) => isComfyCapabilityConfigured({
19
- cfg,
20
- agentDir,
21
- capability: "video"
22
- }),
23
- isConfiguredAsync: ({ cfg, agentDir }) => isComfyCapabilityConfiguredAsync({
24
- cfg,
25
- agentDir,
26
- capability: "video"
27
- }),
28
- capabilities: {
29
- generate: {
30
- maxVideos: 1,
31
- supportsSize: false,
32
- supportsAspectRatio: false,
33
- supportsResolution: false,
34
- supportsAudio: false,
35
- supportsWatermark: false
36
- },
37
- imageToVideo: {
38
- enabled: true,
39
- maxVideos: 1,
40
- maxInputImages: 1,
41
- supportsSize: false,
42
- supportsAspectRatio: false,
43
- supportsResolution: false,
44
- supportsAudio: false,
45
- supportsWatermark: false
46
- },
47
- videoToVideo: { enabled: false }
48
- },
49
- async generateVideo(req) {
50
- if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy video generation currently supports at most one reference image");
51
- if ((req.inputVideos?.length ?? 0) > 0) throw new Error("Comfy video generation does not support input videos");
52
- const result = await runComfyWorkflow({
53
- ...req,
54
- capability: "video",
55
- inputImage: toComfyInputImage(req.inputImages?.[0])
56
- });
57
- return {
58
- videos: result.assets,
59
- model: result.model,
60
- metadata: result.metadata
61
- };
62
- }
63
- };
64
- }
65
- //#endregion
66
- export { buildComfyVideoGenerationProvider };
@@ -1,14 +0,0 @@
1
- import { redactProviderResponseErrorText } from "openclaw/plugin-sdk/provider-http";
2
- import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
3
- //#region extensions/comfy/workflow-history.ts
4
- function isTerminalComfyHistory(entry, headers) {
5
- if (entry.status?.status_str === "error") {
6
- const messages = entry.status.messages;
7
- const executionError = Array.isArray(messages) ? messages.findLast((message) => Array.isArray(message) && message[0] === "execution_error") : void 0;
8
- const detail = Array.isArray(executionError) && isRecord(executionError[1]) ? normalizeOptionalString(executionError[1].exception_message) : void 0;
9
- throw new Error(`Comfy workflow failed${detail ? `: ${redactProviderResponseErrorText(detail, headers)}` : ""}`);
10
- }
11
- return entry.status?.completed === true || Boolean(entry.outputs && Object.keys(entry.outputs).length);
12
- }
13
- //#endregion
14
- export { isTerminalComfyHistory };