@openclaw/comfy-provider 2026.10.2 → 2026.10.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,14 @@
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 };
@@ -1,3 +1,4 @@
1
+ import { isTerminalComfyHistory } from "./workflow-history.js";
1
2
  import { randomInt } from "node:crypto";
2
3
  import fs from "node:fs/promises";
3
4
  import { bufferToBlobPart } from "openclaw/plugin-sdk/blob-runtime";
@@ -11,7 +12,7 @@ import { normalizeSecretInputString, resolveConfiguredSecretInputString, resolve
11
12
  import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
12
13
  import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
13
14
  import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
14
- import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
15
+ import { resolveUserPath, sleep } from "openclaw/plugin-sdk/text-utility-runtime";
15
16
  //#region extensions/comfy/workflow-runtime.ts
16
17
  const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
17
18
  const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
@@ -141,24 +142,23 @@ async function readJsonResponse(params) {
141
142
  await release();
142
143
  }
143
144
  }
144
- async function uploadInputImage(params) {
145
+ async function uploadInputImage(connection, image, capability) {
146
+ const { baseUrl, mode, headers: requestHeaders, ...request } = connection;
145
147
  const form = new FormData();
146
- form.set("image", new Blob([bufferToBlobPart(params.image.buffer)], { type: params.image.mimeType }), normalizeOptionalString(params.image.fileName) || `input.${extensionForMime(params.image.mimeType)?.slice(1) || "bin"}`);
148
+ form.set("image", new Blob([bufferToBlobPart(image.buffer)], { type: image.mimeType }), normalizeOptionalString(image.fileName) || `input.${extensionForMime(image.mimeType)?.slice(1) || "bin"}`);
147
149
  form.set("type", "input");
148
150
  form.set("overwrite", "true");
149
- const headers = new Headers(params.headers);
151
+ const headers = new Headers(requestHeaders);
150
152
  headers.delete("Content-Type");
151
153
  const payload = await readJsonResponse({
152
- url: `${params.baseUrl}${params.mode === "cloud" ? "/api/upload/image" : "/upload/image"}`,
154
+ ...request,
155
+ url: `${baseUrl}${mode === "cloud" ? "/api/upload/image" : "/upload/image"}`,
153
156
  init: {
154
157
  method: "POST",
155
158
  headers,
156
159
  body: form
157
160
  },
158
- timeoutMs: params.timeoutMs,
159
- policy: params.policy,
160
- dispatcherPolicy: params.dispatcherPolicy,
161
- auditContext: `comfy-${params.capability}-upload`,
161
+ auditContext: `comfy-${capability}-upload`,
162
162
  errorPrefix: "Comfy image upload failed"
163
163
  });
164
164
  const uploadedName = normalizeOptionalString(payload.filename) || normalizeOptionalString(payload.name);
@@ -173,37 +173,36 @@ function extractHistoryEntry(history, promptId) {
173
173
  if (isRecord(nested)) return nested;
174
174
  return null;
175
175
  }
176
- async function waitForComfyHistory(params) {
176
+ async function waitForComfyHistory(params, promptId, pollIntervalMs) {
177
+ const { baseUrl, headers: requestHeaders, mode, ...request } = params;
178
+ const headers = new Headers(requestHeaders);
177
179
  const deadline = Date.now() + params.timeoutMs;
178
180
  const read = (path, kind, timeoutMs) => readJsonResponse({
179
- url: `${params.baseUrl}${path}`,
181
+ ...request,
182
+ url: `${baseUrl}${path}`,
180
183
  init: {
181
184
  method: "GET",
182
- headers: params.headers
185
+ headers
183
186
  },
184
187
  timeoutMs,
185
- policy: params.policy,
186
- dispatcherPolicy: params.dispatcherPolicy,
187
188
  auditContext: `comfy-${kind}`,
188
189
  errorPrefix: `Comfy ${kind} lookup failed`
189
190
  });
190
191
  for (;;) {
191
192
  const requestTimeoutMs = resolveComfyRemainingMs(deadline, params.timeoutMs);
192
- if (params.mode === "cloud") {
193
- const status = await read(`/api/job/${params.promptId}/status`, "status", requestTimeoutMs);
194
- if (status.status === "completed") return await read(`/api/history_v2/${params.promptId}`, "history", 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);
195
196
  if (status.status === "failed" || status.status === "cancelled") {
196
- const detail = redactProviderResponseErrorText(status.error ?? status.message ?? params.promptId, params.headers);
197
+ const detail = redactProviderResponseErrorText(status.error ?? status.message ?? promptId, headers);
197
198
  throw new Error(`Comfy workflow ${status.status}: ${detail}`);
198
199
  }
199
200
  } else {
200
- const entry = extractHistoryEntry(await read(`/history/${params.promptId}`, "history", requestTimeoutMs), params.promptId);
201
- if (entry?.outputs && Object.keys(entry.outputs).length > 0) return entry;
201
+ const entry = extractHistoryEntry(await read(`/history/${promptId}`, "history", requestTimeoutMs), promptId);
202
+ if (entry && isTerminalComfyHistory(entry, params.headers)) return entry;
202
203
  }
203
- const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
204
- await new Promise((resolve) => {
205
- setTimeout(resolve, pollDelayMs);
206
- });
204
+ const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, pollIntervalMs);
205
+ await sleep(pollDelayMs);
207
206
  }
208
207
  }
209
208
  function resolveComfyRemainingMs(deadline, timeoutMs, defaultTimeoutMs = timeoutMs) {
@@ -237,36 +236,36 @@ function collectOutputFiles(params) {
237
236
  }
238
237
  return files;
239
238
  }
240
- async function downloadOutputFile(params) {
241
- const fileName = normalizeOptionalString(params.file.filename) || normalizeOptionalString(params.file.name);
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);
242
243
  if (!fileName) throw new Error("Comfy output entry missing filename");
243
244
  const query = new URLSearchParams({
244
245
  filename: fileName,
245
- subfolder: normalizeOptionalString(params.file.subfolder) ?? "",
246
- type: normalizeOptionalString(params.file.type) ?? "output"
246
+ subfolder: normalizeOptionalString(file.subfolder) ?? "",
247
+ type: normalizeOptionalString(file.type) ?? "output"
247
248
  });
248
- const viewPath = params.mode === "cloud" ? "/api/view" : "/view";
249
- const auditContext = `comfy-${params.capability}-download`;
249
+ const viewPath = mode === "cloud" ? "/api/view" : "/view";
250
+ const auditContext = `comfy-${capability}-download`;
250
251
  const firstResponse = await fetchWithSsrFGuard({
251
- url: `${params.baseUrl}${viewPath}?${query.toString()}`,
252
+ ...request,
253
+ url: `${baseUrl}${viewPath}?${query.toString()}`,
252
254
  init: {
253
255
  method: "GET",
254
- headers: params.headers
256
+ headers
255
257
  },
256
- timeoutMs: params.timeoutMs,
257
- policy: params.policy,
258
- dispatcherPolicy: params.dispatcherPolicy,
259
258
  auditContext
260
259
  });
261
260
  try {
262
- await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: params.headers });
261
+ await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: headers });
263
262
  const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
