@openclaw/comfy-provider 2026.8.1-beta.3 → 2026.8.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-runtime.js +69 -6
- package/openclaw.plugin.json +21 -0
- package/package.json +4 -4
package/dist/workflow-runtime.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
|
|
3
4
|
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
|
4
5
|
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
5
6
|
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
|
6
7
|
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
7
|
-
import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
|
8
|
+
import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, redactProviderResponseErrorText, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
|
8
9
|
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
9
|
-
import { normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
10
|
+
import { normalizeSecretInputString, resolveConfiguredSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
10
11
|
import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/secret-ref-readonly";
|
|
11
12
|
import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
13
|
import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -16,8 +17,10 @@ const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
|
|
|
16
17
|
const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
|
|
17
18
|
const DEFAULT_PROMPT_INPUT_NAME = "text";
|
|
18
19
|
const DEFAULT_INPUT_IMAGE_INPUT_NAME = "image";
|
|
20
|
+
const DEFAULT_SEED_INPUT_NAME = "seed";
|
|
19
21
|
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
20
22
|
const DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
23
|
+
const RANDOM_SEED_EXCLUSIVE_MAX = 2 ** 48 - 1;
|
|
21
24
|
const DEFAULT_COMFY_MODEL = "workflow";
|
|
22
25
|
function readConfigInteger(config, key) {
|
|
23
26
|
const value = config[key];
|
|
@@ -107,6 +110,34 @@ function setWorkflowInput(params) {
|
|
|
107
110
|
if (!isRecord(inputs)) throw new Error(`Comfy workflow node "${params.nodeId}" is missing an inputs object`);
|
|
108
111
|
inputs[params.inputName] = params.value;
|
|
109
112
|
}
|
|
113
|
+
async function resolveComfyHeadersConfig(value, cfg) {
|
|
114
|
+
const headers = new Headers();
|
|
115
|
+
if (!isRecord(value)) return headers;
|
|
116
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
117
|
+
const path = `plugins.entries.comfy.config.headers.${name}`;
|
|
118
|
+
const inspected = resolveSecretInputString({
|
|
119
|
+
value: headerValue,
|
|
120
|
+
path,
|
|
121
|
+
defaults: cfg.secrets?.defaults,
|
|
122
|
+
mode: "inspect"
|
|
123
|
+
});
|
|
124
|
+
if (inspected.status === "available") {
|
|
125
|
+
headers.set(name, inspected.value);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (inspected.status === "missing") continue;
|
|
129
|
+
const resolved = await resolveConfiguredSecretInputString({
|
|
130
|
+
config: cfg,
|
|
131
|
+
env: process.env,
|
|
132
|
+
value: headerValue,
|
|
133
|
+
path,
|
|
134
|
+
unresolvedReasonStyle: "detailed"
|
|
135
|
+
});
|
|
136
|
+
if (resolved.unresolvedRefReason) throw new Error(`${path} references an unavailable secret: ${resolved.unresolvedRefReason}`);
|
|
137
|
+
if (resolved.value) headers.set(name, resolved.value);
|
|
138
|
+
}
|
|
139
|
+
return headers;
|
|
140
|
+
}
|
|
110
141
|
function resolveComfyNetworkPolicy(params) {
|
|
111
142
|
let parsed;
|
|
112
143
|
try {
|
|
@@ -138,8 +169,9 @@ async function readJsonResponse(params) {
|
|
|
138
169
|
auditContext: params.auditContext
|
|
139
170
|
});
|
|
140
171
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
const requestHeaders = params.init?.headers;
|
|
173
|
+
await assertOkOrThrowHttpError(response, params.errorPrefix, { requestHeaders });
|
|
174
|
+
return await readProviderJsonResponse(response, params.errorPrefix, { requestHeaders });
|
|
143
175
|
} finally {
|
|
144
176
|
await release();
|
|
145
177
|
}
|
|
@@ -230,7 +262,10 @@ async function waitForCloudCompletion(params) {
|
|
|
230
262
|
errorPrefix: "Comfy status lookup failed"
|
|
231
263
|
});
|
|
232
264
|
if (status.status === "completed") return;
|
|
233
|
-
if (status.status === "failed" || status.status === "cancelled")
|
|
265
|
+
if (status.status === "failed" || status.status === "cancelled") {
|
|
266
|
+
const detail = redactProviderResponseErrorText(status.error ?? status.message ?? params.promptId, params.headers);
|
|
267
|
+
throw new Error(`Comfy workflow ${status.status}: ${detail}`);
|
|
268
|
+
}
|
|
234
269
|
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
|
|
235
270
|
await new Promise((resolve) => {
|
|
236
271
|
setTimeout(resolve, pollDelayMs);
|
|
@@ -290,7 +325,7 @@ async function downloadOutputFile(params) {
|
|
|
290
325
|
auditContext
|
|
291
326
|
});
|
|
292
327
|
try {
|
|
293
|
-
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed");
|
|
328
|
+
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed", { requestHeaders: params.headers });
|
|
294
329
|
const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
|
|
295
330
|
return {
|
|
296
331
|
buffer: await readResponseWithLimit(firstResponse.response, params.maxBytes, {
|
|
@@ -304,11 +339,30 @@ async function downloadOutputFile(params) {
|
|
|
304
339
|
await firstResponse.release();
|
|
305
340
|
}
|
|
306
341
|
}
|
|
342
|
+
function hasUnavailableComfyHeaderSecret(value, cfg) {
|
|
343
|
+
if (!isRecord(value)) return false;
|
|
344
|
+
return Object.entries(value).some(([name, headerValue]) => {
|
|
345
|
+
const inspected = resolveSecretInputString({
|
|
346
|
+
value: headerValue,
|
|
347
|
+
path: `plugins.entries.comfy.config.headers.${name}`,
|
|
348
|
+
defaults: cfg?.secrets?.defaults,
|
|
349
|
+
mode: "inspect"
|
|
350
|
+
});
|
|
351
|
+
if (inspected.status !== "configured_unavailable" || inspected.ref.source !== "env") return false;
|
|
352
|
+
const envVarName = inspected.ref.id.trim();
|
|
353
|
+
return !(canResolveEnvSecretRefInReadOnlyPath({
|
|
354
|
+
cfg,
|
|
355
|
+
provider: inspected.ref.provider,
|
|
356
|
+
id: envVarName
|
|
357
|
+
}) && Boolean(normalizeSecretInputString(process.env[envVarName])));
|
|
358
|
+
});
|
|
359
|
+
}
|
|
307
360
|
function isComfyCapabilityConfigured(params) {
|
|
308
361
|
const capabilityConfig = getComfyCapabilityConfig(getComfyConfig(params.cfg), params.capability);
|
|
309
362
|
const hasWorkflow = Boolean(resolveComfyWorkflowSource(capabilityConfig).workflow || normalizeOptionalString(capabilityConfig.workflowPath));
|
|
310
363
|
const hasPromptNode = Boolean(normalizeOptionalString(capabilityConfig.promptNodeId));
|
|
311
364
|
if (!hasWorkflow || !hasPromptNode) return false;
|
|
365
|
+
if (hasUnavailableComfyHeaderSecret(capabilityConfig.headers, params.cfg)) return false;
|
|
312
366
|
if (resolveComfyMode(capabilityConfig) === "local") return true;
|
|
313
367
|
const configuredApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
314
368
|
if (configuredApiKey.status === "available") return true;
|
|
@@ -327,6 +381,8 @@ async function runComfyWorkflow(params) {
|
|
|
327
381
|
const promptInputName = normalizeOptionalString(capabilityConfig.promptInputName) ?? DEFAULT_PROMPT_INPUT_NAME;
|
|
328
382
|
const inputImageNodeId = normalizeOptionalString(capabilityConfig.inputImageNodeId);
|
|
329
383
|
const inputImageInputName = normalizeOptionalString(capabilityConfig.inputImageInputName) ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
|
|
384
|
+
const seedNodeId = normalizeOptionalString(capabilityConfig.seedNodeId);
|
|
385
|
+
const seedInputName = normalizeOptionalString(capabilityConfig.seedInputName) ?? DEFAULT_SEED_INPUT_NAME;
|
|
330
386
|
const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
|
|
331
387
|
const pollIntervalMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "pollIntervalMs"), DEFAULT_POLL_INTERVAL_MS);
|
|
332
388
|
const timeoutMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "timeoutMs") ?? params.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
@@ -337,6 +393,12 @@ async function runComfyWorkflow(params) {
|
|
|
337
393
|
inputName: promptInputName,
|
|
338
394
|
value: params.prompt
|
|
339
395
|
});
|
|
396
|
+
if (seedNodeId) setWorkflowInput({
|
|
397
|
+
workflow,
|
|
398
|
+
nodeId: seedNodeId,
|
|
399
|
+
inputName: seedInputName,
|
|
400
|
+
value: randomInt(RANDOM_SEED_EXCLUSIVE_MAX)
|
|
401
|
+
});
|
|
340
402
|
const pluginApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
341
403
|
const resolvedAuth = mode === "cloud" ? pluginApiKey.status === "available" ? {
|
|
342
404
|
apiKey: pluginApiKey.apiKey,
|
|
@@ -354,6 +416,7 @@ async function runComfyWorkflow(params) {
|
|
|
354
416
|
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
|
|
355
417
|
defaultBaseUrl: mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
|
|
356
418
|
allowPrivateNetwork: mode === "local" || explicitAllowPrivateNetwork,
|
|
419
|
+
headers: await resolveComfyHeadersConfig(capabilityConfig.headers, params.cfg),
|
|
357
420
|
defaultHeaders: mode === "cloud" ? {
|
|
358
421
|
"X-API-Key": resolvedAuth?.apiKey ?? "",
|
|
359
422
|
"Content-Type": "application/json"
|
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.8.1
|
|
3
|
+
"version": "2026.8.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.8.1
|
|
21
|
+
"pluginApi": ">=2026.8.1"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.8.1
|
|
24
|
+
"openclawVersion": "2026.8.1",
|
|
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.8.1
|
|
41
|
+
"openclaw": ">=2026.8.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"openclaw": {
|