flowscale 2.1.0-beta.4 → 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,10 +109,11 @@ 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, body, contentType, upstream, data, executeMatch, podId, body, _a, _b, result, viewMatch, podId, port, params, upstream, imageBuffer, err_1;
104
- var _c, _d, _e, _f, _g, _h, _j;
105
- return __generator(this, function (_k) {
106
- switch (_k.label) {
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;
113
+ var _this = this;
114
+ var _c, _d, _e, _f, _g, _h;
115
+ return __generator(this, function (_j) {
116
+ switch (_j.label) {
107
117
  case 0:
108
118
  res.setHeader('Access-Control-Allow-Origin', '*');
109
119
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -115,92 +125,242 @@ var PodsOperatorServer = /** @class */ (function () {
115
125
  }
116
126
  url = ((_c = req.url) !== null && _c !== void 0 ? _c : '/').split('?')[0];
117
127
  query = new URLSearchParams((_e = ((_d = req.url) !== null && _d !== void 0 ? _d : '').split('?')[1]) !== null && _e !== void 0 ? _e : '');
118
- _k.label = 1;
128
+ _j.label = 1;
119
129
  case 1:
120
- _k.trys.push([1, 18, , 19]);
130
+ _j.trys.push([1, 31, , 32]);
121
131
  if (!(req.method === 'GET' && (url === '/api/pods' || url === '/api/pods/'))) return [3 /*break*/, 3];
122
132
  return [4 /*yield*/, this.transport.list()];
123
133
  case 2:
124
- pods = _k.sent();
134
+ pods = _j.sent();
125
135
  return [2 /*return*/, this._json(res, 200, pods)];
126
136
  case 3:
127
137
  podMatch = url.match(/^\/api\/pods\/([^/]+)$/);
128
138
  if (!(req.method === 'GET' && podMatch)) return [3 /*break*/, 5];
129
139
  return [4 /*yield*/, this.transport.get(decodeURIComponent(podMatch[1]))];
130
140
  case 4:
131
- pod = _k.sent();
141
+ pod = _j.sent();
132
142
  if (!pod)
133
143
  return [2 /*return*/, this._json(res, 404, { error: 'Pod not found' })];
134
144
  return [2 /*return*/, this._json(res, 200, pod)];
135
145
  case 5:
136
146
  uploadMatch = url.match(/^\/api\/pods\/([^/]+)\/upload$/);
137
- if (!(req.method === 'POST' && uploadMatch)) return [3 /*break*/, 10];
147
+ if (!(req.method === 'POST' && uploadMatch)) return [3 /*break*/, 8];
138
148
  podId = decodeURIComponent(uploadMatch[1]);
139
149
  return [4 /*yield*/, this._runningPort(podId)];
140
150
  case 6:
141
- port = _k.sent();
142
- return [4 /*yield*/, this._readBody(req)];
143
- case 7:
144
- body = _k.sent();
145
- contentType = (_f = req.headers['content-type']) !== null && _f !== void 0 ? _f : 'application/octet-stream';
146
- return [4 /*yield*/, fetch("http://localhost:".concat(port, "/upload/image"), {
147
- method: 'POST',
148
- headers: { 'content-type': contentType },
149
- body: body,
151
+ port_1 = _j.sent();
152
+ return [4 /*yield*/, new Promise(function (resolve, reject) {
153
+ var _a;
154
+ var headers = {
155
+ 'content-type': (_a = req.headers['content-type']) !== null && _a !== void 0 ? _a : 'application/octet-stream',
156
+ };
157
+ if (req.headers['content-length'])
158
+ headers['content-length'] = req.headers['content-length'];
159
+ if (req.headers['transfer-encoding'])
160
+ headers['transfer-encoding'] = req.headers['transfer-encoding'];
161
+ var upstreamReq = http.request({ hostname: 'localhost', port: port_1, path: '/upload/image', method: 'POST', headers: headers }, function (upstreamRes) {
162
+ var chunks = [];
163
+ upstreamRes.on('data', function (chunk) { return chunks.push(chunk); });
164
+ upstreamRes.on('end', function () {
165
+ var _a;
166
+ try {
167
+ var data = JSON.parse(Buffer.concat(chunks).toString());
168
+ _this._json(res, (_a = upstreamRes.statusCode) !== null && _a !== void 0 ? _a : 500, data);
169
+ resolve();
170
+ }
171
+ catch (e) {
172
+ reject(e);
173
+ }
174
+ });
175
+ upstreamRes.on('error', reject);
176
+ });
177
+ upstreamReq.on('error', reject);
178
+ req.pipe(upstreamReq);
150
179
  })];
180
+ case 7:
181
+ _j.sent();
182
+ return [2 /*return*/];
151
183
  case 8:
152
- upstream = _k.sent();
153
- return [4 /*yield*/, upstream.json()];
154
- case 9:
155
- data = _k.sent();
156
- return [2 /*return*/, this._json(res, upstream.status, data)];
157
- case 10:
158
184
  executeMatch = url.match(/^\/api\/pods\/([^/]+)\/execute$/);
159
- if (!(req.method === 'POST' && executeMatch)) return [3 /*break*/, 13];
185
+ if (!(req.method === 'POST' && executeMatch)) return [3 /*break*/, 11];
160
186
  podId = decodeURIComponent(executeMatch[1]);
161
187
  _b = (_a = JSON).parse;
162
188
  return [4 /*yield*/, this._readBody(req)];
163
- case 11:
164
- body = _b.apply(_a, [(_k.sent()).toString()]);
165
- return [4 /*yield*/, this.api.executeWorkflowAndWait(podId, body.workflow, (_g = body.options) !== null && _g !== void 0 ? _g : {})];
166
- case 12:
167
- result = _k.sent();
189
+ case 9:
190
+ body = _b.apply(_a, [(_j.sent()).toString()]);
191
+ return [4 /*yield*/, this.api.executeWorkflowAndWait(podId, body.workflow, (_f = body.options) !== null && _f !== void 0 ? _f : {})];
192
+ case 10:
193
+ result = _j.sent();
168
194
  return [2 /*return*/, this._json(res, 200, result)];
169
- case 13:
195
+ case 11:
170
196
  viewMatch = url.match(/^\/api\/pods\/([^/]+)\/view$/);
171
- if (!(req.method === 'GET' && viewMatch)) return [3 /*break*/, 17];
197
+ if (!(req.method === 'GET' && viewMatch)) return [3 /*break*/, 14];
172
198
  podId = decodeURIComponent(viewMatch[1]);
173
199
  return [4 /*yield*/, this._runningPort(podId)];
200
+ case 12:
201
+ port = _j.sent();
202
+ return [4 /*yield*/, this._proxyGet(res, port, "/view?".concat(query))];
203
+ case 13:
204
+ _j.sent();
205
+ return [2 /*return*/];
174
206
  case 14:
175
- port = _k.sent();
176
- params = new URLSearchParams(query.toString());
177
- return [4 /*yield*/, fetch("http://localhost:".concat(port, "/view?").concat(params))];
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)];
178
211
  case 15:
179
- upstream = _k.sent();
180
- if (!upstream.ok) {
181
- res.writeHead(upstream.status);
182
- res.end();
183
- return [2 /*return*/];
184
- }
185
- return [4 /*yield*/, upstream.arrayBuffer()];
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))];
186
216
  case 16:
187
- imageBuffer = _k.sent();
188
- res.writeHead(200, {
189
- 'Content-Type': (_h = upstream.headers.get('content-type')) !== null && _h !== void 0 ? _h : 'image/png',
190
- 'Cache-Control': 'public, max-age=3600',
191
- });
192
- res.end(Buffer.from(imageBuffer));
217
+ _j.sent();
193
218
  return [2 /*return*/];
194
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:
195
297
  res.writeHead(404);
196
298
  res.end('Not found');
197
- return [3 /*break*/, 19];
198
- case 18:
199
- err_1 = _k.sent();
299
+ return [3 /*break*/, 32];
300
+ case 31:
301
+ err_1 = _j.sent();
200
302
  console.error('[PodsOperatorServer] Error handling request:', err_1);
201
- this._json(res, 500, { error: (_j = err_1 === null || err_1 === void 0 ? void 0 : err_1.message) !== null && _j !== void 0 ? _j : 'Internal server error' });
202
- return [3 /*break*/, 19];
203
- case 19: return [2 /*return*/];
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' });
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*/];
204
364
  }
205
365
  });
206
366
  }); });
@@ -219,9 +379,26 @@ var PodsOperatorServer = /** @class */ (function () {
219
379
  };
220
380
  // ── Private helpers ──────────────────────────────────────────────────────
221
381
  PodsOperatorServer.prototype._json = function (res, status, data) {
222
- res.writeHead(status, { 'Content-Type': 'application/json' });
382
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
223
383
  res.end(JSON.stringify(data));
224
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
+ };
225
402
  PodsOperatorServer.prototype._readBody = function (req) {
226
403
  return new Promise(function (resolve, reject) {
227
404
  var chunks = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowscale",
3
- "version": "2.1.0-beta.4",
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",