@openclaw/comfy-provider 2026.10.1 → 2026.10.5-beta.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/dist/workflow-history.js +14 -0
- package/dist/workflow-runtime.js +50 -71
- package/package.json +4 -4
|
@@ -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 };
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
151
|
+
const headers = new Headers(requestHeaders);
|
|
150
152
|
headers.delete("Content-Type");
|
|
151
153
|
const payload = await readJsonResponse({
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
...request,
|
|
182
|
+
url: `${baseUrl}${path}`,
|
|
180
183
|
init: {
|
|
181
184
|
method: "GET",
|
|
182
|
-
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 (
|
|
193
|
-
const status = await read(`/api/job/${
|
|
194
|
-
if (status.status === "completed") return await read(`/api/history_v2/${
|
|
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 ??
|
|
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/${
|
|
201
|
-
if (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,
|
|
204
|
-
await
|
|
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
|
|
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(
|
|
246
|
-
type: normalizeOptionalString(
|
|
246
|
+
subfolder: normalizeOptionalString(file.subfolder) ?? "",
|
|
247
|
+
type: normalizeOptionalString(file.type) ?? "output"
|
|
247
248
|
});
|
|
248
|
-
const viewPath =
|
|
249
|
-
const auditContext = `comfy-${
|
|
249
|
+
const viewPath = mode === "cloud" ? "/api/view" : "/view";
|
|
250
|
+
const auditContext = `comfy-${capability}-download`;
|
|
250
251
|
const firstResponse = await fetchWithSsrFGuard({
|
|
251
|
-
|
|
252
|
+
...request,
|
|
253
|
+
url: `${baseUrl}${viewPath}?${query.toString()}`,
|
|
252
254
|
init: {
|
|
253
255
|
method: "GET",
|
|
254
|
-
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:
|
|
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 ${
|
|
263
|
+
const downloadLabel = `Comfy ${capability} output download`;
|
|
265
264
|
return {
|
|
266
|
-
buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel,
|
|
267
|
-
maxBytes
|
|
265
|
+
buffer: await readProviderBinaryResponse(firstResponse.response, downloadLabel, capability, {
|
|
266
|
+
maxBytes,
|
|
268
267
|
chunkTimeoutMs: params.timeoutMs,
|
|
269
|
-
onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`${downloadLabel} exceeds ${
|
|
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.1",
|
|
3
|
+
"version": "2026.10.5-beta.1",
|
|
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.1"
|
|
21
|
+
"pluginApi": ">=2026.10.5-beta.1"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.10.1",
|
|
24
|
+
"openclawVersion": "2026.10.5-beta.1",
|
|
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.1"
|
|
43
|
+
"openclaw": ">=2026.10.5-beta.1"
|
|
44
44
|
},
|
|
45
45
|
"peerDependenciesMeta": {
|
|
46
46
|
"openclaw": {
|