@openclaw/comfy-provider 2026.9.1-beta.1 → 2026.9.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.
- package/dist/workflow-runtime.js +75 -15
- package/openclaw.plugin.json +21 -0
- package/package.json +4 -4
package/dist/workflow-runtime.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { bufferToBlobPart } from "openclaw/plugin-sdk/blob-runtime";
|
|
2
4
|
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
|
|
3
5
|
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
|
4
6
|
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
5
7
|
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
|
6
8
|
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
7
|
-
import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
|
9
|
+
import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, redactProviderResponseErrorText, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
|
8
10
|
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
9
|
-
import { normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
11
|
+
import { normalizeSecretInputString, resolveConfiguredSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
10
12
|
import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
|
|
11
13
|
import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
14
|
import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -16,8 +18,10 @@ const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
|
|
|
16
18
|
const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
|
|
17
19
|
const DEFAULT_PROMPT_INPUT_NAME = "text";
|
|
18
20
|
const DEFAULT_INPUT_IMAGE_INPUT_NAME = "image";
|
|
21
|
+
const DEFAULT_SEED_INPUT_NAME = "seed";
|
|
19
22
|
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
20
|
-
const DEFAULT_TIMEOUT_MS =
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 3e5;
|
|
24
|
+
const RANDOM_SEED_EXCLUSIVE_MAX = 2 ** 48 - 1;
|
|
21
25
|
const DEFAULT_COMFY_MODEL = "workflow";
|
|
22
26
|
function readConfigInteger(config, key) {
|
|
23
27
|
const value = config[key];
|
|
@@ -107,6 +111,34 @@ function setWorkflowInput(params) {
|
|
|
107
111
|
if (!isRecord(inputs)) throw new Error(`Comfy workflow node "${params.nodeId}" is missing an inputs object`);
|
|
108
112
|
inputs[params.inputName] = params.value;
|
|
109
113
|
}
|
|
114
|
+
async function resolveComfyHeadersConfig(value, cfg) {
|
|
115
|
+
const headers = new Headers();
|
|
116
|
+
if (!isRecord(value)) return headers;
|
|
117
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
118
|
+
const path = `plugins.entries.comfy.config.headers.${name}`;
|
|
119
|
+
const inspected = resolveSecretInputString({
|
|
120
|
+
value: headerValue,
|
|
121
|
+
path,
|
|
122
|
+
defaults: cfg.secrets?.defaults,
|
|
123
|
+
mode: "inspect"
|
|
124
|
+
});
|
|
125
|
+
if (inspected.status === "available") {
|
|
126
|
+
headers.set(name, inspected.value);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (inspected.status === "missing") continue;
|
|
130
|
+
const resolved = await resolveConfiguredSecretInputString({
|
|
131
|
+
config: cfg,
|
|
132
|
+
env: process.env,
|
|
133
|
+
value: headerValue,
|
|
134
|
+
path,
|
|
135
|
+
unresolvedReasonStyle: "detailed"
|
|
136
|
+
});
|
|
137
|
+
if (resolved.unresolvedRefReason) throw new Error(`${path} references an unavailable secret: ${resolved.unresolvedRefReason}`);
|
|
138
|
+
if (resolved.value) headers.set(name, resolved.value);
|
|
139
|
+
}
|
|
140
|
+
return headers;
|
|
141
|
+
}
|
|
110
142
|
function resolveComfyNetworkPolicy(params) {
|
|
111
143
|
let parsed;
|
|
112
144
|
try {
|
|
@@ -138,8 +170,9 @@ async function readJsonResponse(params) {
|
|
|
138
170
|
auditContext: params.auditContext
|
|
139
171
|
});
|
|
140
172
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
const requestHeaders = params.init?.headers;
|
|
174
|
+
await assertOkOrThrowHttpError(response, params.errorPrefix, { requestHeaders });
|
|
175
|
+
return await readProviderJsonResponse(response, params.errorPrefix, { requestHeaders });
|
|
143
176
|
} finally {
|
|
144
177
|
await release();
|
|
145
178
|
}
|
|
@@ -153,14 +186,9 @@ function resolveFileExtension(params) {
|
|
|
153
186
|
if (dotIndex < 0 || dotIndex === fileName.length - 1) return "bin";
|
|
154
187
|
return fileName.slice(dotIndex + 1);
|
|
155
188
|
}
|
|
156
|
-
function toBlobBytes(buffer) {
|
|
157
|
-
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
|
|
158
|
-
new Uint8Array(arrayBuffer).set(buffer);
|
|
159
|
-
return arrayBuffer;
|
|
160
|
-
}
|
|
161
189
|
async function uploadInputImage(params) {
|
|
162
190
|
const form = new FormData();
|
|
163
|
-
form.set("image", new Blob([
|
|
191
|
+
form.set("image", new Blob([bufferToBlobPart(params.image.buffer)], { type: params.image.mimeType }), normalizeOptionalString(params.image.fileName) || `input.${resolveFileExtension({ mimeType: params.image.mimeType })}`);
|
|
164
192
|
form.set("type", "input");
|
|
165
193
|
form.set("overwrite", "true");
|
|
166
194
|
const headers = new Headers(params.headers);
|
|
@@ -230,7 +258,10 @@ async function waitForCloudCompletion(params) {
|
|
|
230
258
|
errorPrefix: "Comfy status lookup failed"
|
|
231
259
|
});
|
|
232
260
|
if (status.status === "completed") return;
|
|
233
|
-
if (status.status === "failed" || status.status === "cancelled")
|
|
261
|
+
if (status.status === "failed" || status.status === "cancelled") {
|
|
262
|
+
const detail = redactProviderResponseErrorText(status.error ?? status.message ?? params.promptId, params.headers);
|
|
263
|
+
throw new Error(`Comfy workflow ${status.status}: ${detail}`);
|
|
264
|
+
}
|
|
234
265
|
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
|
|
235
266
|
await new Promise((resolve) => {
|
|
236
267
|
setTimeout(resolve, pollDelayMs);
|
|
@@ -290,7 +321,7 @@ async function downloadOutputFile(params) {
|
|
|
290
321
|
auditContext
|
|
291
322
|
});
|
|
292
323
|
try {
|
|
293
|
-
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed");
|
|
324
|
+
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: params.headers });
|
|
294
325
|
const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
|
|
295
326
|
return {
|
|
296
327
|
buffer: await readResponseWithLimit(firstResponse.response, params.maxBytes, {
|
|
@@ -304,11 +335,30 @@ async function downloadOutputFile(params) {
|
|
|
304
335
|
await firstResponse.release();
|
|
305
336
|
}
|
|
306
337
|
}
|
|
338
|
+
function hasUnavailableComfyHeaderSecret(value, cfg) {
|
|
339
|
+
if (!isRecord(value)) return false;
|
|
340
|
+
return Object.entries(value).some(([name, headerValue]) => {
|
|
341
|
+
const inspected = resolveSecretInputString({
|
|
342
|
+
value: headerValue,
|
|
343
|
+
path: `plugins.entries.comfy.config.headers.${name}`,
|
|
344
|
+
defaults: cfg?.secrets?.defaults,
|
|
345
|
+
mode: "inspect"
|
|
346
|
+
});
|
|
347
|
+
if (inspected.status !== "configured_unavailable" || inspected.ref.source !== "env") return false;
|
|
348
|
+
const envVarName = inspected.ref.id.trim();
|
|
349
|
+
return !(canResolveEnvSecretRefInReadOnlyPath({
|
|
350
|
+
cfg,
|
|
351
|
+
provider: inspected.ref.provider,
|
|
352
|
+
id: envVarName
|
|
353
|
+
}) && Boolean(normalizeSecretInputString(process.env[envVarName])));
|
|
354
|
+
});
|
|
355
|
+
}
|
|
307
356
|
function isComfyCapabilityConfigured(params) {
|
|
308
357
|
const capabilityConfig = getComfyCapabilityConfig(getComfyConfig(params.cfg), params.capability);
|
|
309
358
|
const hasWorkflow = Boolean(resolveComfyWorkflowSource(capabilityConfig).workflow || normalizeOptionalString(capabilityConfig.workflowPath));
|
|
310
359
|
const hasPromptNode = Boolean(normalizeOptionalString(capabilityConfig.promptNodeId));
|
|
311
360
|
if (!hasWorkflow || !hasPromptNode) return false;
|
|
361
|
+
if (hasUnavailableComfyHeaderSecret(capabilityConfig.headers, params.cfg)) return false;
|
|
312
362
|
if (resolveComfyMode(capabilityConfig) === "local") return true;
|
|
313
363
|
const configuredApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
314
364
|
if (configuredApiKey.status === "available") return true;
|
|
@@ -327,6 +377,8 @@ async function runComfyWorkflow(params) {
|
|
|
327
377
|
const promptInputName = normalizeOptionalString(capabilityConfig.promptInputName) ?? DEFAULT_PROMPT_INPUT_NAME;
|
|
328
378
|
const inputImageNodeId = normalizeOptionalString(capabilityConfig.inputImageNodeId);
|
|
329
379
|
const inputImageInputName = normalizeOptionalString(capabilityConfig.inputImageInputName) ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
|
|
380
|
+
const seedNodeId = normalizeOptionalString(capabilityConfig.seedNodeId);
|
|
381
|
+
const seedInputName = normalizeOptionalString(capabilityConfig.seedInputName) ?? DEFAULT_SEED_INPUT_NAME;
|
|
330
382
|
const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
|
|
331
383
|
const pollIntervalMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "pollIntervalMs"), DEFAULT_POLL_INTERVAL_MS);
|
|
332
384
|
const timeoutMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "timeoutMs") ?? params.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
@@ -337,6 +389,12 @@ async function runComfyWorkflow(params) {
|
|
|
337
389
|
inputName: promptInputName,
|
|
338
390
|
value: params.prompt
|
|
339
391
|
});
|
|
392
|
+
if (seedNodeId) setWorkflowInput({
|
|
393
|
+
workflow,
|
|
394
|
+
nodeId: seedNodeId,
|
|
395
|
+
inputName: seedInputName,
|
|
396
|
+
value: randomInt(RANDOM_SEED_EXCLUSIVE_MAX)
|
|
397
|
+
});
|
|
340
398
|
const pluginApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
341
399
|
const resolvedAuth = mode === "cloud" ? pluginApiKey.status === "available" ? {
|
|
342
400
|
apiKey: pluginApiKey.apiKey,
|
|
@@ -354,6 +412,7 @@ async function runComfyWorkflow(params) {
|
|
|
354
412
|
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
|
|
355
413
|
defaultBaseUrl: mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
|
|
356
414
|
allowPrivateNetwork: mode === "local" || explicitAllowPrivateNetwork,
|
|
415
|
+
headers: await resolveComfyHeadersConfig(capabilityConfig.headers, params.cfg),
|
|
357
416
|
defaultHeaders: mode === "cloud" ? {
|
|
358
417
|
"X-API-Key": resolvedAuth?.apiKey ?? "",
|
|
359
418
|
"Content-Type": "application/json"
|
|
@@ -391,7 +450,7 @@ async function runComfyWorkflow(params) {
|
|
|
391
450
|
prompt: workflow,
|
|
392
451
|
...mode === "cloud" && resolvedAuth?.apiKey ? { extra_data: { api_key_comfy_org: resolvedAuth.apiKey } } : {}
|
|
393
452
|
};
|
|
394
|
-
const
|
|
453
|
+
const promptResponse = await readJsonResponse({
|
|
395
454
|
url: `${normalizedBaseUrl}${mode === "cloud" ? "/api/prompt" : "/prompt"}`,
|
|
396
455
|
init: {
|
|
397
456
|
method: "POST",
|
|
@@ -403,7 +462,8 @@ async function runComfyWorkflow(params) {
|
|
|
403
462
|
dispatcherPolicy,
|
|
404
463
|
auditContext: `comfy-${params.capability}-generate`,
|
|
405
464
|
errorPrefix: "Comfy workflow submit failed"
|
|
406
|
-
})
|
|
465
|
+
});
|
|
466
|
+
const promptId = normalizeOptionalString(promptResponse.prompt_id);
|
|
407
467
|
if (!promptId) throw new Error("Comfy workflow submit response missing prompt_id");
|
|
408
468
|
const historyEntry = extractHistoryEntry(mode === "cloud" ? await (async () => {
|
|
409
469
|
await waitForCloudCompletion({
|
package/openclaw.plugin.json
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
},
|
|
6
6
|
"enabledByDefault": true,
|
|
7
7
|
"providers": ["comfy"],
|
|
8
|
+
"configContracts": {
|
|
9
|
+
"secretInputs": {
|
|
10
|
+
"paths": [{ "path": "headers.*", "expected": "string" }]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
8
13
|
"setup": {
|
|
9
14
|
"providers": [
|
|
10
15
|
{
|
|
@@ -196,6 +201,10 @@
|
|
|
196
201
|
"allowPrivateNetwork": {
|
|
197
202
|
"type": "boolean"
|
|
198
203
|
},
|
|
204
|
+
"headers": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"additionalProperties": { "type": ["string", "object"] }
|
|
207
|
+
},
|
|
199
208
|
"workflowPath": {
|
|
200
209
|
"type": "string"
|
|
201
210
|
},
|
|
@@ -208,6 +217,12 @@
|
|
|
208
217
|
"promptInputName": {
|
|
209
218
|
"type": "string"
|
|
210
219
|
},
|
|
220
|
+
"seedNodeId": {
|
|
221
|
+
"type": "string"
|
|
222
|
+
},
|
|
223
|
+
"seedInputName": {
|
|
224
|
+
"type": "string"
|
|
225
|
+
},
|
|
211
226
|
"inputImageNodeId": {
|
|
212
227
|
"type": "string"
|
|
213
228
|
},
|
|
@@ -233,6 +248,8 @@
|
|
|
233
248
|
"workflow": { "type": "object" },
|
|
234
249
|
"promptNodeId": { "type": "string" },
|
|
235
250
|
"promptInputName": { "type": "string" },
|
|
251
|
+
"seedNodeId": { "type": "string" },
|
|
252
|
+
"seedInputName": { "type": "string" },
|
|
236
253
|
"inputImageNodeId": { "type": "string" },
|
|
237
254
|
"inputImageInputName": { "type": "string" },
|
|
238
255
|
"outputNodeId": { "type": "string" },
|
|
@@ -248,6 +265,8 @@
|
|
|
248
265
|
"workflow": { "type": "object" },
|
|
249
266
|
"promptNodeId": { "type": "string" },
|
|
250
267
|
"promptInputName": { "type": "string" },
|
|
268
|
+
"seedNodeId": { "type": "string" },
|
|
269
|
+
"seedInputName": { "type": "string" },
|
|
251
270
|
"inputImageNodeId": { "type": "string" },
|
|
252
271
|
"inputImageInputName": { "type": "string" },
|
|
253
272
|
"outputNodeId": { "type": "string" },
|
|
@@ -263,6 +282,8 @@
|
|
|
263
282
|
"workflow": { "type": "object" },
|
|
264
283
|
"promptNodeId": { "type": "string" },
|
|
265
284
|
"promptInputName": { "type": "string" },
|
|
285
|
+
"seedNodeId": { "type": "string" },
|
|
286
|
+
"seedInputName": { "type": "string" },
|
|
266
287
|
"outputNodeId": { "type": "string" },
|
|
267
288
|
"pollIntervalMs": { "type": "integer", "minimum": 100 },
|
|
268
289
|
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/comfy-provider",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.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.9.
|
|
21
|
+
"pluginApi": ">=2026.9.2"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.9.
|
|
24
|
+
"openclawVersion": "2026.9.2",
|
|
25
25
|
"bundledDist": false
|
|
26
26
|
},
|
|
27
27
|
"release": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"README.md"
|
|
39
39
|
],
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"openclaw": ">=2026.9.
|
|
41
|
+
"openclaw": ">=2026.9.2"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"openclaw": {
|