264
- const downloadLabel = `Comfy ${params.capability} output download`;
263
+ const downloadLabel = `Comfy ${capability} output download`;
265
264
  return {
266
- buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel, params.capability, {
267
- maxBytes: params.maxBytes,
265
+ buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel, capability, {
266
+ maxBytes,
268
267
  chunkTimeoutMs: params.timeoutMs,
269
- onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`${downloadLabel} exceeds ${maxBytes} bytes`),
268
+ onOverflow: ({ maxBytes: limit }) => /* @__PURE__ */ new Error(`${downloadLabel} exceeds ${limit} bytes`),
270
269
  onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`${downloadLabel} stalled after ${chunkTimeoutMs}ms`)
271
270
  }),
272
271
  mimeType,
@@ -357,18 +356,17 @@ async function runComfyWorkflow(params) {
357
356
  explicitAllowPrivateNetwork,
358
357
  mode
359
358
  });
359
+ const connection = {
360
+ baseUrl,
361
+ headers,
362
+ timeoutMs,
363
+ policy: networkPolicy,
364
+ dispatcherPolicy,
365
+ mode
366
+ };
360
367
  if (params.inputImage) {
361
368
  if (!inputImageNodeId) throw new Error("Comfy edit requests require plugins.entries.comfy.config.<capability>.inputImageNodeId to be configured");
362
- setWorkflowInput(workflow, inputImageNodeId, inputImageInputName, await uploadInputImage({
363
- baseUrl,
364
- headers: new Headers(headers),
365
- timeoutMs,
366
- policy: networkPolicy,
367
- dispatcherPolicy,
368
- image: params.inputImage,
369
- mode,
370
- capability: params.capability
371
- }));
369
+ setWorkflowInput(workflow, inputImageNodeId, inputImageInputName, await uploadInputImage(connection, params.inputImage, params.capability));
372
370
  }
373
371
  const submitPayload = {
374
372
  prompt: workflow,
@@ -389,16 +387,7 @@ async function runComfyWorkflow(params) {
389
387
  });
390
388
  const promptId = normalizeOptionalString(promptResponse.prompt_id);
391
389
  if (!promptId) throw new Error("Comfy workflow submit response missing prompt_id");
392
- const historyEntry = extractHistoryEntry(await waitForComfyHistory({
393
- baseUrl,
394
- promptId,
395
- headers: new Headers(headers),
396
- timeoutMs,
397
- pollIntervalMs,
398
- policy: networkPolicy,
399
- dispatcherPolicy,
400
- mode
401
- }), promptId);
390
+ const historyEntry = extractHistoryEntry(await waitForComfyHistory(connection, promptId, pollIntervalMs), promptId);
402
391
  if (!historyEntry) throw new Error(`Comfy history response missing outputs for prompt ${promptId}`);
403
392
  const outputFiles = collectOutputFiles({
404
393
  history: historyEntry,
@@ -415,17 +404,7 @@ async function runComfyWorkflow(params) {
415
404
  const outputKind = params.capability === "music" ? "audio" : params.capability;
416
405
  const maxOutputBytes = resolveGeneratedMediaMaxBytes(params.cfg, outputKind);
417
406
  for (const output of outputFiles) {
418
- const downloaded = await downloadOutputFile({
419
- baseUrl,
420
- headers: new Headers(headers),
421
- timeoutMs,
422
- policy: networkPolicy,
423
- dispatcherPolicy,
424
- file: output.file,
425
- mode,
426
- capability: params.capability,
427
- maxBytes: maxOutputBytes
428
- });
407
+ const downloaded = await downloadOutputFile(connection, output.file, params.capability, maxOutputBytes);
429
408
  assets.push({
430
409
  ...downloaded,
431
410
  metadata: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/comfy-provider",
3
- "version": "2026.10.2",
3
+ "version": "2026.10.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.10.2"
21
+ "pluginApi": ">=2026.10.5-beta.2"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.10.2",
24
+ "openclawVersion": "2026.10.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.10.2"
43
+ "openclaw": ">=2026.10.5-beta.2"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {