dsh-comfyui 0.3.0-beta.0
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 +21 -0
- package/README.md +160 -0
- package/README.zh.md +160 -0
- package/client/client.js +3089 -0
- package/client/client.js.map +1 -0
- package/cordis.patch.yml +4 -0
- package/lib/analyze.d.ts +61 -0
- package/lib/analyze.js +96 -0
- package/lib/comfyui.d.ts +195 -0
- package/lib/comfyui.js +392 -0
- package/lib/config.d.ts +71 -0
- package/lib/config.js +28 -0
- package/lib/convert.d.ts +35 -0
- package/lib/convert.js +289 -0
- package/lib/host-hint.d.ts +29 -0
- package/lib/host-hint.js +73 -0
- package/lib/http.d.ts +15 -0
- package/lib/http.js +41 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +344 -0
- package/lib/params.d.ts +63 -0
- package/lib/params.js +537 -0
- package/lib/progress.d.ts +27 -0
- package/lib/progress.js +98 -0
- package/lib/proxy.d.ts +13 -0
- package/lib/proxy.js +89 -0
- package/lib/queue.d.ts +50 -0
- package/lib/queue.js +103 -0
- package/lib/routes.d.ts +13 -0
- package/lib/routes.js +800 -0
- package/lib/skill.d.ts +12 -0
- package/lib/skill.js +72 -0
- package/lib/store.d.ts +120 -0
- package/lib/store.js +193 -0
- package/lib/templates.d.ts +36 -0
- package/lib/templates.js +84 -0
- package/lib/tools.d.ts +165 -0
- package/lib/tools.js +493 -0
- package/package.json +80 -0
package/lib/comfyui.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ComfyUI HTTP API client: queue a workflow, poll history for
|
|
3
|
+
* completion, read object_info/system_stats, and fetch generated media.
|
|
4
|
+
* Only the endpoints dsh-comfyui needs are implemented; the server's own
|
|
5
|
+
* WebSocket progress channel is deliberately unused (history polling is
|
|
6
|
+
* simpler and works across proxies and remote installs).
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
/** Failure talking to the ComfyUI server. */
|
|
10
|
+
export class ComfyUIError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.name = 'ComfyUIError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function sleep(millis, signal) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
if (signal.aborted) {
|
|
21
|
+
reject(new ComfyUIError('ComfyUI generation aborted'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const timer = setTimeout(resolve, millis);
|
|
25
|
+
signal.addEventListener('abort', () => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
reject(new ComfyUIError('ComfyUI generation aborted'));
|
|
28
|
+
}, { once: true });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function guessContentType(filename) {
|
|
32
|
+
const lower = filename.toLowerCase();
|
|
33
|
+
if (lower.endsWith('.png'))
|
|
34
|
+
return 'image/png';
|
|
35
|
+
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
|
|
36
|
+
return 'image/jpeg';
|
|
37
|
+
if (lower.endsWith('.webp'))
|
|
38
|
+
return 'image/webp';
|
|
39
|
+
if (lower.endsWith('.gif'))
|
|
40
|
+
return 'image/gif';
|
|
41
|
+
if (lower.endsWith('.mp4'))
|
|
42
|
+
return 'video/mp4';
|
|
43
|
+
if (lower.endsWith('.webm'))
|
|
44
|
+
return 'video/webm';
|
|
45
|
+
if (lower.endsWith('.avi'))
|
|
46
|
+
return 'video/x-msvideo';
|
|
47
|
+
if (lower.endsWith('.mp3'))
|
|
48
|
+
return 'audio/mpeg';
|
|
49
|
+
if (lower.endsWith('.wav'))
|
|
50
|
+
return 'audio/wav';
|
|
51
|
+
if (lower.endsWith('.ogg'))
|
|
52
|
+
return 'audio/ogg';
|
|
53
|
+
if (lower.endsWith('.flac'))
|
|
54
|
+
return 'audio/flac';
|
|
55
|
+
if (lower.endsWith('.m4a'))
|
|
56
|
+
return 'audio/mp4';
|
|
57
|
+
if (lower.endsWith('.aac'))
|
|
58
|
+
return 'audio/aac';
|
|
59
|
+
if (lower.endsWith('.opus'))
|
|
60
|
+
return 'audio/opus';
|
|
61
|
+
return 'application/octet-stream';
|
|
62
|
+
}
|
|
63
|
+
/** The per-process client id ComfyUI uses to correlate queued prompts. */
|
|
64
|
+
export const CLIENT_ID = randomUUID();
|
|
65
|
+
/** HTTP client over the ComfyUI REST API. */
|
|
66
|
+
export class ComfyUIClient {
|
|
67
|
+
baseUrl;
|
|
68
|
+
apiKey;
|
|
69
|
+
connectTimeoutMs;
|
|
70
|
+
maxMediaBytes;
|
|
71
|
+
constructor(baseUrl, apiKey, connectTimeoutMs, maxMediaBytes) {
|
|
72
|
+
this.baseUrl = baseUrl;
|
|
73
|
+
this.apiKey = apiKey;
|
|
74
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
75
|
+
this.maxMediaBytes = maxMediaBytes;
|
|
76
|
+
}
|
|
77
|
+
endpoint(path) {
|
|
78
|
+
return `${this.baseUrl.replace(/\/+$/, '')}${path}`;
|
|
79
|
+
}
|
|
80
|
+
async request(path, init = {}, timeoutMs) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs ?? this.connectTimeoutMs);
|
|
83
|
+
try {
|
|
84
|
+
const headers = { ...init.headers };
|
|
85
|
+
if (this.apiKey !== undefined)
|
|
86
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
87
|
+
const response = await fetch(this.endpoint(path), { ...init, headers, signal: controller.signal });
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const body = await response.text().catch(() => '');
|
|
90
|
+
throw new ComfyUIError(`ComfyUI ${path} failed: HTTP ${response.status}${body !== '' ? ` — ${body.slice(0, 300)}` : ''}`, response.status);
|
|
91
|
+
}
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
if (text === '')
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new ComfyUIError(`ComfyUI ${path} returned non-JSON body`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Upload a file (multipart body forwarded verbatim) into ComfyUI's input directory. */
|
|
107
|
+
async uploadFile(body, contentType) {
|
|
108
|
+
const data = await this.request('/upload/image', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'content-type': contentType },
|
|
111
|
+
body,
|
|
112
|
+
});
|
|
113
|
+
return data ?? {};
|
|
114
|
+
}
|
|
115
|
+
/** Queue one API-format workflow and return its prompt id. */
|
|
116
|
+
async queuePrompt(workflow, options = {}) {
|
|
117
|
+
const payload = { prompt: workflow, client_id: CLIENT_ID };
|
|
118
|
+
if (options.promptId !== undefined)
|
|
119
|
+
payload['prompt_id'] = options.promptId;
|
|
120
|
+
if (options.front === true)
|
|
121
|
+
payload['front'] = true;
|
|
122
|
+
if (options.extraData !== undefined && Object.keys(options.extraData).length > 0) {
|
|
123
|
+
payload['extra_data'] = options.extraData;
|
|
124
|
+
}
|
|
125
|
+
const data = await this.request('/prompt', {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: { 'content-type': 'application/json' },
|
|
128
|
+
body: JSON.stringify(payload),
|
|
129
|
+
});
|
|
130
|
+
if (typeof data.prompt_id !== 'string') {
|
|
131
|
+
throw new ComfyUIError('ComfyUI /prompt returned no prompt_id');
|
|
132
|
+
}
|
|
133
|
+
return data.prompt_id;
|
|
134
|
+
}
|
|
135
|
+
/** Read one prompt's history entry; undefined while the prompt is unknown or evicted. */
|
|
136
|
+
async getHistory(promptId) {
|
|
137
|
+
const data = await this.request(`/history/${encodeURIComponent(promptId)}`);
|
|
138
|
+
return data[promptId];
|
|
139
|
+
}
|
|
140
|
+
/** The server-side queue: running + pending prompts. */
|
|
141
|
+
async getQueue() {
|
|
142
|
+
// ComfyUI serializes each queue slot as [number, prompt_id, prompt, ...];
|
|
143
|
+
// only the number and prompt_id are needed here.
|
|
144
|
+
const raw = await this.request('/queue');
|
|
145
|
+
const parse = (list) => {
|
|
146
|
+
if (!Array.isArray(list))
|
|
147
|
+
return [];
|
|
148
|
+
const items = [];
|
|
149
|
+
for (const entry of list) {
|
|
150
|
+
if (Array.isArray(entry) && typeof entry[1] === 'string' && entry[1] !== '') {
|
|
151
|
+
items.push({ number: typeof entry[0] === 'number' ? entry[0] : 0, prompt_id: entry[1] });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return items;
|
|
155
|
+
};
|
|
156
|
+
return { queue_running: parse(raw.queue_running), queue_pending: parse(raw.queue_pending) };
|
|
157
|
+
}
|
|
158
|
+
/** Unified job list with status filters, sorting, and pagination. */
|
|
159
|
+
async getJobs(options = {}) {
|
|
160
|
+
const params = new URLSearchParams();
|
|
161
|
+
if (options.status !== undefined && options.status.length > 0)
|
|
162
|
+
params.set('status', options.status.join(','));
|
|
163
|
+
if (options.limit !== undefined)
|
|
164
|
+
params.set('limit', String(options.limit));
|
|
165
|
+
if (options.offset !== undefined)
|
|
166
|
+
params.set('offset', String(options.offset));
|
|
167
|
+
if (options.sortBy !== undefined)
|
|
168
|
+
params.set('sort_by', options.sortBy);
|
|
169
|
+
if (options.sortOrder !== undefined)
|
|
170
|
+
params.set('sort_order', options.sortOrder);
|
|
171
|
+
const query = params.toString();
|
|
172
|
+
return this.request(`/api/jobs${query !== '' ? `?${query}` : ''}`);
|
|
173
|
+
}
|
|
174
|
+
/** One job by id, including its workflow prompt and outputs. */
|
|
175
|
+
async getJob(jobId) {
|
|
176
|
+
try {
|
|
177
|
+
return await this.request(`/api/jobs/${encodeURIComponent(jobId)}`);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof ComfyUIError && error.status === 404)
|
|
181
|
+
return undefined;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Remove specific prompts from the pending queue. */
|
|
186
|
+
async deleteQueueItems(promptIds) {
|
|
187
|
+
await this.request('/queue', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: { 'content-type': 'application/json' },
|
|
190
|
+
body: JSON.stringify({ delete: promptIds }),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** Clear the entire pending queue (running job is unaffected). */
|
|
194
|
+
async clearQueue() {
|
|
195
|
+
await this.request('/queue', {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'content-type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ clear: true }),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** Interrupt the running prompt; without an id, interrupt globally. */
|
|
202
|
+
async interruptPrompt(promptId) {
|
|
203
|
+
await this.request('/interrupt', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'content-type': 'application/json' },
|
|
206
|
+
body: JSON.stringify(promptId !== undefined ? { prompt_id: promptId } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/** Cancel one job regardless of state (running → interrupt, pending → dequeue). */
|
|
210
|
+
async cancelJob(jobId) {
|
|
211
|
+
return this.request(`/api/jobs/${encodeURIComponent(jobId)}/cancel`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'content-type': 'application/json' },
|
|
214
|
+
body: '{}',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Best-effort batch cancel; finished or unknown ids are no-ops. */
|
|
218
|
+
async cancelJobs(jobIds) {
|
|
219
|
+
return this.request('/api/jobs/cancel', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ job_ids: jobIds }),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** Clear or selectively delete history entries. */
|
|
226
|
+
async clearHistory() {
|
|
227
|
+
await this.request('/history', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'content-type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({ clear: true }),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** Delete specific history entries. */
|
|
234
|
+
async deleteHistory(promptIds) {
|
|
235
|
+
await this.request('/history', {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: { 'content-type': 'application/json' },
|
|
238
|
+
body: JSON.stringify({ delete: promptIds }),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Ask ComfyUI to unload models / free memory (per /free flags). */
|
|
242
|
+
async freeMemory(options = {}) {
|
|
243
|
+
await this.request('/free', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'content-type': 'application/json' },
|
|
246
|
+
body: JSON.stringify({ unload_models: options.unloadModels === true, free_memory: options.freeMemory === true }),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/** List one user-data subdirectory (e.g. 'workflows') on the ComfyUI server. */
|
|
250
|
+
async listUserData(subdir) {
|
|
251
|
+
const data = await this.request(`/v2/userdata?path=${encodeURIComponent(subdir)}`);
|
|
252
|
+
if (!Array.isArray(data))
|
|
253
|
+
return [];
|
|
254
|
+
if (typeof data[0] === 'string') {
|
|
255
|
+
return data.map((name) => ({ name, path: `${subdir}/${name}`, type: 'file' }));
|
|
256
|
+
}
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
/** Read one user-data file (path relative to the user root, e.g. 'workflows/x.json'). */
|
|
260
|
+
async getUserDataFile(relPath) {
|
|
261
|
+
// The {file} route matches a single segment only, so the relative path is
|
|
262
|
+
// URL-encoded (the handler unquotes it) — see app/user_manager.py.
|
|
263
|
+
return this.request(`/userdata/${encodeURIComponent(relPath)}`);
|
|
264
|
+
}
|
|
265
|
+
/** Node definitions for workflow construction (comfyui_object_info). */
|
|
266
|
+
async objectInfo() {
|
|
267
|
+
return this.request('/object_info');
|
|
268
|
+
}
|
|
269
|
+
/** Server health/version probe. */
|
|
270
|
+
async systemStats() {
|
|
271
|
+
return this.request('/system_stats');
|
|
272
|
+
}
|
|
273
|
+
/** Ask ComfyUI to interrupt the running prompt. */
|
|
274
|
+
async interrupt() {
|
|
275
|
+
await this.request('/interrupt', { method: 'POST' });
|
|
276
|
+
}
|
|
277
|
+
/** Download one generated media file through GET /view. */
|
|
278
|
+
async fetchView(ref) {
|
|
279
|
+
const params = new URLSearchParams({ filename: ref.filename, subfolder: ref.subfolder, type: ref.type });
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const timer = setTimeout(() => controller.abort(), this.connectTimeoutMs);
|
|
282
|
+
try {
|
|
283
|
+
const headers = {};
|
|
284
|
+
if (this.apiKey !== undefined)
|
|
285
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
286
|
+
const response = await fetch(this.endpoint(`/view?${params.toString()}`), { headers, signal: controller.signal });
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`);
|
|
289
|
+
}
|
|
290
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
291
|
+
if (bytes.byteLength > this.maxMediaBytes) {
|
|
292
|
+
throw new ComfyUIError(`ComfyUI media too large: ${bytes.byteLength} bytes exceeds maxMediaBytes ${this.maxMediaBytes}`);
|
|
293
|
+
}
|
|
294
|
+
return { bytes, contentType: response.headers.get('content-type') ?? guessContentType(ref.filename) };
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Poll history until the prompt completes, fails, or the budget/signal ends.
|
|
302
|
+
* Interrupts the server when the signal aborts before throwing.
|
|
303
|
+
*/
|
|
304
|
+
async waitForCompletion(opts) {
|
|
305
|
+
const { promptId, timeoutMs, pollIntervalMs, signal } = opts;
|
|
306
|
+
const deadline = Date.now() + timeoutMs;
|
|
307
|
+
for (;;) {
|
|
308
|
+
if (signal.aborted) {
|
|
309
|
+
await this.interrupt().catch(() => undefined);
|
|
310
|
+
throw new ComfyUIError(`ComfyUI generation interrupted (prompt ${promptId})`);
|
|
311
|
+
}
|
|
312
|
+
const entry = await this.getHistory(promptId);
|
|
313
|
+
if (entry !== undefined) {
|
|
314
|
+
const status = entry.status;
|
|
315
|
+
if (status?.status_str === 'success' || status?.completed === true || hasMedia(entry)) {
|
|
316
|
+
return entry;
|
|
317
|
+
}
|
|
318
|
+
if (status?.status_str === 'error') {
|
|
319
|
+
throw new ComfyUIError(historyErrorMessage(promptId, entry));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (Date.now() >= deadline) {
|
|
323
|
+
throw new ComfyUIError(`ComfyUI generation timed out after ${timeoutMs} ms (prompt ${promptId})`);
|
|
324
|
+
}
|
|
325
|
+
await sleep(pollIntervalMs, signal);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function hasMedia(entry) {
|
|
330
|
+
for (const output of Object.values(entry.outputs ?? {})) {
|
|
331
|
+
if ((output.images?.length ?? 0) > 0 || (output.videos?.length ?? 0) > 0 || (output.gifs?.length ?? 0) > 0) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
/** Compose a readable failure message from history status messages. */
|
|
338
|
+
export function historyErrorMessage(promptId, entry) {
|
|
339
|
+
const details = [];
|
|
340
|
+
for (const message of entry.status?.messages ?? []) {
|
|
341
|
+
if (Array.isArray(message) && typeof message[0] === 'string') {
|
|
342
|
+
const [, payload] = message;
|
|
343
|
+
if (typeof payload === 'object' && payload !== null) {
|
|
344
|
+
const record = payload;
|
|
345
|
+
if (typeof record.exception_message === 'string') {
|
|
346
|
+
details.push(record.exception_message.slice(0, 500));
|
|
347
|
+
}
|
|
348
|
+
else if (typeof record.exception_type === 'string') {
|
|
349
|
+
details.push(record.exception_type);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return `ComfyUI execution failed (prompt ${promptId}): ${details.join('; ') || 'unknown error'}`;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Collect media items from a completed history entry, in node/output order,
|
|
358
|
+
* capped by maxItems. The URL is the same-origin proxy route when a web
|
|
359
|
+
* server is present, otherwise a ComfyUI /view URL (for headless hosts).
|
|
360
|
+
*/
|
|
361
|
+
export function collectMedia(opts) {
|
|
362
|
+
const { promptId, entry, maxItems, proxyBase } = opts;
|
|
363
|
+
const items = [];
|
|
364
|
+
const AUDIO_EXT = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
|
|
365
|
+
const VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi)$/i;
|
|
366
|
+
for (const [node, output] of Object.entries(entry.outputs ?? {})) {
|
|
367
|
+
const collections = [
|
|
368
|
+
['image', output.images],
|
|
369
|
+
['video', output.videos],
|
|
370
|
+
// GIFs are displayable images (animated), not opaque "other".
|
|
371
|
+
['image', output.gifs],
|
|
372
|
+
];
|
|
373
|
+
for (const [kind, refs] of collections) {
|
|
374
|
+
for (const [index, ref] of (refs ?? []).entries()) {
|
|
375
|
+
if (items.length >= maxItems)
|
|
376
|
+
return items;
|
|
377
|
+
// Some nodes emit audio/video filenames through the image/video
|
|
378
|
+
// arrays; classify them by extension so the card renders a player.
|
|
379
|
+
const itemKind = AUDIO_EXT.test(ref.filename) ? 'audio' : VIDEO_EXT.test(ref.filename) ? 'video' : kind;
|
|
380
|
+
const query = new URLSearchParams({ prompt: promptId, node, index: String(index) });
|
|
381
|
+
items.push({
|
|
382
|
+
...ref,
|
|
383
|
+
node,
|
|
384
|
+
index,
|
|
385
|
+
kind: itemKind,
|
|
386
|
+
url: proxyBase !== undefined ? `${proxyBase}/comfyui/media?${query.toString()}` : ref.filename,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return items;
|
|
392
|
+
}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-comfyui host configuration. The same schema drives the Loader entry
|
|
3
|
+
* config (cordis.yml patch) and the `comfyui:` settings section the browser
|
|
4
|
+
* settings page writes, so one shape covers both doors into the same values.
|
|
5
|
+
*/
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
8
|
+
/** ComfyUI HTTP server base URL. */
|
|
9
|
+
baseUrl: z<string, string>;
|
|
10
|
+
/** Environment-variable name of the optional API key (credentials ref). */
|
|
11
|
+
apiKeyEnv: z<string, string>;
|
|
12
|
+
/** Per-request connect/read timeout for the ComfyUI HTTP client. */
|
|
13
|
+
connectTimeoutMs: z<number, number>;
|
|
14
|
+
/** How long a synchronous generation (or background job) waits for workflow completion. */
|
|
15
|
+
timeoutMs: z<number, number>;
|
|
16
|
+
/** History polling interval while waiting for completion. */
|
|
17
|
+
pollIntervalMs: z<number, number>;
|
|
18
|
+
/** Max media items returned per completed workflow. */
|
|
19
|
+
maxMediaItems: z<number, number>;
|
|
20
|
+
/** Max bytes the media proxy streams for one file. */
|
|
21
|
+
maxMediaBytes: z<number, number>;
|
|
22
|
+
/** Directory for plugin data (workflow library, asset index); empty means DSH_HOME/data/dsh-comfyui. */
|
|
23
|
+
dataDir: z<string, string>;
|
|
24
|
+
/** Max asset records kept in the asset index. */
|
|
25
|
+
maxAssets: z<number, number>;
|
|
26
|
+
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
27
|
+
mediaHost: z<string, string>;
|
|
28
|
+
}>, Schemastery.ObjectT<{
|
|
29
|
+
/** ComfyUI HTTP server base URL. */
|
|
30
|
+
baseUrl: z<string, string>;
|
|
31
|
+
/** Environment-variable name of the optional API key (credentials ref). */
|
|
32
|
+
apiKeyEnv: z<string, string>;
|
|
33
|
+
/** Per-request connect/read timeout for the ComfyUI HTTP client. */
|
|
34
|
+
connectTimeoutMs: z<number, number>;
|
|
35
|
+
/** How long a synchronous generation (or background job) waits for workflow completion. */
|
|
36
|
+
timeoutMs: z<number, number>;
|
|
37
|
+
/** History polling interval while waiting for completion. */
|
|
38
|
+
pollIntervalMs: z<number, number>;
|
|
39
|
+
/** Max media items returned per completed workflow. */
|
|
40
|
+
maxMediaItems: z<number, number>;
|
|
41
|
+
/** Max bytes the media proxy streams for one file. */
|
|
42
|
+
maxMediaBytes: z<number, number>;
|
|
43
|
+
/** Directory for plugin data (workflow library, asset index); empty means DSH_HOME/data/dsh-comfyui. */
|
|
44
|
+
dataDir: z<string, string>;
|
|
45
|
+
/** Max asset records kept in the asset index. */
|
|
46
|
+
maxAssets: z<number, number>;
|
|
47
|
+
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
48
|
+
mediaHost: z<string, string>;
|
|
49
|
+
}>>;
|
|
50
|
+
export type Config = {
|
|
51
|
+
/** ComfyUI HTTP server base URL. */
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
/** Environment-variable name of the optional API key (credentials ref). */
|
|
54
|
+
apiKeyEnv: string;
|
|
55
|
+
/** Per-request connect/read timeout for the ComfyUI HTTP client. */
|
|
56
|
+
connectTimeoutMs: number;
|
|
57
|
+
/** How long a synchronous generation waits for workflow completion. */
|
|
58
|
+
timeoutMs: number;
|
|
59
|
+
/** History polling interval while waiting for completion. */
|
|
60
|
+
pollIntervalMs: number;
|
|
61
|
+
/** Max media items returned per completed workflow. */
|
|
62
|
+
maxMediaItems: number;
|
|
63
|
+
/** Max bytes the media proxy streams for one file. */
|
|
64
|
+
maxMediaBytes: number;
|
|
65
|
+
/** Directory for plugin data (workflow library, asset index); empty means DSH_HOME/data/dsh-comfyui. */
|
|
66
|
+
dataDir: string;
|
|
67
|
+
/** Max asset records kept in the asset index. */
|
|
68
|
+
maxAssets: number;
|
|
69
|
+
/** External base URL for generated media; empty auto-detects the request host. */
|
|
70
|
+
mediaHost: string;
|
|
71
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-comfyui host configuration. The same schema drives the Loader entry
|
|
3
|
+
* config (cordis.yml patch) and the `comfyui:` settings section the browser
|
|
4
|
+
* settings page writes, so one shape covers both doors into the same values.
|
|
5
|
+
*/
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
export const Config = z.object({
|
|
8
|
+
/** ComfyUI HTTP server base URL. */
|
|
9
|
+
baseUrl: z.string().default('http://127.0.0.1:8188'),
|
|
10
|
+
/** Environment-variable name of the optional API key (credentials ref). */
|
|
11
|
+
apiKeyEnv: z.string().default('COMFYUI_API_KEY'),
|
|
12
|
+
/** Per-request connect/read timeout for the ComfyUI HTTP client. */
|
|
13
|
+
connectTimeoutMs: z.number().min(1_000).max(60_000).default(10_000),
|
|
14
|
+
/** How long a synchronous generation (or background job) waits for workflow completion. */
|
|
15
|
+
timeoutMs: z.number().min(5_000).max(3_600_000).default(900_000),
|
|
16
|
+
/** History polling interval while waiting for completion. */
|
|
17
|
+
pollIntervalMs: z.number().min(200).max(10_000).default(1_000),
|
|
18
|
+
/** Max media items returned per completed workflow. */
|
|
19
|
+
maxMediaItems: z.number().min(1).max(50).default(12),
|
|
20
|
+
/** Max bytes the media proxy streams for one file. */
|
|
21
|
+
maxMediaBytes: z.number().min(64 * 1024).max(512 * 1024 * 1024).default(64 * 1024 * 1024),
|
|
22
|
+
/** Directory for plugin data (workflow library, asset index); empty means DSH_HOME/data/dsh-comfyui. */
|
|
23
|
+
dataDir: z.string().default(''),
|
|
24
|
+
/** Max asset records kept in the asset index. */
|
|
25
|
+
maxAssets: z.number().min(1).max(10_000).default(200),
|
|
26
|
+
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
27
|
+
mediaHost: z.string().default(''),
|
|
28
|
+
});
|
package/lib/convert.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a ComfyUI UI-graph workflow (the format the ComfyUI frontend saves
|
|
3
|
+
* to the server via /api/userdata) into the API format accepted by POST
|
|
4
|
+
* /prompt. Modeled on the frontend's convertToApiFormat: input links become
|
|
5
|
+
* [nodeId, slot] references, widgets_values are zipped onto input names in
|
|
6
|
+
* object_info order (control_after_generate is the extra seed widget), and
|
|
7
|
+
* Reroute / bypassed (mode 4) nodes are skipped with their links rewired.
|
|
8
|
+
* Nodes the conversion cannot represent fail loudly with the offending type.
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiWorkflow {
|
|
11
|
+
[nodeId: string]: {
|
|
12
|
+
class_type: string;
|
|
13
|
+
inputs: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export type ConvertResult = {
|
|
17
|
+
ok: true;
|
|
18
|
+
workflow: ApiWorkflow;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Convert a UI-graph workflow (or one extracted component of it) to API
|
|
26
|
+
* format using the live node definitions.
|
|
27
|
+
* @param graph - parsed ComfyUI UI graph (v0.4 format).
|
|
28
|
+
* @param objectInfo - the server's `/object_info` response.
|
|
29
|
+
* @param options - `includeNodeIds` restricts conversion to one connected
|
|
30
|
+
* component (extraction); link resolution still uses the full graph, which
|
|
31
|
+
* is safe because components never share links.
|
|
32
|
+
*/
|
|
33
|
+
export declare function convertGraphToApi(graph: unknown, objectInfo: Record<string, unknown>, options?: {
|
|
34
|
+
includeNodeIds?: Set<number>;
|
|
35
|
+
}): ConvertResult;
|