n8n-nodes-comfyui-all 2.2.9 → 2.2.11
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/LICENSE +1 -1
- package/README.md +126 -11
- package/dist/agent-tools/ComfyUIAgentTool.d.ts +62 -0
- package/dist/agent-tools/ComfyUIAgentTool.d.ts.map +1 -0
- package/dist/agent-tools/ComfyUIAgentTool.js +440 -0
- package/dist/agent-tools/ComfyUIAgentTool.js.map +1 -0
- package/dist/nodes/AxiosAdapter.d.ts +5 -0
- package/dist/nodes/AxiosAdapter.d.ts.map +1 -0
- package/dist/nodes/AxiosAdapter.js +31 -0
- package/dist/nodes/AxiosAdapter.js.map +1 -0
- package/dist/nodes/ComfyUi/ComfyUi.node.d.ts +30 -0
- package/dist/nodes/ComfyUi/ComfyUi.node.d.ts.map +1 -1
- package/dist/nodes/ComfyUi/ComfyUi.node.js +47 -251
- package/dist/nodes/ComfyUi/ComfyUi.node.js.map +1 -1
- package/dist/nodes/ComfyUiClient.d.ts +116 -3
- package/dist/nodes/ComfyUiClient.d.ts.map +1 -1
- package/dist/nodes/ComfyUiClient.js +374 -98
- package/dist/nodes/ComfyUiClient.js.map +1 -1
- package/dist/nodes/HttpClient.d.ts +34 -0
- package/dist/nodes/HttpClient.d.ts.map +1 -0
- package/dist/nodes/HttpClient.js +85 -0
- package/dist/nodes/HttpClient.js.map +1 -0
- package/dist/nodes/N8nHelpersAdapter.d.ts +8 -0
- package/dist/nodes/N8nHelpersAdapter.d.ts.map +1 -0
- package/dist/nodes/N8nHelpersAdapter.js +23 -0
- package/dist/nodes/N8nHelpersAdapter.js.map +1 -0
- package/dist/nodes/constants.d.ts +14 -3
- package/dist/nodes/constants.d.ts.map +1 -1
- package/dist/nodes/constants.js +20 -14
- package/dist/nodes/constants.js.map +1 -1
- package/dist/nodes/errors.d.ts +35 -0
- package/dist/nodes/errors.d.ts.map +1 -0
- package/dist/nodes/errors.js +111 -0
- package/dist/nodes/errors.js.map +1 -0
- package/dist/nodes/logger.d.ts +9 -17
- package/dist/nodes/logger.d.ts.map +1 -1
- package/dist/nodes/logger.js +72 -23
- package/dist/nodes/logger.js.map +1 -1
- package/dist/nodes/parameterProcessor.d.ts +22 -0
- package/dist/nodes/parameterProcessor.d.ts.map +1 -0
- package/dist/nodes/parameterProcessor.js +268 -0
- package/dist/nodes/parameterProcessor.js.map +1 -0
- package/dist/nodes/types.d.ts +129 -0
- package/dist/nodes/types.d.ts.map +1 -1
- package/dist/nodes/utils.d.ts +65 -0
- package/dist/nodes/utils.d.ts.map +1 -0
- package/dist/nodes/utils.js +172 -0
- package/dist/nodes/utils.js.map +1 -0
- package/dist/nodes/validation.d.ts +8 -0
- package/dist/nodes/validation.d.ts.map +1 -1
- package/dist/nodes/validation.js +129 -7
- package/dist/nodes/validation.js.map +1 -1
- package/dist/nodes/workflowConfig.d.ts +13 -0
- package/dist/nodes/workflowConfig.d.ts.map +1 -0
- package/dist/nodes/workflowConfig.js +91 -0
- package/dist/nodes/workflowConfig.js.map +1 -0
- package/package.json +18 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -131,22 +131,74 @@ By default, the first output image/video uses `data` as the binary property name
|
|
|
131
131
|
|
|
132
132
|
## 🤖 AI Agent Integration
|
|
133
133
|
|
|
134
|
-
Use ComfyUI as a tool in AI Agent workflows
|
|
134
|
+
Use ComfyUI as a tool in AI Agent workflows.
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
2. In the **Tools** section, add **ComfyUI**
|
|
138
|
-
3. Configure the ComfyUI node parameters
|
|
139
|
-
4. Start chatting!
|
|
136
|
+
### Quick Setup
|
|
140
137
|
|
|
141
|
-
**
|
|
138
|
+
**Step 1: Create AI Agent**
|
|
139
|
+
1. Add **OpenAI Conversational Agent** node
|
|
140
|
+
2. Configure Chat Model (GPT-4/GPT-3.5)
|
|
141
|
+
3. Add Memory (optional but recommended)
|
|
142
|
+
|
|
143
|
+
**Step 2: Add ComfyUI Tool**
|
|
144
|
+
1. Click AI Agent node
|
|
145
|
+
2. In **Tools** section, click **+ Add Tool**
|
|
146
|
+
3. Search and select **ComfyUI**
|
|
147
|
+
4. Configure:
|
|
148
|
+
- **ComfyUI URL**: `http://127.0.0.1:8188`
|
|
149
|
+
- **Workflow JSON**: Your ComfyUI workflow (API format)
|
|
150
|
+
- **Node Parameters**: Configure text parameter override
|
|
151
|
+
|
|
152
|
+
**Step 3: Start Chatting**
|
|
153
|
+
Execute workflow and start conversing!
|
|
154
|
+
|
|
155
|
+
### Example Conversations
|
|
156
|
+
|
|
157
|
+
**Basic Image Generation:**
|
|
158
|
+
|
|
159
|
+
**User:**
|
|
160
|
+
```
|
|
161
|
+
Generate a picture of a cute cat sitting on a fence
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**AI:**
|
|
165
|
+
```
|
|
166
|
+
I'll generate that for you.
|
|
167
|
+
[Executes ComfyUI with prompt: "a cute cat sitting on a fence"]
|
|
168
|
+
Done! Here's the image.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**With Parameters:**
|
|
172
|
+
|
|
173
|
+
**User:**
|
|
174
|
+
```
|
|
175
|
+
Create a cyberpunk city, size:1024x768, steps:30
|
|
142
176
|
```
|
|
143
|
-
User: Generate a cute cat picture
|
|
144
177
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
**Supported Parameters:**
|
|
179
|
+
- `size:WIDTHxHEIGHT` - Image dimensions
|
|
180
|
+
- `steps:N` - Sampling steps
|
|
181
|
+
- `cfg:N` - CFG strength
|
|
182
|
+
- `seed:N` - Random seed
|
|
183
|
+
- `negative:TEXT` - Negative prompt
|
|
184
|
+
|
|
185
|
+
**Negative Prompts:**
|
|
186
|
+
|
|
187
|
+
**User:**
|
|
188
|
+
```
|
|
189
|
+
Draw a beautiful sunset, negative: blurry, low quality
|
|
148
190
|
```
|
|
149
191
|
|
|
192
|
+
### Configuring Node Parameters for AI Agent
|
|
193
|
+
|
|
194
|
+
In the ComfyUI node, configure parameter overrides:
|
|
195
|
+
|
|
196
|
+
- **Node ID**: `6` (your CLIP text node)
|
|
197
|
+
- **Parameter Mode**: Single Parameter
|
|
198
|
+
- **Parameter Name**: `text`
|
|
199
|
+
- **Type**: Text
|
|
200
|
+
- **Value**: [Leave empty - AI Agent will fill this]
|
|
201
|
+
|
|
150
202
|
### Tool Mode Output Format
|
|
151
203
|
|
|
152
204
|
When used as an AI Agent tool, ComfyUI returns both binary data and URL information:
|
|
@@ -167,7 +219,70 @@ When used as an AI Agent tool, ComfyUI returns both binary data and URL informat
|
|
|
167
219
|
|
|
168
220
|
This ensures compatibility with both workflow mode (binary data) and tool mode (URLs for AI Agent).
|
|
169
221
|
|
|
170
|
-
|
|
222
|
+
### Best Practices
|
|
223
|
+
|
|
224
|
+
**1. Clear Prompts**
|
|
225
|
+
|
|
226
|
+
**Good:** "Generate a landscape painting of mountains at sunset"
|
|
227
|
+
|
|
228
|
+
**Bad:** "Make a picture"
|
|
229
|
+
|
|
230
|
+
**2. Use Specific Parameters**
|
|
231
|
+
|
|
232
|
+
**Good:** "Create a portrait, size:512x768, steps:25"
|
|
233
|
+
|
|
234
|
+
**Bad:** "Create a high-quality portrait"
|
|
235
|
+
|
|
236
|
+
**3. Negative Prompts**
|
|
237
|
+
|
|
238
|
+
Always use negative prompts for better quality:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
A beautiful landscape, negative: blurry, distorted, low quality
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Advanced Usage
|
|
245
|
+
|
|
246
|
+
**Style Transfer:**
|
|
247
|
+
|
|
248
|
+
**User:**
|
|
249
|
+
```
|
|
250
|
+
Transform this image to oil painting style
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Setup:
|
|
254
|
+
- Upload input image as binary data
|
|
255
|
+
- Configure style parameters
|
|
256
|
+
|
|
257
|
+
**Multi-Modal Workflows:**
|
|
258
|
+
|
|
259
|
+
Combine ComfyUI with other tools:
|
|
260
|
+
|
|
261
|
+
**User:**
|
|
262
|
+
```
|
|
263
|
+
Generate an image of a futuristic city, then write a poem about it
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
AI Agent:
|
|
267
|
+
1. Calls ComfyUI to generate image
|
|
268
|
+
2. Calls Chat Model to write poem
|
|
269
|
+
3. Returns both results
|
|
270
|
+
|
|
271
|
+
### Troubleshooting
|
|
272
|
+
|
|
273
|
+
**AI Agent Doesn't Call ComfyUI**
|
|
274
|
+
|
|
275
|
+
**Check:**
|
|
276
|
+
1. ComfyUI tool added to Tools list?
|
|
277
|
+
2. Workflow JSON configured?
|
|
278
|
+
3. ComfyUI server running?
|
|
279
|
+
|
|
280
|
+
**Images Don't Match Prompts**
|
|
281
|
+
|
|
282
|
+
**Check:**
|
|
283
|
+
1. Correct node ID (e.g., `6` for CLIP text)
|
|
284
|
+
2. Parameter name matches workflow (`text`)
|
|
285
|
+
3. Workflow uses dynamic text input
|
|
171
286
|
|
|
172
287
|
## 🔧 Configuration Reference
|
|
173
288
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComfyUI Agent Tool - Custom Code Tool for n8n AI Agent
|
|
3
|
+
*
|
|
4
|
+
* This tool allows AI Agents to directly call ComfyUI API to generate images
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* 1. Create a "Custom Code Tool" node in n8n
|
|
8
|
+
* 2. Copy this code into the JavaScript code box
|
|
9
|
+
* 3. Fill in the Description: Generates images using ComfyUI. Use this tool when the user asks to create, generate, or make images.
|
|
10
|
+
* 4. Add this tool to the tools list of the AI Agent node
|
|
11
|
+
*/
|
|
12
|
+
import { Workflow, ImageInfo, ParsedParameters, ToolInputOptions, ToolResult } from '../nodes/types';
|
|
13
|
+
/**
|
|
14
|
+
* Cancel any ongoing ComfyUI workflow execution
|
|
15
|
+
* @returns {boolean} True if a request was cancelled, false if no request was in progress
|
|
16
|
+
*/
|
|
17
|
+
export declare function cancelComfyUIExecution(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Parse user input to extract image generation parameters
|
|
20
|
+
* @param query - User query text
|
|
21
|
+
* @returns Parsed parameters object
|
|
22
|
+
*/
|
|
23
|
+
declare function parseInput(query: string): ParsedParameters;
|
|
24
|
+
/**
|
|
25
|
+
* Find node ID by node type
|
|
26
|
+
* @param workflow - Workflow object
|
|
27
|
+
* @param classType - Node type
|
|
28
|
+
* @returns Node ID or null
|
|
29
|
+
*/
|
|
30
|
+
declare function findNodeByClassType(workflow: Workflow, classType: string): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Update workflow parameters
|
|
33
|
+
* @param workflow - ComfyUI workflow object
|
|
34
|
+
* @param params - Parameters object
|
|
35
|
+
* @returns Updated workflow
|
|
36
|
+
*/
|
|
37
|
+
declare function updateWorkflow(workflow: Workflow, params: ParsedParameters): Workflow;
|
|
38
|
+
/**
|
|
39
|
+
* Extract image information from output nodes
|
|
40
|
+
* @param outputs - ComfyUI output object
|
|
41
|
+
* @param url - ComfyUI server URL
|
|
42
|
+
* @returns Array of image information
|
|
43
|
+
*/
|
|
44
|
+
declare function extractImagesFromOutputs(outputs: Record<string, unknown>, url: string): ImageInfo[];
|
|
45
|
+
/**
|
|
46
|
+
* Process single image object
|
|
47
|
+
* @param image - Image object
|
|
48
|
+
* @param nodeId - Node ID
|
|
49
|
+
* @param url - ComfyUI server URL
|
|
50
|
+
* @returns Processed image information or null
|
|
51
|
+
*/
|
|
52
|
+
declare function processImage(image: unknown, nodeId: string, url: string): ImageInfo | null;
|
|
53
|
+
/**
|
|
54
|
+
* Main function: Handle tool invocation
|
|
55
|
+
* @param query - User query text
|
|
56
|
+
* @param options - Optional configuration parameters
|
|
57
|
+
* @param options.comfyUiUrl - ComfyUI server URL
|
|
58
|
+
* @param options.workflowConfig - Workflow configuration options
|
|
59
|
+
*/
|
|
60
|
+
export declare function handleToolInput(query: string, options?: ToolInputOptions): Promise<ToolResult>;
|
|
61
|
+
export { parseInput, updateWorkflow, findNodeByClassType, extractImagesFromOutputs, processImage };
|
|
62
|
+
//# sourceMappingURL=ComfyUIAgentTool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComfyUIAgentTool.d.ts","sourceRoot":"","sources":["../../agent-tools/ComfyUIAgentTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAGL,QAAQ,EACR,SAAS,EACT,gBAAgB,EAEhB,gBAAgB,EAChB,UAAU,EAGX,MAAM,gBAAgB,CAAC;AA0BxB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAQhD;AA2ED;;;;GAIG;AACH,iBAAS,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CA4CnD;AAED;;;;;GAKG;AACH,iBAAS,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOjF;AAED;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,GAAG,QAAQ,CAyC9E;AAED;;;;;GAKG;AACH,iBAAS,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,CA2B5F;AAED;;;;;;GAMG;AACH,iBAAS,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsBnF;AA0ID;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4CxG;AAaD,OAAO,EACL,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACb,CAAC"}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ComfyUI Agent Tool - Custom Code Tool for n8n AI Agent
|
|
4
|
+
*
|
|
5
|
+
* This tool allows AI Agents to directly call ComfyUI API to generate images
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* 1. Create a "Custom Code Tool" node in n8n
|
|
9
|
+
* 2. Copy this code into the JavaScript code box
|
|
10
|
+
* 3. Fill in the Description: Generates images using ComfyUI. Use this tool when the user asks to create, generate, or make images.
|
|
11
|
+
* 4. Add this tool to the tools list of the AI Agent node
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.cancelComfyUIExecution = cancelComfyUIExecution;
|
|
15
|
+
exports.handleToolInput = handleToolInput;
|
|
16
|
+
exports.parseInput = parseInput;
|
|
17
|
+
exports.updateWorkflow = updateWorkflow;
|
|
18
|
+
exports.findNodeByClassType = findNodeByClassType;
|
|
19
|
+
exports.extractImagesFromOutputs = extractImagesFromOutputs;
|
|
20
|
+
exports.processImage = processImage;
|
|
21
|
+
const crypto_1 = require("crypto");
|
|
22
|
+
const workflowConfig_1 = require("../nodes/workflowConfig");
|
|
23
|
+
const HttpClient_1 = require("../nodes/HttpClient");
|
|
24
|
+
const AxiosAdapter_1 = require("../nodes/AxiosAdapter");
|
|
25
|
+
const logger_1 = require("../nodes/logger");
|
|
26
|
+
// Default configuration
|
|
27
|
+
const DEFAULT_COMFYUI_URL = 'http://127.0.0.1:8188';
|
|
28
|
+
// Request timeout configuration
|
|
29
|
+
const QUEUE_REQUEST_TIMEOUT = 30000; // Queue request timeout 30 seconds
|
|
30
|
+
const HISTORY_REQUEST_TIMEOUT = 10000; // History request timeout 10 seconds
|
|
31
|
+
// Create logger instance
|
|
32
|
+
const logger = new logger_1.Logger(undefined, ['ComfyUI Tool']);
|
|
33
|
+
// Create HTTP client instance
|
|
34
|
+
const httpClient = new HttpClient_1.HttpClient({
|
|
35
|
+
adapter: new AxiosAdapter_1.AxiosAdapter(),
|
|
36
|
+
logger,
|
|
37
|
+
defaultTimeout: QUEUE_REQUEST_TIMEOUT,
|
|
38
|
+
});
|
|
39
|
+
// Global AbortController for cancelling requests
|
|
40
|
+
let currentAbortController = null;
|
|
41
|
+
/**
|
|
42
|
+
* Cancel any ongoing ComfyUI workflow execution
|
|
43
|
+
* @returns {boolean} True if a request was cancelled, false if no request was in progress
|
|
44
|
+
*/
|
|
45
|
+
function cancelComfyUIExecution() {
|
|
46
|
+
if (currentAbortController) {
|
|
47
|
+
currentAbortController.abort();
|
|
48
|
+
currentAbortController = null;
|
|
49
|
+
logger.info('ComfyUI workflow execution cancelled');
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Parameter default values
|
|
55
|
+
const DEFAULT_NEGATIVE_PROMPT = 'ugly, blurry, low quality, distorted';
|
|
56
|
+
const DEFAULT_WIDTH = 512;
|
|
57
|
+
const DEFAULT_HEIGHT = 512;
|
|
58
|
+
const DEFAULT_STEPS = 20;
|
|
59
|
+
const DEFAULT_CFG = 8;
|
|
60
|
+
// Maximum random seed value (32-bit signed integer max value)
|
|
61
|
+
const MAX_SEED_VALUE = 2147483647;
|
|
62
|
+
// Polling configuration
|
|
63
|
+
const POLLING_MAX_ATTEMPTS = 120; // Maximum wait time of 2 minutes (120 seconds)
|
|
64
|
+
const POLLING_INTERVAL_MS = 1000; // Polling interval of 1 second
|
|
65
|
+
/**
|
|
66
|
+
* Promise-based delay function
|
|
67
|
+
* @param ms - Delay time in milliseconds
|
|
68
|
+
* @returns Promise object
|
|
69
|
+
*/
|
|
70
|
+
function delay(ms) {
|
|
71
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
73
|
+
// Parameter extraction rules configuration
|
|
74
|
+
const PARAM_PATTERNS = {
|
|
75
|
+
negative: {
|
|
76
|
+
regex: /(?<=\s|,|,|^)negative:\s*([^\n]+)/i,
|
|
77
|
+
paramKey: 'negative_prompt',
|
|
78
|
+
parser: (match) => match[1].trim()
|
|
79
|
+
},
|
|
80
|
+
size: {
|
|
81
|
+
regex: /(?<=\s|,|,|^)size:\s*(\d+)x(\d+)/i,
|
|
82
|
+
paramKeys: ['width', 'height'],
|
|
83
|
+
parser: (match) => ({
|
|
84
|
+
width: parseInt(match[1]),
|
|
85
|
+
height: parseInt(match[2])
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
steps: {
|
|
89
|
+
regex: /(?<=\s|,|,|^)steps:\s*(\d+)/i,
|
|
90
|
+
paramKey: 'steps',
|
|
91
|
+
parser: (match) => parseInt(match[1])
|
|
92
|
+
},
|
|
93
|
+
cfg: {
|
|
94
|
+
regex: /(?<=\s|,|,|^)cfg:\s*([\d.]+)/i,
|
|
95
|
+
paramKey: 'cfg',
|
|
96
|
+
parser: (match) => parseFloat(match[1])
|
|
97
|
+
},
|
|
98
|
+
seed: {
|
|
99
|
+
regex: /(?<=\s|,|,|^)seed:\s*(\d+)/i,
|
|
100
|
+
paramKey: 'seed',
|
|
101
|
+
parser: (match) => parseInt(match[1])
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Extract parameter from query
|
|
106
|
+
* @param query - User query text
|
|
107
|
+
* @param pattern - Parameter pattern configuration
|
|
108
|
+
* @returns Extracted parameter and cleaned query
|
|
109
|
+
*/
|
|
110
|
+
function extractParameter(query, pattern) {
|
|
111
|
+
const match = query.match(pattern.regex);
|
|
112
|
+
if (!match) {
|
|
113
|
+
return { value: null, cleanedQuery: query };
|
|
114
|
+
}
|
|
115
|
+
const value = pattern.parser(match);
|
|
116
|
+
const cleanedQuery = query.replace(pattern.regex, '').trim();
|
|
117
|
+
return { value, cleanedQuery };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Parse user input to extract image generation parameters
|
|
121
|
+
* @param query - User query text
|
|
122
|
+
* @returns Parsed parameters object
|
|
123
|
+
*/
|
|
124
|
+
function parseInput(query) {
|
|
125
|
+
const params = {
|
|
126
|
+
prompt: '',
|
|
127
|
+
negative_prompt: DEFAULT_NEGATIVE_PROMPT,
|
|
128
|
+
width: DEFAULT_WIDTH,
|
|
129
|
+
height: DEFAULT_HEIGHT,
|
|
130
|
+
steps: DEFAULT_STEPS,
|
|
131
|
+
cfg: DEFAULT_CFG,
|
|
132
|
+
seed: (0, crypto_1.randomInt)(0, MAX_SEED_VALUE)
|
|
133
|
+
};
|
|
134
|
+
let currentQuery = query.trim();
|
|
135
|
+
for (const [, pattern] of Object.entries(PARAM_PATTERNS)) {
|
|
136
|
+
const { value, cleanedQuery } = extractParameter(currentQuery, pattern);
|
|
137
|
+
if (value !== null) {
|
|
138
|
+
if (pattern.paramKeys) {
|
|
139
|
+
// Type guard: check if value is an object with keys
|
|
140
|
+
if (typeof value === 'object' && value !== null) {
|
|
141
|
+
const valueRecord = value;
|
|
142
|
+
const valueKeys = Object.keys(valueRecord);
|
|
143
|
+
pattern.paramKeys.forEach((key, index) => {
|
|
144
|
+
params[key] = valueRecord[valueKeys[index]];
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (pattern.paramKey) {
|
|
149
|
+
params[pattern.paramKey] = value;
|
|
150
|
+
}
|
|
151
|
+
currentQuery = cleanedQuery;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Clean up remaining parameter markers and extra punctuation
|
|
155
|
+
currentQuery = currentQuery
|
|
156
|
+
.replace(/,\s*,/g, ',') // Remove consecutive Chinese commas
|
|
157
|
+
.replace(/,\s*,/g, ',') // Remove consecutive English commas
|
|
158
|
+
.replace(/[,,]\s*[,,]/g, ' ') // Remove extra spaces between commas
|
|
159
|
+
.replace(/^\s*[,,]+/g, '') // Remove all leading commas
|
|
160
|
+
.replace(/[,,]+\s*$/g, '') // Remove all trailing commas
|
|
161
|
+
.trim();
|
|
162
|
+
params.prompt = currentQuery;
|
|
163
|
+
return params;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Find node ID by node type
|
|
167
|
+
* @param workflow - Workflow object
|
|
168
|
+
* @param classType - Node type
|
|
169
|
+
* @returns Node ID or null
|
|
170
|
+
*/
|
|
171
|
+
function findNodeByClassType(workflow, classType) {
|
|
172
|
+
for (const nodeId in workflow) {
|
|
173
|
+
if (workflow[nodeId] && workflow[nodeId].class_type === classType) {
|
|
174
|
+
return nodeId;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Update workflow parameters
|
|
181
|
+
* @param workflow - ComfyUI workflow object
|
|
182
|
+
* @param params - Parameters object
|
|
183
|
+
* @returns Updated workflow
|
|
184
|
+
*/
|
|
185
|
+
function updateWorkflow(workflow, params) {
|
|
186
|
+
const updatedWorkflow = JSON.parse(JSON.stringify(workflow));
|
|
187
|
+
// Find and update positive and negative prompt nodes
|
|
188
|
+
const clipTextEncodeNodes = [];
|
|
189
|
+
for (const nodeId in updatedWorkflow) {
|
|
190
|
+
if (updatedWorkflow[nodeId] && updatedWorkflow[nodeId].class_type === 'CLIPTextEncode') {
|
|
191
|
+
clipTextEncodeNodes.push(nodeId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (clipTextEncodeNodes.length >= 1) {
|
|
195
|
+
updatedWorkflow[clipTextEncodeNodes[0]].inputs.text = params.prompt;
|
|
196
|
+
}
|
|
197
|
+
if (clipTextEncodeNodes.length >= 2) {
|
|
198
|
+
updatedWorkflow[clipTextEncodeNodes[1]].inputs.text = params.negative_prompt;
|
|
199
|
+
}
|
|
200
|
+
// Find and update image size node
|
|
201
|
+
const latentNodeId = findNodeByClassType(updatedWorkflow, 'EmptyLatentImage') ||
|
|
202
|
+
findNodeByClassType(updatedWorkflow, 'EmptySD3LatentImage');
|
|
203
|
+
if (latentNodeId && updatedWorkflow[latentNodeId].inputs) {
|
|
204
|
+
updatedWorkflow[latentNodeId].inputs.width = params.width;
|
|
205
|
+
updatedWorkflow[latentNodeId].inputs.height = params.height;
|
|
206
|
+
}
|
|
207
|
+
// Find and update sampling parameters node
|
|
208
|
+
const samplerNodeId = findNodeByClassType(updatedWorkflow, 'KSampler');
|
|
209
|
+
if (samplerNodeId && updatedWorkflow[samplerNodeId].inputs) {
|
|
210
|
+
updatedWorkflow[samplerNodeId].inputs.steps = params.steps;
|
|
211
|
+
updatedWorkflow[samplerNodeId].inputs.cfg = params.cfg;
|
|
212
|
+
updatedWorkflow[samplerNodeId].inputs.seed = params.seed;
|
|
213
|
+
}
|
|
214
|
+
// Find and update edit instruction node (for image editing workflows)
|
|
215
|
+
const primitiveNodeId = findNodeByClassType(updatedWorkflow, 'PrimitiveStringMultiline');
|
|
216
|
+
if (primitiveNodeId && updatedWorkflow[primitiveNodeId].inputs) {
|
|
217
|
+
updatedWorkflow[primitiveNodeId].inputs.value = params.prompt;
|
|
218
|
+
}
|
|
219
|
+
return updatedWorkflow;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract image information from output nodes
|
|
223
|
+
* @param outputs - ComfyUI output object
|
|
224
|
+
* @param url - ComfyUI server URL
|
|
225
|
+
* @returns Array of image information
|
|
226
|
+
*/
|
|
227
|
+
function extractImagesFromOutputs(outputs, url) {
|
|
228
|
+
const images = [];
|
|
229
|
+
for (const nodeId in outputs) {
|
|
230
|
+
const nodeOutput = outputs[nodeId];
|
|
231
|
+
if (!nodeOutput || typeof nodeOutput !== 'object') {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const nodeImages = nodeOutput.images;
|
|
235
|
+
if (!nodeImages) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!Array.isArray(nodeImages) || nodeImages.length === 0) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const image of nodeImages) {
|
|
242
|
+
const imageInfo = processImage(image, nodeId, url);
|
|
243
|
+
if (imageInfo) {
|
|
244
|
+
images.push(imageInfo);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return images;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Process single image object
|
|
252
|
+
* @param image - Image object
|
|
253
|
+
* @param nodeId - Node ID
|
|
254
|
+
* @param url - ComfyUI server URL
|
|
255
|
+
* @returns Processed image information or null
|
|
256
|
+
*/
|
|
257
|
+
function processImage(image, nodeId, url) {
|
|
258
|
+
if (!image || typeof image !== 'object') {
|
|
259
|
+
logger.warn(`Invalid image object in node ${nodeId}`);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const imageObj = image;
|
|
263
|
+
const filename = imageObj.filename;
|
|
264
|
+
const subfolder = imageObj.subfolder || '';
|
|
265
|
+
const type = imageObj.type || 'output';
|
|
266
|
+
if (!filename) {
|
|
267
|
+
logger.warn(`Image missing filename in node ${nodeId}`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
filename: filename,
|
|
272
|
+
subfolder: subfolder,
|
|
273
|
+
type: type,
|
|
274
|
+
url: `${url}/view?filename=${filename}&subfolder=${subfolder}&type=${type}`
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Execute ComfyUI workflow
|
|
279
|
+
* @param workflow - ComfyUI workflow object
|
|
280
|
+
* @param comfyUiUrl - ComfyUI server URL
|
|
281
|
+
* @param signal - Optional AbortSignal for cancelling the request
|
|
282
|
+
*/
|
|
283
|
+
async function executeComfyUIWorkflow(workflow, comfyUiUrl, signal) {
|
|
284
|
+
const url = comfyUiUrl || DEFAULT_COMFYUI_URL;
|
|
285
|
+
// Create new AbortController for this execution if not provided
|
|
286
|
+
const abortController = signal ? null : new AbortController();
|
|
287
|
+
const effectiveSignal = signal || abortController?.signal;
|
|
288
|
+
if (abortController) {
|
|
289
|
+
currentAbortController = abortController;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
// Check if already aborted
|
|
293
|
+
if (effectiveSignal?.aborted) {
|
|
294
|
+
throw new Error('Workflow execution was cancelled before starting');
|
|
295
|
+
}
|
|
296
|
+
// 1. Queue prompt
|
|
297
|
+
let promptResponse;
|
|
298
|
+
try {
|
|
299
|
+
promptResponse = await httpClient.post(`${url}/prompt`, {
|
|
300
|
+
prompt: workflow
|
|
301
|
+
}, {
|
|
302
|
+
timeout: QUEUE_REQUEST_TIMEOUT,
|
|
303
|
+
abortSignal: effectiveSignal,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error instanceof Error) {
|
|
308
|
+
if (error.name === 'CanceledError' || effectiveSignal?.aborted) {
|
|
309
|
+
throw new Error('Workflow execution was cancelled');
|
|
310
|
+
}
|
|
311
|
+
const httpError = error;
|
|
312
|
+
const errorCode = httpError.code;
|
|
313
|
+
if (errorCode === 'ECONNREFUSED') {
|
|
314
|
+
throw new Error(`Failed to connect to ComfyUI server at ${url}. Please check if the server is running.`);
|
|
315
|
+
}
|
|
316
|
+
else if (errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED') {
|
|
317
|
+
throw new Error(`Connection timeout while connecting to ComfyUI server at ${url}.`);
|
|
318
|
+
}
|
|
319
|
+
else if (httpError.response) {
|
|
320
|
+
throw new Error(`ComfyUI server returned error: ${httpError.response.statusCode} ${httpError.response.statusMessage}`);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
throw new Error(`Failed to queue workflow: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw new Error(`Failed to queue workflow: ${String(error)}`);
|
|
327
|
+
}
|
|
328
|
+
if (!promptResponse || !promptResponse.prompt_id) {
|
|
329
|
+
throw new Error('Invalid response from ComfyUI server: missing prompt_id');
|
|
330
|
+
}
|
|
331
|
+
const promptId = promptResponse.prompt_id;
|
|
332
|
+
logger.info(`Workflow queued with ID: ${promptId}`);
|
|
333
|
+
// 2. Poll for results
|
|
334
|
+
let attempts = 0;
|
|
335
|
+
while (attempts < POLLING_MAX_ATTEMPTS) {
|
|
336
|
+
// Check if aborted before each polling attempt
|
|
337
|
+
if (effectiveSignal?.aborted) {
|
|
338
|
+
throw new Error('Workflow execution was cancelled');
|
|
339
|
+
}
|
|
340
|
+
await delay(POLLING_INTERVAL_MS);
|
|
341
|
+
// Check history
|
|
342
|
+
let historyResponse;
|
|
343
|
+
try {
|
|
344
|
+
historyResponse = await httpClient.get(`${url}/history/${promptId}`, {
|
|
345
|
+
timeout: HISTORY_REQUEST_TIMEOUT,
|
|
346
|
+
abortSignal: effectiveSignal,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (effectiveSignal?.aborted || (error instanceof Error && error.name === 'CanceledError')) {
|
|
351
|
+
throw new Error('Workflow execution was cancelled');
|
|
352
|
+
}
|
|
353
|
+
const httpError = error instanceof Error ? error : undefined;
|
|
354
|
+
const errorCode = httpError?.code;
|
|
355
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
356
|
+
if (errorCode === 'ECONNREFUSED') {
|
|
357
|
+
throw new Error(`Lost connection to ComfyUI server at ${url}.`);
|
|
358
|
+
}
|
|
359
|
+
else if (errorCode === 'ETIMEDOUT' || errorCode === 'ECONNABORTED') {
|
|
360
|
+
logger.warn(`Timeout checking history (attempt ${attempts + 1}/${POLLING_MAX_ATTEMPTS})`);
|
|
361
|
+
attempts++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
logger.warn(`Error checking history: ${errorMessage}`);
|
|
366
|
+
attempts++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (historyResponse && historyResponse[promptId]) {
|
|
371
|
+
const outputs = historyResponse[promptId].outputs;
|
|
372
|
+
if (outputs) {
|
|
373
|
+
const images = extractImagesFromOutputs(outputs, url);
|
|
374
|
+
if (images.length === 0) {
|
|
375
|
+
throw new Error('Workflow completed but no images were generated');
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
prompt_id: promptId,
|
|
380
|
+
images: images
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
attempts++;
|
|
385
|
+
}
|
|
386
|
+
throw new Error(`Workflow execution timeout after ${POLLING_MAX_ATTEMPTS} seconds (prompt_id: ${promptId}). The workflow may still be running on ComfyUI server.`);
|
|
387
|
+
}
|
|
388
|
+
finally {
|
|
389
|
+
// Clean up AbortController
|
|
390
|
+
if (abortController) {
|
|
391
|
+
currentAbortController = null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Main function: Handle tool invocation
|
|
397
|
+
* @param query - User query text
|
|
398
|
+
* @param options - Optional configuration parameters
|
|
399
|
+
* @param options.comfyUiUrl - ComfyUI server URL
|
|
400
|
+
* @param options.workflowConfig - Workflow configuration options
|
|
401
|
+
*/
|
|
402
|
+
async function handleToolInput(query, options = {}) {
|
|
403
|
+
try {
|
|
404
|
+
const { comfyUiUrl, workflowConfig } = options;
|
|
405
|
+
logger.info(`Received query: ${query}`);
|
|
406
|
+
if (comfyUiUrl) {
|
|
407
|
+
logger.info(`Using ComfyUI URL: ${comfyUiUrl}`);
|
|
408
|
+
}
|
|
409
|
+
// Parse input parameters
|
|
410
|
+
const params = parseInput(query);
|
|
411
|
+
logger.debug('Parsed parameters:', JSON.stringify(params, null, 2));
|
|
412
|
+
// Get workflow template from configuration
|
|
413
|
+
const workflowTemplate = (0, workflowConfig_1.getWorkflowTemplate)(workflowConfig);
|
|
414
|
+
// Update workflow
|
|
415
|
+
const workflow = updateWorkflow(workflowTemplate, params);
|
|
416
|
+
// Execute workflow
|
|
417
|
+
const result = await executeComfyUIWorkflow(workflow, comfyUiUrl);
|
|
418
|
+
logger.info(`Generated ${result.images.length} image(s)`);
|
|
419
|
+
// Return result
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
message: `Successfully generated ${result.images.length} image(s) with prompt: "${params.prompt}"`,
|
|
423
|
+
data: {
|
|
424
|
+
prompt: params.prompt,
|
|
425
|
+
images: result.images.map(img => img.url),
|
|
426
|
+
parameters: params
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
432
|
+
logger.error('Error:', errorMessage);
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
error: errorMessage,
|
|
436
|
+
message: `Failed to generate image: ${errorMessage}`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
//# sourceMappingURL=ComfyUIAgentTool.js.map
|