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.
@@ -1,15 +1,23 @@
1
1
  import type { NodePodsTransport } from './node-transport';
2
2
  /**
3
- * PodsOperatorServer — exposes a pod management and workflow execution HTTP API
4
- * so that remote FlowScale apps (e.g. AI OS) can discover pods and run workflows
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 — list all pods
9
- * GET /api/pods/:id — get a single pod
10
- * POST /api/pods/:id/upload — upload a file to the pod's ComfyUI instance
11
- * POST /api/pods/:id/execute — execute a workflow and wait for outputs
12
- * GET /api/pods/:id/view?filename= — proxy a ComfyUI output image
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;
@@ -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 pod management and workflow execution HTTP API
77
- * so that remote FlowScale apps (e.g. AI OS) can discover pods and run workflows
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 — list all pods
82
- * GET /api/pods/:id — get a single pod
83
- * POST /api/pods/:id/upload — upload a file to the pod's ComfyUI instance
84
- * POST /api/pods/:id/execute — execute a workflow and wait for outputs
85
- * GET /api/pods/:id/view?filename= — proxy a ComfyUI output image
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, params, upstream, imageBuffer, err_1;
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, 16, , 17]);
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*/, 15];
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
- params = new URLSearchParams(query.toString());
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
- upstream = _j.sent();
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*/, 17];
219
- case 16:
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*/, 17];
224
- case 17: return [2 /*return*/];
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 = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowscale",
3
- "version": "2.1.0-beta.5",
3
+ "version": "2.1.0-beta.6",
4
4
  "description": "An NPM library for communicating with the Flowscale APIs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",