@openclaw/comfy-provider 0.0.0 → 2026.8.1-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.
- package/README.md +24 -1
- package/dist/image-generation-provider.js +63 -0
- package/dist/index.js +44 -0
- package/dist/music-generation-provider.js +63 -0
- package/dist/video-generation-provider.js +82 -0
- package/dist/workflow-runtime.js +480 -0
- package/openclaw.plugin.json +273 -0
- package/package.json +43 -8
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
1
|
# @openclaw/comfy-provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official ComfyUI image, video, and music generation provider plugin for
|
|
4
|
+
OpenClaw.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
openclaw plugins install @openclaw/comfy-provider
|
|
10
|
+
openclaw gateway restart
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configure
|
|
14
|
+
|
|
15
|
+
Local ComfyUI workflows do not require credentials. Comfy Cloud workflows use
|
|
16
|
+
`COMFY_API_KEY` or `COMFY_CLOUD_API_KEY`.
|
|
17
|
+
|
|
18
|
+
Full workflow, model, and provider configuration:
|
|
19
|
+
|
|
20
|
+
- https://docs.openclaw.ai/providers/comfy
|
|
21
|
+
|
|
22
|
+
## Package
|
|
23
|
+
|
|
24
|
+
- Plugin id: `comfy`
|
|
25
|
+
- Package: `@openclaw/comfy-provider`
|
|
26
|
+
- Minimum OpenClaw host: `2026.7.2`
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, 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
|
+
capabilities: {
|
|
15
|
+
generate: {
|
|
16
|
+
maxCount: 1,
|
|
17
|
+
supportsSize: false,
|
|
18
|
+
supportsAspectRatio: false,
|
|
19
|
+
supportsResolution: false
|
|
20
|
+
},
|
|
21
|
+
edit: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
maxCount: 1,
|
|
24
|
+
maxInputImages: 1,
|
|
25
|
+
supportsSize: false,
|
|
26
|
+
supportsAspectRatio: false,
|
|
27
|
+
supportsResolution: false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async generateImage(req) {
|
|
31
|
+
if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy image generation currently supports at most one reference image");
|
|
32
|
+
const result = await runComfyWorkflow({
|
|
33
|
+
cfg: req.cfg,
|
|
34
|
+
agentDir: req.agentDir,
|
|
35
|
+
authStore: req.authStore,
|
|
36
|
+
prompt: req.prompt,
|
|
37
|
+
model: req.model,
|
|
38
|
+
timeoutMs: req.timeoutMs,
|
|
39
|
+
capability: "image",
|
|
40
|
+
outputKinds: ["images"],
|
|
41
|
+
inputImage: req.inputImages?.[0]
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
images: result.assets.map((asset) => ({
|
|
45
|
+
buffer: asset.buffer,
|
|
46
|
+
mimeType: asset.mimeType,
|
|
47
|
+
fileName: asset.fileName,
|
|
48
|
+
metadata: {
|
|
49
|
+
nodeId: asset.nodeId,
|
|
50
|
+
promptId: result.promptId
|
|
51
|
+
}
|
|
52
|
+
})),
|
|
53
|
+
model: result.model,
|
|
54
|
+
metadata: {
|
|
55
|
+
promptId: result.promptId,
|
|
56
|
+
outputNodeIds: result.outputNodeIds
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { buildComfyImageGenerationProvider };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { buildComfyImageGenerationProvider } from "./image-generation-provider.js";
|
|
2
|
+
import { buildComfyMusicGenerationProvider } from "./music-generation-provider.js";
|
|
3
|
+
import { buildComfyVideoGenerationProvider } from "./video-generation-provider.js";
|
|
4
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
5
|
+
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
|
6
|
+
//#region extensions/comfy/index.ts
|
|
7
|
+
const PROVIDER_ID = "comfy";
|
|
8
|
+
var comfy_default = definePluginEntry({
|
|
9
|
+
id: PROVIDER_ID,
|
|
10
|
+
name: "ComfyUI Provider",
|
|
11
|
+
description: "Bundled ComfyUI workflow media generation provider",
|
|
12
|
+
register(api) {
|
|
13
|
+
api.registerProvider({
|
|
14
|
+
id: PROVIDER_ID,
|
|
15
|
+
label: "ComfyUI",
|
|
16
|
+
docsPath: "/providers/comfy",
|
|
17
|
+
envVars: ["COMFY_API_KEY", "COMFY_CLOUD_API_KEY"],
|
|
18
|
+
auth: [createProviderApiKeyAuthMethod({
|
|
19
|
+
providerId: PROVIDER_ID,
|
|
20
|
+
methodId: "cloud-api-key",
|
|
21
|
+
label: "Comfy Cloud API key",
|
|
22
|
+
hint: "API key for Comfy Cloud workflow runs",
|
|
23
|
+
optionKey: "comfyApiKey",
|
|
24
|
+
flagName: "--comfy-api-key",
|
|
25
|
+
envVar: "COMFY_API_KEY",
|
|
26
|
+
promptMessage: "Enter Comfy Cloud API key",
|
|
27
|
+
wizard: {
|
|
28
|
+
choiceId: "comfy-cloud-api-key",
|
|
29
|
+
choiceLabel: "Comfy Cloud API key",
|
|
30
|
+
choiceHint: "Required for cloud workflows",
|
|
31
|
+
groupId: "comfy",
|
|
32
|
+
groupLabel: "ComfyUI",
|
|
33
|
+
groupHint: "Local or cloud workflows",
|
|
34
|
+
onboardingScopes: ["image-generation"]
|
|
35
|
+
}
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
api.registerImageGenerationProvider(buildComfyImageGenerationProvider());
|
|
39
|
+
api.registerMusicGenerationProvider(buildComfyMusicGenerationProvider());
|
|
40
|
+
api.registerVideoGenerationProvider(buildComfyVideoGenerationProvider());
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
//#endregion
|
|
44
|
+
export { comfy_default as default };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, runComfyWorkflow } from "./workflow-runtime.js";
|
|
2
|
+
//#region extensions/comfy/music-generation-provider.ts
|
|
3
|
+
const COMFY_MAX_INPUT_IMAGES = 1;
|
|
4
|
+
function toGeneratedTrack(asset) {
|
|
5
|
+
return {
|
|
6
|
+
buffer: asset.buffer,
|
|
7
|
+
mimeType: asset.mimeType,
|
|
8
|
+
fileName: asset.fileName
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function resolveInputImage(inputImage) {
|
|
12
|
+
if (!inputImage) return;
|
|
13
|
+
if (!inputImage.buffer) throw new Error("Comfy music generation requires loaded reference image bytes.");
|
|
14
|
+
return {
|
|
15
|
+
buffer: inputImage.buffer,
|
|
16
|
+
mimeType: inputImage.mimeType ?? "image/png",
|
|
17
|
+
fileName: inputImage.fileName
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildComfyMusicGenerationProvider() {
|
|
21
|
+
return {
|
|
22
|
+
id: "comfy",
|
|
23
|
+
label: "ComfyUI",
|
|
24
|
+
defaultModel: DEFAULT_COMFY_MODEL,
|
|
25
|
+
models: [DEFAULT_COMFY_MODEL],
|
|
26
|
+
isConfigured: ({ cfg, agentDir }) => isComfyCapabilityConfigured({
|
|
27
|
+
cfg,
|
|
28
|
+
agentDir,
|
|
29
|
+
capability: "music"
|
|
30
|
+
}),
|
|
31
|
+
capabilities: {
|
|
32
|
+
generate: {},
|
|
33
|
+
edit: {
|
|
34
|
+
enabled: true,
|
|
35
|
+
maxInputImages: COMFY_MAX_INPUT_IMAGES
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async generateMusic(req) {
|
|
39
|
+
if ((req.inputImages?.length ?? 0) > COMFY_MAX_INPUT_IMAGES) throw new Error(`Comfy music generation supports at most ${COMFY_MAX_INPUT_IMAGES} reference image.`);
|
|
40
|
+
const result = await runComfyWorkflow({
|
|
41
|
+
cfg: req.cfg,
|
|
42
|
+
agentDir: req.agentDir,
|
|
43
|
+
authStore: req.authStore,
|
|
44
|
+
prompt: req.prompt,
|
|
45
|
+
model: req.model,
|
|
46
|
+
capability: "music",
|
|
47
|
+
outputKinds: ["audio"],
|
|
48
|
+
inputImage: resolveInputImage(req.inputImages?.[0])
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
tracks: result.assets.map(toGeneratedTrack),
|
|
52
|
+
model: result.model,
|
|
53
|
+
metadata: {
|
|
54
|
+
promptId: result.promptId,
|
|
55
|
+
outputNodeIds: result.outputNodeIds,
|
|
56
|
+
inputImageCount: req.inputImages?.length ?? 0
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { buildComfyMusicGenerationProvider };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, 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
|
+
capabilities: {
|
|
24
|
+
generate: {
|
|
25
|
+
maxVideos: 1,
|
|
26
|
+
supportsSize: false,
|
|
27
|
+
supportsAspectRatio: false,
|
|
28
|
+
supportsResolution: false,
|
|
29
|
+
supportsAudio: false,
|
|
30
|
+
supportsWatermark: false
|
|
31
|
+
},
|
|
32
|
+
imageToVideo: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
maxVideos: 1,
|
|
35
|
+
maxInputImages: 1,
|
|
36
|
+
supportsSize: false,
|
|
37
|
+
supportsAspectRatio: false,
|
|
38
|
+
supportsResolution: false,
|
|
39
|
+
supportsAudio: false,
|
|
40
|
+
supportsWatermark: false
|
|
41
|
+
},
|
|
42
|
+
videoToVideo: { enabled: false }
|
|
43
|
+
},
|
|
44
|
+
async generateVideo(req) {
|
|
45
|
+
if ((req.inputImages?.length ?? 0) > 1) throw new Error("Comfy video generation currently supports at most one reference image");
|
|
46
|
+
if ((req.inputVideos?.length ?? 0) > 0) throw new Error("Comfy video generation does not support input videos");
|
|
47
|
+
const result = await runComfyWorkflow({
|
|
48
|
+
cfg: req.cfg,
|
|
49
|
+
agentDir: req.agentDir,
|
|
50
|
+
authStore: req.authStore,
|
|
51
|
+
prompt: req.prompt,
|
|
52
|
+
model: req.model,
|
|
53
|
+
timeoutMs: req.timeoutMs,
|
|
54
|
+
capability: "video",
|
|
55
|
+
outputKinds: [
|
|
56
|
+
"images",
|
|
57
|
+
"gifs",
|
|
58
|
+
"videos"
|
|
59
|
+
],
|
|
60
|
+
inputImage: toComfyInputImage(req.inputImages?.[0])
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
videos: result.assets.map((asset) => ({
|
|
64
|
+
buffer: asset.buffer,
|
|
65
|
+
mimeType: asset.mimeType,
|
|
66
|
+
fileName: asset.fileName,
|
|
67
|
+
metadata: {
|
|
68
|
+
nodeId: asset.nodeId,
|
|
69
|
+
promptId: result.promptId
|
|
70
|
+
}
|
|
71
|
+
})),
|
|
72
|
+
model: result.model,
|
|
73
|
+
metadata: {
|
|
74
|
+
promptId: result.promptId,
|
|
75
|
+
outputNodeIds: result.outputNodeIds
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { buildComfyVideoGenerationProvider };
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/extension-shared";
|
|
3
|
+
import { resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
|
|
4
|
+
import { extensionForMime } from "openclaw/plugin-sdk/media-mime";
|
|
5
|
+
import { resolvePositiveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
6
|
+
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
|
|
7
|
+
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
|
|
8
|
+
import { assertOkOrThrowHttpError, normalizeBaseUrl, readProviderJsonResponse, resolveProviderHttpRequestConfig } from "openclaw/plugin-sdk/provider-http";
|
|
9
|
+
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
10
|
+
import { normalizeSecretInputString, resolveSecretInputString } from "openclaw/plugin-sdk/secret-input-runtime";
|
|
11
|
+
import { fetchWithSsrFGuard, isPrivateOrLoopbackHost, mergeSsrFPolicies, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
|
+
import { asBoolean, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
|
+
import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
14
|
+
//#region extensions/comfy/workflow-runtime.ts
|
|
15
|
+
const DEFAULT_COMFY_LOCAL_BASE_URL = "http://127.0.0.1:8188";
|
|
16
|
+
const DEFAULT_COMFY_CLOUD_BASE_URL = "https://cloud.comfy.org";
|
|
17
|
+
const DEFAULT_PROMPT_INPUT_NAME = "text";
|
|
18
|
+
const DEFAULT_INPUT_IMAGE_INPUT_NAME = "image";
|
|
19
|
+
const DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
21
|
+
const DEFAULT_COMFY_MODEL = "workflow";
|
|
22
|
+
function readConfigInteger(config, key) {
|
|
23
|
+
const value = config[key];
|
|
24
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
25
|
+
}
|
|
26
|
+
function getComfyConfig(cfg) {
|
|
27
|
+
const pluginConfig = cfg?.plugins?.entries?.comfy?.config;
|
|
28
|
+
if (isRecord(pluginConfig)) return pluginConfig;
|
|
29
|
+
const legacyConfig = cfg?.models?.providers?.comfy;
|
|
30
|
+
return isRecord(legacyConfig) ? legacyConfig : {};
|
|
31
|
+
}
|
|
32
|
+
function stripNestedCapabilityConfig(config) {
|
|
33
|
+
const next = { ...config };
|
|
34
|
+
delete next.image;
|
|
35
|
+
delete next.video;
|
|
36
|
+
delete next.music;
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
function getComfyCapabilityConfig(config, capability) {
|
|
40
|
+
const shared = stripNestedCapabilityConfig(config);
|
|
41
|
+
const nested = config[capability];
|
|
42
|
+
if (!isRecord(nested)) return shared;
|
|
43
|
+
return {
|
|
44
|
+
...shared,
|
|
45
|
+
...nested
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function resolveComfyMode(config) {
|
|
49
|
+
return normalizeOptionalString(config.mode) === "cloud" ? "cloud" : "local";
|
|
50
|
+
}
|
|
51
|
+
function resolveComfyApiKey(config, cfg) {
|
|
52
|
+
const resolved = resolveSecretInputString({
|
|
53
|
+
value: config.apiKey,
|
|
54
|
+
path: "plugins.entries.comfy.config.apiKey",
|
|
55
|
+
defaults: cfg?.secrets?.defaults,
|
|
56
|
+
mode: "inspect"
|
|
57
|
+
});
|
|
58
|
+
if (resolved.status === "available") {
|
|
59
|
+
const apiKey = normalizeSecretInputString(resolved.value);
|
|
60
|
+
return apiKey ? {
|
|
61
|
+
status: "available",
|
|
62
|
+
apiKey,
|
|
63
|
+
source: "plugins.entries.comfy.config.apiKey"
|
|
64
|
+
} : { status: "missing" };
|
|
65
|
+
}
|
|
66
|
+
if (resolved.status === "configured_unavailable") {
|
|
67
|
+
if (resolved.ref.source !== "env") return { status: "configured_unavailable" };
|
|
68
|
+
const envVarName = resolved.ref.id.trim();
|
|
69
|
+
if (!canResolveEnvSecretRefInReadOnlyPath({
|
|
70
|
+
cfg,
|
|
71
|
+
provider: resolved.ref.provider,
|
|
72
|
+
id: envVarName
|
|
73
|
+
})) return { status: "configured_unavailable" };
|
|
74
|
+
const apiKey = normalizeSecretInputString(process.env[envVarName]);
|
|
75
|
+
return apiKey ? {
|
|
76
|
+
status: "available",
|
|
77
|
+
apiKey,
|
|
78
|
+
source: `plugins.entries.comfy.config.apiKey (${envVarName})`
|
|
79
|
+
} : { status: "configured_unavailable" };
|
|
80
|
+
}
|
|
81
|
+
return { status: "missing" };
|
|
82
|
+
}
|
|
83
|
+
function getRequiredConfigString(config, key) {
|
|
84
|
+
const value = normalizeOptionalString(config[key]);
|
|
85
|
+
if (!value) throw new Error(`plugins.entries.comfy.config.${key} is required`);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function resolveComfyWorkflowSource(config) {
|
|
89
|
+
const workflow = config.workflow;
|
|
90
|
+
if (isRecord(workflow)) return { workflow: structuredClone(workflow) };
|
|
91
|
+
return { workflowPath: normalizeOptionalString(config.workflowPath) };
|
|
92
|
+
}
|
|
93
|
+
async function loadComfyWorkflow(config) {
|
|
94
|
+
const source = resolveComfyWorkflowSource(config);
|
|
95
|
+
if (source.workflow) return source.workflow;
|
|
96
|
+
if (!source.workflowPath) throw new Error("plugins.entries.comfy.config.<capability>.workflow or workflowPath is required");
|
|
97
|
+
const resolvedPath = resolveUserPath(source.workflowPath);
|
|
98
|
+
const raw = await fs.readFile(resolvedPath, "utf8");
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (!isRecord(parsed)) throw new Error(`Comfy workflow at ${resolvedPath} must be a JSON object`);
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
function setWorkflowInput(params) {
|
|
104
|
+
const node = params.workflow[params.nodeId];
|
|
105
|
+
if (!isRecord(node)) throw new Error(`Comfy workflow missing node "${params.nodeId}"`);
|
|
106
|
+
const inputs = node.inputs;
|
|
107
|
+
if (!isRecord(inputs)) throw new Error(`Comfy workflow node "${params.nodeId}" is missing an inputs object`);
|
|
108
|
+
inputs[params.inputName] = params.value;
|
|
109
|
+
}
|
|
110
|
+
function resolveComfyNetworkPolicy(params) {
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = new URL(params.baseUrl);
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
const hostname = normalizeOptionalLowercaseString(parsed.hostname) ?? "";
|
|
118
|
+
if (!hostname) return {};
|
|
119
|
+
const localHostnamePolicy = params.mode === "local" ? { hostnameAllowlist: [hostname] } : void 0;
|
|
120
|
+
const hostnameOnlyPolicy = localHostnamePolicy ? { apiPolicy: localHostnamePolicy } : {};
|
|
121
|
+
if (!params.allowPrivateNetwork) return hostnameOnlyPolicy;
|
|
122
|
+
if (!params.explicitAllowPrivateNetwork && params.mode !== "local") return {};
|
|
123
|
+
if (!params.explicitAllowPrivateNetwork && params.mode === "local" && !isPrivateOrLoopbackHost(hostname) && !isSingleLabelServiceHostname(hostname)) return hostnameOnlyPolicy;
|
|
124
|
+
const originPolicy = ssrfPolicyFromHttpBaseUrlAllowedOrigin(params.baseUrl);
|
|
125
|
+
if (!originPolicy) return hostnameOnlyPolicy;
|
|
126
|
+
return { apiPolicy: params.mode === "local" ? mergeSsrFPolicies(originPolicy, localHostnamePolicy) : originPolicy };
|
|
127
|
+
}
|
|
128
|
+
function isSingleLabelServiceHostname(hostname) {
|
|
129
|
+
return /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/u.test(hostname);
|
|
130
|
+
}
|
|
131
|
+
async function readJsonResponse(params) {
|
|
132
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
133
|
+
url: params.url,
|
|
134
|
+
init: params.init,
|
|
135
|
+
timeoutMs: params.timeoutMs,
|
|
136
|
+
policy: params.policy,
|
|
137
|
+
dispatcherPolicy: params.dispatcherPolicy,
|
|
138
|
+
auditContext: params.auditContext
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
await assertOkOrThrowHttpError(response, params.errorPrefix);
|
|
142
|
+
return await readProviderJsonResponse(response, params.errorPrefix);
|
|
143
|
+
} finally {
|
|
144
|
+
await release();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function resolveFileExtension(params) {
|
|
148
|
+
const extension = extensionForMime(params.mimeType);
|
|
149
|
+
if (extension) return extension.slice(1);
|
|
150
|
+
const fileName = params.fileName?.trim();
|
|
151
|
+
if (!fileName) return "bin";
|
|
152
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
153
|
+
if (dotIndex < 0 || dotIndex === fileName.length - 1) return "bin";
|
|
154
|
+
return fileName.slice(dotIndex + 1);
|
|
155
|
+
}
|
|
156
|
+
function toBlobBytes(buffer) {
|
|
157
|
+
const arrayBuffer = new ArrayBuffer(buffer.byteLength);
|
|
158
|
+
new Uint8Array(arrayBuffer).set(buffer);
|
|
159
|
+
return arrayBuffer;
|
|
160
|
+
}
|
|
161
|
+
async function uploadInputImage(params) {
|
|
162
|
+
const form = new FormData();
|
|
163
|
+
form.set("image", new Blob([toBlobBytes(params.image.buffer)], { type: params.image.mimeType }), normalizeOptionalString(params.image.fileName) || `input.${resolveFileExtension({ mimeType: params.image.mimeType })}`);
|
|
164
|
+
form.set("type", "input");
|
|
165
|
+
form.set("overwrite", "true");
|
|
166
|
+
const headers = new Headers(params.headers);
|
|
167
|
+
headers.delete("Content-Type");
|
|
168
|
+
const payload = await readJsonResponse({
|
|
169
|
+
url: `${params.baseUrl}${params.mode === "cloud" ? "/api/upload/image" : "/upload/image"}`,
|
|
170
|
+
init: {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers,
|
|
173
|
+
body: form
|
|
174
|
+
},
|
|
175
|
+
timeoutMs: params.timeoutMs,
|
|
176
|
+
policy: params.policy,
|
|
177
|
+
dispatcherPolicy: params.dispatcherPolicy,
|
|
178
|
+
auditContext: `comfy-${params.capability}-upload`,
|
|
179
|
+
errorPrefix: "Comfy image upload failed"
|
|
180
|
+
});
|
|
181
|
+
const uploadedName = normalizeOptionalString(payload.filename) || normalizeOptionalString(payload.name);
|
|
182
|
+
if (!uploadedName) throw new Error("Comfy image upload response missing filename");
|
|
183
|
+
return uploadedName;
|
|
184
|
+
}
|
|
185
|
+
function extractHistoryEntry(history, promptId) {
|
|
186
|
+
if (!isRecord(history)) return null;
|
|
187
|
+
const directOutputs = history.outputs;
|
|
188
|
+
if (isRecord(directOutputs)) return history;
|
|
189
|
+
const nested = history[promptId];
|
|
190
|
+
if (isRecord(nested)) return nested;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
async function waitForLocalHistory(params) {
|
|
194
|
+
const deadline = Date.now() + params.timeoutMs;
|
|
195
|
+
for (;;) {
|
|
196
|
+
const requestTimeoutMs = resolveComfyRemainingMs(deadline, params.timeoutMs);
|
|
197
|
+
const entry = extractHistoryEntry(await readJsonResponse({
|
|
198
|
+
url: `${params.baseUrl}/history/${params.promptId}`,
|
|
199
|
+
init: {
|
|
200
|
+
method: "GET",
|
|
201
|
+
headers: params.headers
|
|
202
|
+
},
|
|
203
|
+
timeoutMs: requestTimeoutMs,
|
|
204
|
+
policy: params.policy,
|
|
205
|
+
dispatcherPolicy: params.dispatcherPolicy,
|
|
206
|
+
auditContext: "comfy-history",
|
|
207
|
+
errorPrefix: "Comfy history lookup failed"
|
|
208
|
+
}), params.promptId);
|
|
209
|
+
if (entry?.outputs && Object.keys(entry.outputs).length > 0) return entry;
|
|
210
|
+
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
|
|
211
|
+
await new Promise((resolve) => {
|
|
212
|
+
setTimeout(resolve, pollDelayMs);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function waitForCloudCompletion(params) {
|
|
217
|
+
const deadline = Date.now() + params.timeoutMs;
|
|
218
|
+
for (;;) {
|
|
219
|
+
const requestTimeoutMs = resolveComfyRemainingMs(deadline, params.timeoutMs);
|
|
220
|
+
const status = await readJsonResponse({
|
|
221
|
+
url: `${params.baseUrl}/api/job/${params.promptId}/status`,
|
|
222
|
+
init: {
|
|
223
|
+
method: "GET",
|
|
224
|
+
headers: params.headers
|
|
225
|
+
},
|
|
226
|
+
timeoutMs: requestTimeoutMs,
|
|
227
|
+
policy: params.policy,
|
|
228
|
+
dispatcherPolicy: params.dispatcherPolicy,
|
|
229
|
+
auditContext: "comfy-status",
|
|
230
|
+
errorPrefix: "Comfy status lookup failed"
|
|
231
|
+
});
|
|
232
|
+
if (status.status === "completed") return;
|
|
233
|
+
if (status.status === "failed" || status.status === "cancelled") throw new Error(`Comfy workflow ${status.status}: ${status.error ?? status.message ?? params.promptId}`);
|
|
234
|
+
const pollDelayMs = resolveComfyRemainingMs(deadline, params.timeoutMs, params.pollIntervalMs);
|
|
235
|
+
await new Promise((resolve) => {
|
|
236
|
+
setTimeout(resolve, pollDelayMs);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function resolveComfyRemainingMs(deadline, timeoutMs, defaultTimeoutMs = timeoutMs) {
|
|
241
|
+
const defaultMs = resolvePositiveTimerTimeoutMs(defaultTimeoutMs, 1);
|
|
242
|
+
const remainingMs = deadline - Date.now();
|
|
243
|
+
if (remainingMs <= 0) throw new Error(`Comfy workflow did not finish within ${Math.ceil(timeoutMs / 1e3)}s`);
|
|
244
|
+
return Math.max(1, Math.min(defaultMs, remainingMs));
|
|
245
|
+
}
|
|
246
|
+
function collectOutputFiles(params) {
|
|
247
|
+
const outputs = params.history.outputs;
|
|
248
|
+
if (!outputs) return [];
|
|
249
|
+
const nodeIds = params.outputNodeId ? [params.outputNodeId] : Object.keys(outputs);
|
|
250
|
+
const files = [];
|
|
251
|
+
for (const nodeId of nodeIds) {
|
|
252
|
+
const entry = outputs[nodeId];
|
|
253
|
+
if (!entry) continue;
|
|
254
|
+
for (const kind of params.outputKinds) {
|
|
255
|
+
const bucket = entry[kind];
|
|
256
|
+
if (!Array.isArray(bucket)) continue;
|
|
257
|
+
for (const file of bucket) {
|
|
258
|
+
if (params.capability === "video" && kind === "images") {
|
|
259
|
+
const fileName = normalizeOptionalString(file.filename) || normalizeOptionalString(file.name);
|
|
260
|
+
if (!fileName || !/\.(?:mp4|webm)$/i.test(fileName)) continue;
|
|
261
|
+
}
|
|
262
|
+
files.push({
|
|
263
|
+
nodeId,
|
|
264
|
+
file
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return files;
|
|
270
|
+
}
|
|
271
|
+
async function downloadOutputFile(params) {
|
|
272
|
+
const fileName = normalizeOptionalString(params.file.filename) || normalizeOptionalString(params.file.name);
|
|
273
|
+
if (!fileName) throw new Error("Comfy output entry missing filename");
|
|
274
|
+
const query = new URLSearchParams({
|
|
275
|
+
filename: fileName,
|
|
276
|
+
subfolder: normalizeOptionalString(params.file.subfolder) ?? "",
|
|
277
|
+
type: normalizeOptionalString(params.file.type) ?? "output"
|
|
278
|
+
});
|
|
279
|
+
const viewPath = params.mode === "cloud" ? "/api/view" : "/view";
|
|
280
|
+
const auditContext = `comfy-${params.capability}-download`;
|
|
281
|
+
const firstResponse = await fetchWithSsrFGuard({
|
|
282
|
+
url: `${params.baseUrl}${viewPath}?${query.toString()}`,
|
|
283
|
+
init: {
|
|
284
|
+
method: "GET",
|
|
285
|
+
headers: params.headers
|
|
286
|
+
},
|
|
287
|
+
timeoutMs: params.timeoutMs,
|
|
288
|
+
policy: params.policy,
|
|
289
|
+
dispatcherPolicy: params.dispatcherPolicy,
|
|
290
|
+
auditContext
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed");
|
|
294
|
+
const mimeType = normalizeOptionalString(firstResponse.response.headers.get("content-type")) || "application/octet-stream";
|
|
295
|
+
return {
|
|
296
|
+
buffer: await readResponseWithLimit(firstResponse.response, params.maxBytes, {
|
|
297
|
+
chunkTimeoutMs: params.timeoutMs,
|
|
298
|
+
onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`Comfy ${params.capability} output download exceeds ${maxBytes} bytes`),
|
|
299
|
+
onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Comfy ${params.capability} output download stalled after ${chunkTimeoutMs}ms`)
|
|
300
|
+
}),
|
|
301
|
+
mimeType
|
|
302
|
+
};
|
|
303
|
+
} finally {
|
|
304
|
+
await firstResponse.release();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function isComfyCapabilityConfigured(params) {
|
|
308
|
+
const capabilityConfig = getComfyCapabilityConfig(getComfyConfig(params.cfg), params.capability);
|
|
309
|
+
const hasWorkflow = Boolean(resolveComfyWorkflowSource(capabilityConfig).workflow || normalizeOptionalString(capabilityConfig.workflowPath));
|
|
310
|
+
const hasPromptNode = Boolean(normalizeOptionalString(capabilityConfig.promptNodeId));
|
|
311
|
+
if (!hasWorkflow || !hasPromptNode) return false;
|
|
312
|
+
if (resolveComfyMode(capabilityConfig) === "local") return true;
|
|
313
|
+
const configuredApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
314
|
+
if (configuredApiKey.status === "available") return true;
|
|
315
|
+
if (configuredApiKey.status === "configured_unavailable") return false;
|
|
316
|
+
return isProviderApiKeyConfigured({
|
|
317
|
+
provider: "comfy",
|
|
318
|
+
cfg: params.cfg,
|
|
319
|
+
agentDir: params.agentDir
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function runComfyWorkflow(params) {
|
|
323
|
+
const capabilityConfig = getComfyCapabilityConfig(getComfyConfig(params.cfg), params.capability);
|
|
324
|
+
const mode = resolveComfyMode(capabilityConfig);
|
|
325
|
+
const workflow = await loadComfyWorkflow(capabilityConfig);
|
|
326
|
+
const promptNodeId = getRequiredConfigString(capabilityConfig, "promptNodeId");
|
|
327
|
+
const promptInputName = normalizeOptionalString(capabilityConfig.promptInputName) ?? DEFAULT_PROMPT_INPUT_NAME;
|
|
328
|
+
const inputImageNodeId = normalizeOptionalString(capabilityConfig.inputImageNodeId);
|
|
329
|
+
const inputImageInputName = normalizeOptionalString(capabilityConfig.inputImageInputName) ?? DEFAULT_INPUT_IMAGE_INPUT_NAME;
|
|
330
|
+
const outputNodeId = normalizeOptionalString(capabilityConfig.outputNodeId);
|
|
331
|
+
const pollIntervalMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "pollIntervalMs"), DEFAULT_POLL_INTERVAL_MS);
|
|
332
|
+
const timeoutMs = resolvePositiveTimerTimeoutMs(readConfigInteger(capabilityConfig, "timeoutMs") ?? params.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
333
|
+
const providerModel = normalizeOptionalString(params.model) || "workflow";
|
|
334
|
+
setWorkflowInput({
|
|
335
|
+
workflow,
|
|
336
|
+
nodeId: promptNodeId,
|
|
337
|
+
inputName: promptInputName,
|
|
338
|
+
value: params.prompt
|
|
339
|
+
});
|
|
340
|
+
const pluginApiKey = resolveComfyApiKey(capabilityConfig, params.cfg);
|
|
341
|
+
const resolvedAuth = mode === "cloud" ? pluginApiKey.status === "available" ? {
|
|
342
|
+
apiKey: pluginApiKey.apiKey,
|
|
343
|
+
source: pluginApiKey.source,
|
|
344
|
+
mode: "api-key"
|
|
345
|
+
} : pluginApiKey.status === "configured_unavailable" ? null : await resolveApiKeyForProvider({
|
|
346
|
+
provider: "comfy",
|
|
347
|
+
cfg: params.cfg,
|
|
348
|
+
agentDir: params.agentDir,
|
|
349
|
+
store: params.authStore
|
|
350
|
+
}) : null;
|
|
351
|
+
if (mode === "cloud" && !resolvedAuth?.apiKey) throw new Error("Comfy Cloud API key missing");
|
|
352
|
+
const explicitAllowPrivateNetwork = asBoolean(capabilityConfig.allowPrivateNetwork) === true;
|
|
353
|
+
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } = resolveProviderHttpRequestConfig({
|
|
354
|
+
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
|
|
355
|
+
defaultBaseUrl: mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
|
|
356
|
+
allowPrivateNetwork: mode === "local" || explicitAllowPrivateNetwork,
|
|
357
|
+
defaultHeaders: mode === "cloud" ? {
|
|
358
|
+
"X-API-Key": resolvedAuth?.apiKey ?? "",
|
|
359
|
+
"Content-Type": "application/json"
|
|
360
|
+
} : { "Content-Type": "application/json" },
|
|
361
|
+
provider: "comfy",
|
|
362
|
+
capability: params.capability === "music" ? "audio" : params.capability,
|
|
363
|
+
transport: "http"
|
|
364
|
+
});
|
|
365
|
+
const normalizedBaseUrl = normalizeBaseUrl(baseUrl) || (mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL);
|
|
366
|
+
const networkPolicy = resolveComfyNetworkPolicy({
|
|
367
|
+
baseUrl: normalizedBaseUrl,
|
|
368
|
+
allowPrivateNetwork,
|
|
369
|
+
explicitAllowPrivateNetwork,
|
|
370
|
+
mode
|
|
371
|
+
});
|
|
372
|
+
if (params.inputImage) {
|
|
373
|
+
if (!inputImageNodeId) throw new Error("Comfy edit requests require plugins.entries.comfy.config.<capability>.inputImageNodeId to be configured");
|
|
374
|
+
setWorkflowInput({
|
|
375
|
+
workflow,
|
|
376
|
+
nodeId: inputImageNodeId,
|
|
377
|
+
inputName: inputImageInputName,
|
|
378
|
+
value: await uploadInputImage({
|
|
379
|
+
baseUrl: normalizedBaseUrl,
|
|
380
|
+
headers: new Headers(headers),
|
|
381
|
+
timeoutMs,
|
|
382
|
+
policy: networkPolicy.apiPolicy,
|
|
383
|
+
dispatcherPolicy,
|
|
384
|
+
image: params.inputImage,
|
|
385
|
+
mode,
|
|
386
|
+
capability: params.capability
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const submitPayload = {
|
|
391
|
+
prompt: workflow,
|
|
392
|
+
...mode === "cloud" && resolvedAuth?.apiKey ? { extra_data: { api_key_comfy_org: resolvedAuth.apiKey } } : {}
|
|
393
|
+
};
|
|
394
|
+
const promptId = normalizeOptionalString((await readJsonResponse({
|
|
395
|
+
url: `${normalizedBaseUrl}${mode === "cloud" ? "/api/prompt" : "/prompt"}`,
|
|
396
|
+
init: {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers,
|
|
399
|
+
body: JSON.stringify(submitPayload)
|
|
400
|
+
},
|
|
401
|
+
timeoutMs,
|
|
402
|
+
policy: networkPolicy.apiPolicy,
|
|
403
|
+
dispatcherPolicy,
|
|
404
|
+
auditContext: `comfy-${params.capability}-generate`,
|
|
405
|
+
errorPrefix: "Comfy workflow submit failed"
|
|
406
|
+
})).prompt_id);
|
|
407
|
+
if (!promptId) throw new Error("Comfy workflow submit response missing prompt_id");
|
|
408
|
+
const historyEntry = extractHistoryEntry(mode === "cloud" ? await (async () => {
|
|
409
|
+
await waitForCloudCompletion({
|
|
410
|
+
baseUrl: normalizedBaseUrl,
|
|
411
|
+
promptId,
|
|
412
|
+
headers: new Headers(headers),
|
|
413
|
+
timeoutMs,
|
|
414
|
+
pollIntervalMs,
|
|
415
|
+
policy: networkPolicy.apiPolicy,
|
|
416
|
+
dispatcherPolicy
|
|
417
|
+
});
|
|
418
|
+
return await readJsonResponse({
|
|
419
|
+
url: `${normalizedBaseUrl}/api/history_v2/${promptId}`,
|
|
420
|
+
init: {
|
|
421
|
+
method: "GET",
|
|
422
|
+
headers: new Headers(headers)
|
|
423
|
+
},
|
|
424
|
+
timeoutMs,
|
|
425
|
+
policy: networkPolicy.apiPolicy,
|
|
426
|
+
dispatcherPolicy,
|
|
427
|
+
auditContext: "comfy-history",
|
|
428
|
+
errorPrefix: "Comfy history lookup failed"
|
|
429
|
+
});
|
|
430
|
+
})() : await waitForLocalHistory({
|
|
431
|
+
baseUrl: normalizedBaseUrl,
|
|
432
|
+
promptId,
|
|
433
|
+
headers: new Headers(headers),
|
|
434
|
+
timeoutMs,
|
|
435
|
+
pollIntervalMs,
|
|
436
|
+
policy: networkPolicy.apiPolicy,
|
|
437
|
+
dispatcherPolicy
|
|
438
|
+
}), promptId);
|
|
439
|
+
if (!historyEntry) throw new Error(`Comfy history response missing outputs for prompt ${promptId}`);
|
|
440
|
+
const outputFiles = collectOutputFiles({
|
|
441
|
+
history: historyEntry,
|
|
442
|
+
outputNodeId,
|
|
443
|
+
outputKinds: params.outputKinds,
|
|
444
|
+
capability: params.capability
|
|
445
|
+
});
|
|
446
|
+
if (outputFiles.length === 0) throw new Error(`Comfy workflow ${promptId} completed without ${params.capability} outputs`);
|
|
447
|
+
const assets = [];
|
|
448
|
+
const outputKind = params.capability === "music" ? "audio" : params.capability;
|
|
449
|
+
const maxOutputBytes = resolveGeneratedMediaMaxBytes(params.cfg, outputKind);
|
|
450
|
+
let assetIndex = 0;
|
|
451
|
+
for (const output of outputFiles) {
|
|
452
|
+
const downloaded = await downloadOutputFile({
|
|
453
|
+
baseUrl: normalizedBaseUrl,
|
|
454
|
+
headers: new Headers(headers),
|
|
455
|
+
timeoutMs,
|
|
456
|
+
policy: networkPolicy.apiPolicy,
|
|
457
|
+
dispatcherPolicy,
|
|
458
|
+
file: output.file,
|
|
459
|
+
mode,
|
|
460
|
+
capability: params.capability,
|
|
461
|
+
maxBytes: maxOutputBytes
|
|
462
|
+
});
|
|
463
|
+
assetIndex += 1;
|
|
464
|
+
const originalName = normalizeOptionalString(output.file.filename) || normalizeOptionalString(output.file.name);
|
|
465
|
+
assets.push({
|
|
466
|
+
buffer: downloaded.buffer,
|
|
467
|
+
mimeType: downloaded.mimeType,
|
|
468
|
+
fileName: originalName || `${params.capability}-${assetIndex}.${resolveFileExtension({ mimeType: downloaded.mimeType })}`,
|
|
469
|
+
nodeId: output.nodeId
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
assets,
|
|
474
|
+
model: providerModel,
|
|
475
|
+
promptId,
|
|
476
|
+
outputNodeIds: uniqueStrings(outputFiles.map((entry) => entry.nodeId))
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
480
|
+
export { DEFAULT_COMFY_MODEL, isComfyCapabilityConfigured, runComfyWorkflow };
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "comfy",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"enabledByDefault": true,
|
|
7
|
+
"providers": ["comfy"],
|
|
8
|
+
"setup": {
|
|
9
|
+
"providers": [
|
|
10
|
+
{
|
|
11
|
+
"id": "comfy",
|
|
12
|
+
"envVars": ["COMFY_API_KEY", "COMFY_CLOUD_API_KEY"]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"providerAuthChoices": [
|
|
17
|
+
{
|
|
18
|
+
"provider": "comfy",
|
|
19
|
+
"method": "cloud-api-key",
|
|
20
|
+
"choiceId": "comfy-cloud-api-key",
|
|
21
|
+
"choiceLabel": "Comfy Cloud API key",
|
|
22
|
+
"choiceHint": "Required for cloud workflows",
|
|
23
|
+
"groupId": "comfy",
|
|
24
|
+
"groupLabel": "ComfyUI",
|
|
25
|
+
"groupHint": "Local or cloud workflows",
|
|
26
|
+
"optionKey": "comfyApiKey",
|
|
27
|
+
"cliFlag": "--comfy-api-key",
|
|
28
|
+
"cliOption": "--comfy-api-key <key>",
|
|
29
|
+
"cliDescription": "Comfy Cloud API key",
|
|
30
|
+
"onboardingScopes": ["image-generation"]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"contracts": {
|
|
34
|
+
"imageGenerationProviders": ["comfy"],
|
|
35
|
+
"musicGenerationProviders": ["comfy"],
|
|
36
|
+
"videoGenerationProviders": ["comfy"]
|
|
37
|
+
},
|
|
38
|
+
"imageGenerationProviderMetadata": {
|
|
39
|
+
"comfy": {
|
|
40
|
+
"configSignals": [
|
|
41
|
+
{
|
|
42
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
43
|
+
"overlayPath": "image",
|
|
44
|
+
"mode": {
|
|
45
|
+
"path": "mode",
|
|
46
|
+
"default": "local",
|
|
47
|
+
"allowed": ["local"]
|
|
48
|
+
},
|
|
49
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
50
|
+
"required": ["promptNodeId"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"rootPath": "models.providers.comfy",
|
|
54
|
+
"overlayPath": "image",
|
|
55
|
+
"mode": {
|
|
56
|
+
"path": "mode",
|
|
57
|
+
"default": "local",
|
|
58
|
+
"allowed": ["local"]
|
|
59
|
+
},
|
|
60
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
61
|
+
"required": ["promptNodeId"]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
65
|
+
"overlayPath": "image",
|
|
66
|
+
"mode": {
|
|
67
|
+
"path": "mode",
|
|
68
|
+
"allowed": ["cloud"]
|
|
69
|
+
},
|
|
70
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
71
|
+
"required": ["promptNodeId", "apiKey"]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"rootPath": "models.providers.comfy",
|
|
75
|
+
"overlayPath": "image",
|
|
76
|
+
"mode": {
|
|
77
|
+
"path": "mode",
|
|
78
|
+
"allowed": ["cloud"]
|
|
79
|
+
},
|
|
80
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
81
|
+
"required": ["promptNodeId", "apiKey"]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"musicGenerationProviderMetadata": {
|
|
87
|
+
"comfy": {
|
|
88
|
+
"configSignals": [
|
|
89
|
+
{
|
|
90
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
91
|
+
"overlayPath": "music",
|
|
92
|
+
"mode": {
|
|
93
|
+
"path": "mode",
|
|
94
|
+
"default": "local",
|
|
95
|
+
"allowed": ["local"]
|
|
96
|
+
},
|
|
97
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
98
|
+
"required": ["promptNodeId"]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"rootPath": "models.providers.comfy",
|
|
102
|
+
"overlayPath": "music",
|
|
103
|
+
"mode": {
|
|
104
|
+
"path": "mode",
|
|
105
|
+
"default": "local",
|
|
106
|
+
"allowed": ["local"]
|
|
107
|
+
},
|
|
108
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
109
|
+
"required": ["promptNodeId"]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
113
|
+
"overlayPath": "music",
|
|
114
|
+
"mode": {
|
|
115
|
+
"path": "mode",
|
|
116
|
+
"allowed": ["cloud"]
|
|
117
|
+
},
|
|
118
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
119
|
+
"required": ["promptNodeId", "apiKey"]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"rootPath": "models.providers.comfy",
|
|
123
|
+
"overlayPath": "music",
|
|
124
|
+
"mode": {
|
|
125
|
+
"path": "mode",
|
|
126
|
+
"allowed": ["cloud"]
|
|
127
|
+
},
|
|
128
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
129
|
+
"required": ["promptNodeId", "apiKey"]
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"videoGenerationProviderMetadata": {
|
|
135
|
+
"comfy": {
|
|
136
|
+
"configSignals": [
|
|
137
|
+
{
|
|
138
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
139
|
+
"overlayPath": "video",
|
|
140
|
+
"mode": {
|
|
141
|
+
"path": "mode",
|
|
142
|
+
"default": "local",
|
|
143
|
+
"allowed": ["local"]
|
|
144
|
+
},
|
|
145
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
146
|
+
"required": ["promptNodeId"]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"rootPath": "models.providers.comfy",
|
|
150
|
+
"overlayPath": "video",
|
|
151
|
+
"mode": {
|
|
152
|
+
"path": "mode",
|
|
153
|
+
"default": "local",
|
|
154
|
+
"allowed": ["local"]
|
|
155
|
+
},
|
|
156
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
157
|
+
"required": ["promptNodeId"]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"rootPath": "plugins.entries.comfy.config",
|
|
161
|
+
"overlayPath": "video",
|
|
162
|
+
"mode": {
|
|
163
|
+
"path": "mode",
|
|
164
|
+
"allowed": ["cloud"]
|
|
165
|
+
},
|
|
166
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
167
|
+
"required": ["promptNodeId", "apiKey"]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"rootPath": "models.providers.comfy",
|
|
171
|
+
"overlayPath": "video",
|
|
172
|
+
"mode": {
|
|
173
|
+
"path": "mode",
|
|
174
|
+
"allowed": ["cloud"]
|
|
175
|
+
},
|
|
176
|
+
"requiredAny": ["workflow", "workflowPath"],
|
|
177
|
+
"required": ["promptNodeId", "apiKey"]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"configSchema": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"additionalProperties": false,
|
|
185
|
+
"properties": {
|
|
186
|
+
"mode": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"enum": ["local", "cloud"]
|
|
189
|
+
},
|
|
190
|
+
"baseUrl": {
|
|
191
|
+
"type": "string"
|
|
192
|
+
},
|
|
193
|
+
"apiKey": {
|
|
194
|
+
"type": ["string", "object"]
|
|
195
|
+
},
|
|
196
|
+
"allowPrivateNetwork": {
|
|
197
|
+
"type": "boolean"
|
|
198
|
+
},
|
|
199
|
+
"workflowPath": {
|
|
200
|
+
"type": "string"
|
|
201
|
+
},
|
|
202
|
+
"workflow": {
|
|
203
|
+
"type": "object"
|
|
204
|
+
},
|
|
205
|
+
"promptNodeId": {
|
|
206
|
+
"type": "string"
|
|
207
|
+
},
|
|
208
|
+
"promptInputName": {
|
|
209
|
+
"type": "string"
|
|
210
|
+
},
|
|
211
|
+
"inputImageNodeId": {
|
|
212
|
+
"type": "string"
|
|
213
|
+
},
|
|
214
|
+
"inputImageInputName": {
|
|
215
|
+
"type": "string"
|
|
216
|
+
},
|
|
217
|
+
"outputNodeId": {
|
|
218
|
+
"type": "string"
|
|
219
|
+
},
|
|
220
|
+
"pollIntervalMs": {
|
|
221
|
+
"type": "integer",
|
|
222
|
+
"minimum": 100
|
|
223
|
+
},
|
|
224
|
+
"timeoutMs": {
|
|
225
|
+
"type": "integer",
|
|
226
|
+
"minimum": 1000
|
|
227
|
+
},
|
|
228
|
+
"image": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"additionalProperties": false,
|
|
231
|
+
"properties": {
|
|
232
|
+
"workflowPath": { "type": "string" },
|
|
233
|
+
"workflow": { "type": "object" },
|
|
234
|
+
"promptNodeId": { "type": "string" },
|
|
235
|
+
"promptInputName": { "type": "string" },
|
|
236
|
+
"inputImageNodeId": { "type": "string" },
|
|
237
|
+
"inputImageInputName": { "type": "string" },
|
|
238
|
+
"outputNodeId": { "type": "string" },
|
|
239
|
+
"pollIntervalMs": { "type": "integer", "minimum": 100 },
|
|
240
|
+
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
"video": {
|
|
244
|
+
"type": "object",
|
|
245
|
+
"additionalProperties": false,
|
|
246
|
+
"properties": {
|
|
247
|
+
"workflowPath": { "type": "string" },
|
|
248
|
+
"workflow": { "type": "object" },
|
|
249
|
+
"promptNodeId": { "type": "string" },
|
|
250
|
+
"promptInputName": { "type": "string" },
|
|
251
|
+
"inputImageNodeId": { "type": "string" },
|
|
252
|
+
"inputImageInputName": { "type": "string" },
|
|
253
|
+
"outputNodeId": { "type": "string" },
|
|
254
|
+
"pollIntervalMs": { "type": "integer", "minimum": 100 },
|
|
255
|
+
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"music": {
|
|
259
|
+
"type": "object",
|
|
260
|
+
"additionalProperties": false,
|
|
261
|
+
"properties": {
|
|
262
|
+
"workflowPath": { "type": "string" },
|
|
263
|
+
"workflow": { "type": "object" },
|
|
264
|
+
"promptNodeId": { "type": "string" },
|
|
265
|
+
"promptInputName": { "type": "string" },
|
|
266
|
+
"outputNodeId": { "type": "string" },
|
|
267
|
+
"pollIntervalMs": { "type": "integer", "minimum": 100 },
|
|
268
|
+
"timeoutMs": { "type": "integer", "minimum": 1000 }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/comfy-provider",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "2026.8.1-beta.2",
|
|
4
|
+
"description": "OpenClaw ComfyUI provider plugin.",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/openclaw/openclaw"
|
|
9
8
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
9
|
+
"type": "module",
|
|
10
|
+
"openclaw": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./index.ts"
|
|
13
|
+
],
|
|
14
|
+
"install": {
|
|
15
|
+
"clawhubSpec": "clawhub:@openclaw/comfy-provider",
|
|
16
|
+
"npmSpec": "@openclaw/comfy-provider",
|
|
17
|
+
"defaultChoice": "npm",
|
|
18
|
+
"minHostVersion": ">=2026.7.2"
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.8.1-beta.2"
|
|
22
|
+
},
|
|
23
|
+
"build": {
|
|
24
|
+
"openclawVersion": "2026.8.1-beta.2",
|
|
25
|
+
"bundledDist": false
|
|
26
|
+
},
|
|
27
|
+
"release": {
|
|
28
|
+
"publishToClawHub": true,
|
|
29
|
+
"publishToNpm": true
|
|
30
|
+
},
|
|
31
|
+
"runtimeExtensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**",
|
|
37
|
+
"openclaw.plugin.json",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"openclaw": ">=2026.8.1-beta.2"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"openclaw": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"bundledDependencies": []
|
|
14
49
|
}
|