flowscale 2.1.0-beta.5 → 2.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/operator-server.d.ts +19 -9
- package/dist/operator-server.js +194 -38
- package/package.json +1 -1
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { NodePodsTransport } from './node-transport';
|
|
2
2
|
/**
|
|
3
|
-
* PodsOperatorServer — exposes a
|
|
4
|
-
*
|
|
5
|
-
* without any direct knowledge of ComfyUI.
|
|
3
|
+
* PodsOperatorServer — exposes a complete ComfyUI proxy API so that remote
|
|
4
|
+
* FlowScale apps (e.g. AI OS, Studio) can operate entirely through the operator
|
|
5
|
+
* without any direct knowledge of ComfyUI ports or URLs.
|
|
6
6
|
*
|
|
7
|
-
* Endpoints:
|
|
8
|
-
* GET /api/pods
|
|
9
|
-
* GET /api/pods/:id
|
|
10
|
-
* POST /api/pods/:id/upload
|
|
11
|
-
* POST /api/pods/:id/execute
|
|
12
|
-
* GET /api/pods/:id/view?filename=
|
|
7
|
+
* HTTP Endpoints:
|
|
8
|
+
* GET /api/pods — list all pods
|
|
9
|
+
* GET /api/pods/:id — get a single pod
|
|
10
|
+
* POST /api/pods/:id/upload — upload a file to ComfyUI
|
|
11
|
+
* POST /api/pods/:id/execute — execute workflow and wait (blocking)
|
|
12
|
+
* GET /api/pods/:id/view?filename= — proxy a ComfyUI output image
|
|
13
|
+
* GET /api/pods/:id/workflow?filename= — load a workflow JSON from ComfyUI userdata
|
|
14
|
+
* POST /api/pods/:id/queue — queue a prompt (non-blocking, returns prompt_id)
|
|
15
|
+
* GET /api/pods/:id/history/:promptId — fetch execution history for a prompt
|
|
16
|
+
* GET /api/pods/:id/object_info — proxy ComfyUI node type definitions
|
|
17
|
+
* POST /api/pods/:id/interrupt — cancel current execution
|
|
18
|
+
*
|
|
19
|
+
* WebSocket:
|
|
20
|
+
* WS /api/pods/:id/ws?clientId= — transparent tunnel to ComfyUI WS
|
|
13
21
|
*
|
|
14
22
|
* Serves on 0.0.0.0 so any machine on the LAN can reach it.
|
|
15
23
|
* Includes CORS headers for browser-based clients.
|
|
@@ -23,6 +31,8 @@ export declare class PodsOperatorServer {
|
|
|
23
31
|
start(): void;
|
|
24
32
|
stop(): void;
|
|
25
33
|
private _json;
|
|
34
|
+
/** Pipe a GET response from ComfyUI directly to the client (handles large payloads). */
|
|
35
|
+
private _proxyGet;
|
|
26
36
|
private _readBody;
|
|
27
37
|
/** Returns the port of the first running instance in the given pod. */
|
|
28
38
|
private _runningPort;
|
package/dist/operator-server.js
CHANGED
|
@@ -71,18 +71,27 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
71
71
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72
72
|
exports.PodsOperatorServer = void 0;
|
|
73
73
|
var http = __importStar(require("http"));
|
|
74
|
+
var net = __importStar(require("net"));
|
|
74
75
|
var local_pods_1 = require("./local-pods");
|
|
75
76
|
/**
|
|
76
|
-
* PodsOperatorServer — exposes a
|
|
77
|
-
*
|
|
78
|
-
* without any direct knowledge of ComfyUI.
|
|
77
|
+
* PodsOperatorServer — exposes a complete ComfyUI proxy API so that remote
|
|
78
|
+
* FlowScale apps (e.g. AI OS, Studio) can operate entirely through the operator
|
|
79
|
+
* without any direct knowledge of ComfyUI ports or URLs.
|
|
79
80
|
*
|
|
80
|
-
* Endpoints:
|
|
81
|
-
* GET /api/pods
|
|
82
|
-
* GET /api/pods/:id
|
|
83
|
-
* POST /api/pods/:id/upload
|
|
84
|
-
* POST /api/pods/:id/execute
|
|
85
|
-
* GET /api/pods/:id/view?filename=
|
|
81
|
+
* HTTP Endpoints:
|
|
82
|
+
* GET /api/pods — list all pods
|
|
83
|
+
* GET /api/pods/:id — get a single pod
|
|
84
|
+
* POST /api/pods/:id/upload — upload a file to ComfyUI
|
|
85
|
+
* POST /api/pods/:id/execute — execute workflow and wait (blocking)
|
|
86
|
+
* GET /api/pods/:id/view?filename= — proxy a ComfyUI output image
|
|
87
|
+
* GET /api/pods/:id/workflow?filename= — load a workflow JSON from ComfyUI userdata
|
|
88
|
+
* POST /api/pods/:id/queue — queue a prompt (non-blocking, returns prompt_id)
|
|
89
|
+
* GET /api/pods/:id/history/:promptId — fetch execution history for a prompt
|
|
90
|
+
* GET /api/pods/:id/object_info — proxy ComfyUI node type definitions
|
|
91
|
+
* POST /api/pods/:id/interrupt — cancel current execution
|
|
92
|
+
*
|
|
93
|
+
* WebSocket:
|
|
94
|
+
* WS /api/pods/:id/ws?clientId= — transparent tunnel to ComfyUI WS
|
|
86
95
|
*
|
|
87
96
|
* Serves on 0.0.0.0 so any machine on the LAN can reach it.
|
|
88
97
|
* Includes CORS headers for browser-based clients.
|
|
@@ -100,7 +109,7 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
100
109
|
if (this.server)
|
|
101
110
|
return;
|
|
102
111
|
this.server = http.createServer(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
103
|
-
var url, query, pods, podMatch, pod, uploadMatch, podId, port_1, executeMatch, podId, body, _a, _b, result, viewMatch, podId, port,
|
|
112
|
+
var url, query, pods, podMatch, pod, uploadMatch, podId, port_1, executeMatch, podId, body, _a, _b, result, viewMatch, podId, port, workflowMatch, podId, port, filename, encoded, queueMatch, podId, port_2, body_1, historyMatch, podId, promptId, port, objectInfoMatch, podId, port, interruptMatch, podId, port_3, err_1;
|
|
104
113
|
var _this = this;
|
|
105
114
|
var _c, _d, _e, _f, _g, _h;
|
|
106
115
|
return __generator(this, function (_j) {
|
|
@@ -118,7 +127,7 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
118
127
|
query = new URLSearchParams((_e = ((_d = req.url) !== null && _d !== void 0 ? _d : '').split('?')[1]) !== null && _e !== void 0 ? _e : '');
|
|
119
128
|
_j.label = 1;
|
|
120
129
|
case 1:
|
|
121
|
-
_j.trys.push([1,
|
|
130
|
+
_j.trys.push([1, 31, , 32]);
|
|
122
131
|
if (!(req.method === 'GET' && (url === '/api/pods' || url === '/api/pods/'))) return [3 /*break*/, 3];
|
|
123
132
|
return [4 /*yield*/, this.transport.list()];
|
|
124
133
|
case 2:
|
|
@@ -137,12 +146,9 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
137
146
|
uploadMatch = url.match(/^\/api\/pods\/([^/]+)\/upload$/);
|
|
138
147
|
if (!(req.method === 'POST' && uploadMatch)) return [3 /*break*/, 8];
|
|
139
148
|
podId = decodeURIComponent(uploadMatch[1]);
|
|
140
|
-
return [4 /*yield*/, this._runningPort(podId)
|
|
141
|
-
// Pipe the multipart body directly to ComfyUI to preserve boundary and encoding
|
|
142
|
-
];
|
|
149
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
143
150
|
case 6:
|
|
144
151
|
port_1 = _j.sent();
|
|
145
|
-
// Pipe the multipart body directly to ComfyUI to preserve boundary and encoding
|
|
146
152
|
return [4 /*yield*/, new Promise(function (resolve, reject) {
|
|
147
153
|
var _a;
|
|
148
154
|
var headers = {
|
|
@@ -172,7 +178,6 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
172
178
|
req.pipe(upstreamReq);
|
|
173
179
|
})];
|
|
174
180
|
case 7:
|
|
175
|
-
// Pipe the multipart body directly to ComfyUI to preserve boundary and encoding
|
|
176
181
|
_j.sent();
|
|
177
182
|
return [2 /*return*/];
|
|
178
183
|
case 8:
|
|
@@ -189,39 +194,173 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
189
194
|
return [2 /*return*/, this._json(res, 200, result)];
|
|
190
195
|
case 11:
|
|
191
196
|
viewMatch = url.match(/^\/api\/pods\/([^/]+)\/view$/);
|
|
192
|
-
if (!(req.method === 'GET' && viewMatch)) return [3 /*break*/,
|
|
197
|
+
if (!(req.method === 'GET' && viewMatch)) return [3 /*break*/, 14];
|
|
193
198
|
podId = decodeURIComponent(viewMatch[1]);
|
|
194
199
|
return [4 /*yield*/, this._runningPort(podId)];
|
|
195
200
|
case 12:
|
|
196
201
|
port = _j.sent();
|
|
197
|
-
|
|
198
|
-
return [4 /*yield*/, fetch("http://localhost:".concat(port, "/view?").concat(params))];
|
|
202
|
+
return [4 /*yield*/, this._proxyGet(res, port, "/view?".concat(query))];
|
|
199
203
|
case 13:
|
|
200
|
-
|
|
201
|
-
if (!upstream.ok) {
|
|
202
|
-
res.writeHead(upstream.status);
|
|
203
|
-
res.end();
|
|
204
|
-
return [2 /*return*/];
|
|
205
|
-
}
|
|
206
|
-
return [4 /*yield*/, upstream.arrayBuffer()];
|
|
207
|
-
case 14:
|
|
208
|
-
imageBuffer = _j.sent();
|
|
209
|
-
res.writeHead(200, {
|
|
210
|
-
'Content-Type': (_g = upstream.headers.get('content-type')) !== null && _g !== void 0 ? _g : 'image/png',
|
|
211
|
-
'Cache-Control': 'public, max-age=3600',
|
|
212
|
-
});
|
|
213
|
-
res.end(Buffer.from(imageBuffer));
|
|
204
|
+
_j.sent();
|
|
214
205
|
return [2 /*return*/];
|
|
206
|
+
case 14:
|
|
207
|
+
workflowMatch = url.match(/^\/api\/pods\/([^/]+)\/workflow$/);
|
|
208
|
+
if (!(req.method === 'GET' && workflowMatch)) return [3 /*break*/, 17];
|
|
209
|
+
podId = decodeURIComponent(workflowMatch[1]);
|
|
210
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
215
211
|
case 15:
|
|
212
|
+
port = _j.sent();
|
|
213
|
+
filename = (_g = query.get('filename')) !== null && _g !== void 0 ? _g : '';
|
|
214
|
+
encoded = encodeURIComponent("workflows/".concat(filename));
|
|
215
|
+
return [4 /*yield*/, this._proxyGet(res, port, "/api/userdata/".concat(encoded))];
|
|
216
|
+
case 16:
|
|
217
|
+
_j.sent();
|
|
218
|
+
return [2 /*return*/];
|
|
219
|
+
case 17:
|
|
220
|
+
queueMatch = url.match(/^\/api\/pods\/([^/]+)\/queue$/);
|
|
221
|
+
if (!(req.method === 'POST' && queueMatch)) return [3 /*break*/, 21];
|
|
222
|
+
podId = decodeURIComponent(queueMatch[1]);
|
|
223
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
224
|
+
case 18:
|
|
225
|
+
port_2 = _j.sent();
|
|
226
|
+
return [4 /*yield*/, this._readBody(req)];
|
|
227
|
+
case 19:
|
|
228
|
+
body_1 = _j.sent();
|
|
229
|
+
return [4 /*yield*/, new Promise(function (resolve, reject) {
|
|
230
|
+
var upstreamReq = http.request({
|
|
231
|
+
hostname: 'localhost',
|
|
232
|
+
port: port_2,
|
|
233
|
+
path: '/prompt', method: 'POST',
|
|
234
|
+
headers: { 'content-type': 'application/json', 'content-length': body_1.length },
|
|
235
|
+
}, function (upstreamRes) {
|
|
236
|
+
var chunks = [];
|
|
237
|
+
upstreamRes.on('data', function (c) { return chunks.push(c); });
|
|
238
|
+
upstreamRes.on('end', function () {
|
|
239
|
+
var _a;
|
|
240
|
+
try {
|
|
241
|
+
var data = JSON.parse(Buffer.concat(chunks).toString());
|
|
242
|
+
_this._json(res, (_a = upstreamRes.statusCode) !== null && _a !== void 0 ? _a : 500, data);
|
|
243
|
+
resolve();
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
reject(e);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
upstreamRes.on('error', reject);
|
|
250
|
+
});
|
|
251
|
+
upstreamReq.on('error', reject);
|
|
252
|
+
upstreamReq.write(body_1);
|
|
253
|
+
upstreamReq.end();
|
|
254
|
+
})];
|
|
255
|
+
case 20:
|
|
256
|
+
_j.sent();
|
|
257
|
+
return [2 /*return*/];
|
|
258
|
+
case 21:
|
|
259
|
+
historyMatch = url.match(/^\/api\/pods\/([^/]+)\/history\/([^/]+)$/);
|
|
260
|
+
if (!(req.method === 'GET' && historyMatch)) return [3 /*break*/, 24];
|
|
261
|
+
podId = decodeURIComponent(historyMatch[1]);
|
|
262
|
+
promptId = decodeURIComponent(historyMatch[2]);
|
|
263
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
264
|
+
case 22:
|
|
265
|
+
port = _j.sent();
|
|
266
|
+
return [4 /*yield*/, this._proxyGet(res, port, "/history/".concat(promptId))];
|
|
267
|
+
case 23:
|
|
268
|
+
_j.sent();
|
|
269
|
+
return [2 /*return*/];
|
|
270
|
+
case 24:
|
|
271
|
+
objectInfoMatch = url.match(/^\/api\/pods\/([^/]+)\/object_info$/);
|
|
272
|
+
if (!(req.method === 'GET' && objectInfoMatch)) return [3 /*break*/, 27];
|
|
273
|
+
podId = decodeURIComponent(objectInfoMatch[1]);
|
|
274
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
275
|
+
case 25:
|
|
276
|
+
port = _j.sent();
|
|
277
|
+
return [4 /*yield*/, this._proxyGet(res, port, '/object_info')];
|
|
278
|
+
case 26:
|
|
279
|
+
_j.sent();
|
|
280
|
+
return [2 /*return*/];
|
|
281
|
+
case 27:
|
|
282
|
+
interruptMatch = url.match(/^\/api\/pods\/([^/]+)\/interrupt$/);
|
|
283
|
+
if (!(req.method === 'POST' && interruptMatch)) return [3 /*break*/, 30];
|
|
284
|
+
podId = decodeURIComponent(interruptMatch[1]);
|
|
285
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
286
|
+
case 28:
|
|
287
|
+
port_3 = _j.sent();
|
|
288
|
+
return [4 /*yield*/, new Promise(function (resolve, reject) {
|
|
289
|
+
var upstreamReq = http.request({ hostname: 'localhost', port: port_3, path: '/interrupt', method: 'POST' }, function (upstreamRes) { upstreamRes.resume(); upstreamRes.on('end', resolve); });
|
|
290
|
+
upstreamReq.on('error', reject);
|
|
291
|
+
upstreamReq.end();
|
|
292
|
+
})];
|
|
293
|
+
case 29:
|
|
294
|
+
_j.sent();
|
|
295
|
+
return [2 /*return*/, this._json(res, 200, { ok: true })];
|
|
296
|
+
case 30:
|
|
216
297
|
res.writeHead(404);
|
|
217
298
|
res.end('Not found');
|
|
218
|
-
return [3 /*break*/,
|
|
219
|
-
case
|
|
299
|
+
return [3 /*break*/, 32];
|
|
300
|
+
case 31:
|
|
220
301
|
err_1 = _j.sent();
|
|
221
302
|
console.error('[PodsOperatorServer] Error handling request:', err_1);
|
|
222
303
|
this._json(res, 500, { error: (_h = err_1 === null || err_1 === void 0 ? void 0 : err_1.message) !== null && _h !== void 0 ? _h : 'Internal server error' });
|
|
223
|
-
return [3 /*break*/,
|
|
224
|
-
case
|
|
304
|
+
return [3 /*break*/, 32];
|
|
305
|
+
case 32: return [2 /*return*/];
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}); });
|
|
309
|
+
// ── WebSocket tunnel: WS /api/pods/:id/ws?clientId= ─────────────────────
|
|
310
|
+
this.server.on('upgrade', function (req, socket, head) { return __awaiter(_this, void 0, void 0, function () {
|
|
311
|
+
var rawUrl, urlPath, qs, wsMatch, podId, port_4, clientId_1, upstream_1, err_2;
|
|
312
|
+
var _a, _b, _c;
|
|
313
|
+
return __generator(this, function (_d) {
|
|
314
|
+
switch (_d.label) {
|
|
315
|
+
case 0:
|
|
316
|
+
rawUrl = (_a = req.url) !== null && _a !== void 0 ? _a : '/';
|
|
317
|
+
urlPath = rawUrl.split('?')[0];
|
|
318
|
+
qs = new URLSearchParams((_b = rawUrl.split('?')[1]) !== null && _b !== void 0 ? _b : '');
|
|
319
|
+
wsMatch = urlPath.match(/^\/api\/pods\/([^/]+)\/ws$/);
|
|
320
|
+
if (!wsMatch) {
|
|
321
|
+
socket.destroy();
|
|
322
|
+
return [2 /*return*/];
|
|
323
|
+
}
|
|
324
|
+
podId = decodeURIComponent(wsMatch[1]);
|
|
325
|
+
_d.label = 1;
|
|
326
|
+
case 1:
|
|
327
|
+
_d.trys.push([1, 3, , 4]);
|
|
328
|
+
return [4 /*yield*/, this._runningPort(podId)];
|
|
329
|
+
case 2:
|
|
330
|
+
port_4 = _d.sent();
|
|
331
|
+
clientId_1 = (_c = qs.get('clientId')) !== null && _c !== void 0 ? _c : '';
|
|
332
|
+
upstream_1 = net.createConnection({ host: 'localhost', port: port_4 });
|
|
333
|
+
upstream_1.on('connect', function () {
|
|
334
|
+
var _a, _b;
|
|
335
|
+
var upgradeReq = [
|
|
336
|
+
"GET /ws?clientId=".concat(encodeURIComponent(clientId_1), " HTTP/1.1"),
|
|
337
|
+
"Host: localhost:".concat(port_4),
|
|
338
|
+
'Upgrade: websocket',
|
|
339
|
+
'Connection: Upgrade',
|
|
340
|
+
"Sec-WebSocket-Key: ".concat((_a = req.headers['sec-websocket-key']) !== null && _a !== void 0 ? _a : ''),
|
|
341
|
+
"Sec-WebSocket-Version: ".concat((_b = req.headers['sec-websocket-version']) !== null && _b !== void 0 ? _b : '13'),
|
|
342
|
+
'',
|
|
343
|
+
'',
|
|
344
|
+
].join('\r\n');
|
|
345
|
+
upstream_1.write(upgradeReq);
|
|
346
|
+
if (head && head.length > 0)
|
|
347
|
+
upstream_1.write(head);
|
|
348
|
+
// Bidirectional pipe — all WS frames pass through transparently
|
|
349
|
+
socket.pipe(upstream_1);
|
|
350
|
+
upstream_1.pipe(socket);
|
|
351
|
+
});
|
|
352
|
+
upstream_1.on('error', function () { return socket.destroy(); });
|
|
353
|
+
socket.on('error', function () { return upstream_1.destroy(); });
|
|
354
|
+
socket.on('close', function () { return upstream_1.destroy(); });
|
|
355
|
+
upstream_1.on('close', function () { return socket.destroy(); });
|
|
356
|
+
return [3 /*break*/, 4];
|
|
357
|
+
case 3:
|
|
358
|
+
err_2 = _d.sent();
|
|
359
|
+
console.error('[PodsOperatorServer] WS tunnel error:', err_2);
|
|
360
|
+
socket.write('HTTP/1.1 503 Service Unavailable\r\n\r\n');
|
|
361
|
+
socket.destroy();
|
|
362
|
+
return [3 /*break*/, 4];
|
|
363
|
+
case 4: return [2 /*return*/];
|
|
225
364
|
}
|
|
226
365
|
});
|
|
227
366
|
}); });
|
|
@@ -240,9 +379,26 @@ var PodsOperatorServer = /** @class */ (function () {
|
|
|
240
379
|
};
|
|
241
380
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
242
381
|
PodsOperatorServer.prototype._json = function (res, status, data) {
|
|
243
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
382
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
244
383
|
res.end(JSON.stringify(data));
|
|
245
384
|
};
|
|
385
|
+
/** Pipe a GET response from ComfyUI directly to the client (handles large payloads). */
|
|
386
|
+
PodsOperatorServer.prototype._proxyGet = function (res, port, path) {
|
|
387
|
+
return new Promise(function (resolve, reject) {
|
|
388
|
+
var upstreamReq = http.request({ hostname: 'localhost', port: port, path: path, method: 'GET' }, function (upstreamRes) {
|
|
389
|
+
var _a, _b;
|
|
390
|
+
res.writeHead((_a = upstreamRes.statusCode) !== null && _a !== void 0 ? _a : 200, {
|
|
391
|
+
'Content-Type': (_b = upstreamRes.headers['content-type']) !== null && _b !== void 0 ? _b : 'application/json',
|
|
392
|
+
'Access-Control-Allow-Origin': '*',
|
|
393
|
+
});
|
|
394
|
+
upstreamRes.pipe(res);
|
|
395
|
+
upstreamRes.on('end', resolve);
|
|
396
|
+
upstreamRes.on('error', reject);
|
|
397
|
+
});
|
|
398
|
+
upstreamReq.on('error', reject);
|
|
399
|
+
upstreamReq.end();
|
|
400
|
+
});
|
|
401
|
+
};
|
|
246
402
|
PodsOperatorServer.prototype._readBody = function (req) {
|
|
247
403
|
return new Promise(function (resolve, reject) {
|
|
248
404
|
var chunks = [];
|