flowscale 2.1.0-beta.3 → 2.1.0-beta.5

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,26 +1,29 @@
1
1
  import type { NodePodsTransport } from './node-transport';
2
2
  /**
3
- * PodsOperatorServer — exposes a read-only pod list HTTP API so that remote
4
- * FlowScale Studio instances on the same network can discover and use pods
5
- * managed by this machine.
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.
6
6
  *
7
- * Serves on all interfaces (0.0.0.0) so any machine on the LAN can reach it.
8
- * Includes CORS headers for browser-based clients.
9
- *
10
- * Usage in creativeflow-electron (main process):
11
- * import { NodePodsTransport, PodsOperatorServer } from 'flowscale/node'
12
- * const server = new PodsOperatorServer(transport) // default port 3001
13
- * server.start()
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
14
13
  *
15
- * Remote clients connect via:
16
- * import { RemotePodsTransport, LocalPodsAPI } from 'flowscale'
17
- * const api = new LocalPodsAPI(new RemotePodsTransport('http://192.168.1.100:3001'))
14
+ * Serves on 0.0.0.0 so any machine on the LAN can reach it.
15
+ * Includes CORS headers for browser-based clients.
18
16
  */
19
17
  export declare class PodsOperatorServer {
20
18
  private server;
21
19
  private readonly transport;
20
+ private readonly api;
22
21
  private readonly port;
23
22
  constructor(transport: NodePodsTransport, port?: number);
24
23
  start(): void;
25
24
  stop(): void;
25
+ private _json;
26
+ private _readBody;
27
+ /** Returns the port of the first running instance in the given pod. */
28
+ private _runningPort;
26
29
  }
@@ -71,28 +71,28 @@ 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 local_pods_1 = require("./local-pods");
74
75
  /**
75
- * PodsOperatorServer — exposes a read-only pod list HTTP API so that remote
76
- * FlowScale Studio instances on the same network can discover and use pods
77
- * managed by this machine.
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.
78
79
  *
79
- * Serves on all interfaces (0.0.0.0) so any machine on the LAN can reach it.
80
- * Includes CORS headers for browser-based clients.
81
- *
82
- * Usage in creativeflow-electron (main process):
83
- * import { NodePodsTransport, PodsOperatorServer } from 'flowscale/node'
84
- * const server = new PodsOperatorServer(transport) // default port 3001
85
- * server.start()
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
86
86
  *
87
- * Remote clients connect via:
88
- * import { RemotePodsTransport, LocalPodsAPI } from 'flowscale'
89
- * const api = new LocalPodsAPI(new RemotePodsTransport('http://192.168.1.100:3001'))
87
+ * Serves on 0.0.0.0 so any machine on the LAN can reach it.
88
+ * Includes CORS headers for browser-based clients.
90
89
  */
91
90
  var PodsOperatorServer = /** @class */ (function () {
92
91
  function PodsOperatorServer(transport, port) {
93
92
  if (port === void 0) { port = 3001; }
94
93
  this.server = null;
95
94
  this.transport = transport;
95
+ this.api = new local_pods_1.LocalPodsAPI(transport);
96
96
  this.port = port;
97
97
  }
98
98
  PodsOperatorServer.prototype.start = function () {
@@ -100,63 +100,128 @@ var PodsOperatorServer = /** @class */ (function () {
100
100
  if (this.server)
101
101
  return;
102
102
  this.server = http.createServer(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
103
- var url, pods, podMatch, id, pod, err_1;
104
- var _a;
105
- return __generator(this, function (_b) {
106
- switch (_b.label) {
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;
104
+ var _this = this;
105
+ var _c, _d, _e, _f, _g, _h;
106
+ return __generator(this, function (_j) {
107
+ switch (_j.label) {
107
108
  case 0:
108
- // CORS — allow any origin (LAN usage only, not a public service)
109
109
  res.setHeader('Access-Control-Allow-Origin', '*');
110
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
110
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
111
111
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
112
112
  if (req.method === 'OPTIONS') {
113
113
  res.writeHead(204);
114
114
  res.end();
115
115
  return [2 /*return*/];
116
116
  }
117
- if (req.method !== 'GET') {
118
- res.writeHead(405);
119
- res.end('Method Not Allowed');
120
- return [2 /*return*/];
121
- }
122
- url = ((_a = req.url) !== null && _a !== void 0 ? _a : '/').split('?')[0];
123
- _b.label = 1;
117
+ url = ((_c = req.url) !== null && _c !== void 0 ? _c : '/').split('?')[0];
118
+ query = new URLSearchParams((_e = ((_d = req.url) !== null && _d !== void 0 ? _d : '').split('?')[1]) !== null && _e !== void 0 ? _e : '');
119
+ _j.label = 1;
124
120
  case 1:
125
- _b.trys.push([1, 6, , 7]);
126
- if (!(url === '/api/pods' || url === '/api/pods/')) return [3 /*break*/, 3];
121
+ _j.trys.push([1, 16, , 17]);
122
+ if (!(req.method === 'GET' && (url === '/api/pods' || url === '/api/pods/'))) return [3 /*break*/, 3];
127
123
  return [4 /*yield*/, this.transport.list()];
128
124
  case 2:
129
- pods = _b.sent();
130
- res.writeHead(200, { 'Content-Type': 'application/json' });
131
- res.end(JSON.stringify(pods));
132
- return [2 /*return*/];
125
+ pods = _j.sent();
126
+ return [2 /*return*/, this._json(res, 200, pods)];
133
127
  case 3:
134
128
  podMatch = url.match(/^\/api\/pods\/([^/]+)$/);
135
- if (!podMatch) return [3 /*break*/, 5];
136
- id = decodeURIComponent(podMatch[1]);
137
- return [4 /*yield*/, this.transport.get(id)];
129
+ if (!(req.method === 'GET' && podMatch)) return [3 /*break*/, 5];
130
+ return [4 /*yield*/, this.transport.get(decodeURIComponent(podMatch[1]))];
138
131
  case 4:
139
- pod = _b.sent();
140
- if (!pod) {
141
- res.writeHead(404, { 'Content-Type': 'application/json' });
142
- res.end(JSON.stringify({ error: 'Pod not found' }));
143
- }
144
- else {
145
- res.writeHead(200, { 'Content-Type': 'application/json' });
146
- res.end(JSON.stringify(pod));
132
+ pod = _j.sent();
133
+ if (!pod)
134
+ return [2 /*return*/, this._json(res, 404, { error: 'Pod not found' })];
135
+ return [2 /*return*/, this._json(res, 200, pod)];
136
+ case 5:
137
+ uploadMatch = url.match(/^\/api\/pods\/([^/]+)\/upload$/);
138
+ if (!(req.method === 'POST' && uploadMatch)) return [3 /*break*/, 8];
139
+ 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
+ ];
143
+ case 6:
144
+ port_1 = _j.sent();
145
+ // Pipe the multipart body directly to ComfyUI to preserve boundary and encoding
146
+ return [4 /*yield*/, new Promise(function (resolve, reject) {
147
+ var _a;
148
+ var headers = {
149
+ 'content-type': (_a = req.headers['content-type']) !== null && _a !== void 0 ? _a : 'application/octet-stream',
150
+ };
151
+ if (req.headers['content-length'])
152
+ headers['content-length'] = req.headers['content-length'];
153
+ if (req.headers['transfer-encoding'])
154
+ headers['transfer-encoding'] = req.headers['transfer-encoding'];
155
+ var upstreamReq = http.request({ hostname: 'localhost', port: port_1, path: '/upload/image', method: 'POST', headers: headers }, function (upstreamRes) {
156
+ var chunks = [];
157
+ upstreamRes.on('data', function (chunk) { return chunks.push(chunk); });
158
+ upstreamRes.on('end', function () {
159
+ var _a;
160
+ try {
161
+ var data = JSON.parse(Buffer.concat(chunks).toString());
162
+ _this._json(res, (_a = upstreamRes.statusCode) !== null && _a !== void 0 ? _a : 500, data);
163
+ resolve();
164
+ }
165
+ catch (e) {
166
+ reject(e);
167
+ }
168
+ });
169
+ upstreamRes.on('error', reject);
170
+ });
171
+ upstreamReq.on('error', reject);
172
+ req.pipe(upstreamReq);
173
+ })];
174
+ case 7:
175
+ // Pipe the multipart body directly to ComfyUI to preserve boundary and encoding
176
+ _j.sent();
177
+ return [2 /*return*/];
178
+ case 8:
179
+ executeMatch = url.match(/^\/api\/pods\/([^/]+)\/execute$/);
180
+ if (!(req.method === 'POST' && executeMatch)) return [3 /*break*/, 11];
181
+ podId = decodeURIComponent(executeMatch[1]);
182
+ _b = (_a = JSON).parse;
183
+ return [4 /*yield*/, this._readBody(req)];
184
+ case 9:
185
+ body = _b.apply(_a, [(_j.sent()).toString()]);
186
+ return [4 /*yield*/, this.api.executeWorkflowAndWait(podId, body.workflow, (_f = body.options) !== null && _f !== void 0 ? _f : {})];
187
+ case 10:
188
+ result = _j.sent();
189
+ return [2 /*return*/, this._json(res, 200, result)];
190
+ case 11:
191
+ viewMatch = url.match(/^\/api\/pods\/([^/]+)\/view$/);
192
+ if (!(req.method === 'GET' && viewMatch)) return [3 /*break*/, 15];
193
+ podId = decodeURIComponent(viewMatch[1]);
194
+ return [4 /*yield*/, this._runningPort(podId)];
195
+ case 12:
196
+ port = _j.sent();
197
+ params = new URLSearchParams(query.toString());
198
+ return [4 /*yield*/, fetch("http://localhost:".concat(port, "/view?").concat(params))];
199
+ case 13:
200
+ upstream = _j.sent();
201
+ if (!upstream.ok) {
202
+ res.writeHead(upstream.status);
203
+ res.end();
204
+ return [2 /*return*/];
147
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));
148
214
  return [2 /*return*/];
149
- case 5:
215
+ case 15:
150
216
  res.writeHead(404);
151
217
  res.end('Not found');
152
- return [3 /*break*/, 7];
153
- case 6:
154
- err_1 = _b.sent();
218
+ return [3 /*break*/, 17];
219
+ case 16:
220
+ err_1 = _j.sent();
155
221
  console.error('[PodsOperatorServer] Error handling request:', err_1);
156
- res.writeHead(500, { 'Content-Type': 'application/json' });
157
- res.end(JSON.stringify({ error: 'Internal server error' }));
158
- return [3 /*break*/, 7];
159
- case 7: return [2 /*return*/];
222
+ 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*/];
160
225
  }
161
226
  });
162
227
  }); });
@@ -173,6 +238,38 @@ var PodsOperatorServer = /** @class */ (function () {
173
238
  this.server = null;
174
239
  }
175
240
  };
241
+ // ── Private helpers ──────────────────────────────────────────────────────
242
+ PodsOperatorServer.prototype._json = function (res, status, data) {
243
+ res.writeHead(status, { 'Content-Type': 'application/json' });
244
+ res.end(JSON.stringify(data));
245
+ };
246
+ PodsOperatorServer.prototype._readBody = function (req) {
247
+ return new Promise(function (resolve, reject) {
248
+ var chunks = [];
249
+ req.on('data', function (chunk) { return chunks.push(chunk); });
250
+ req.on('end', function () { return resolve(Buffer.concat(chunks)); });
251
+ req.on('error', reject);
252
+ });
253
+ };
254
+ /** Returns the port of the first running instance in the given pod. */
255
+ PodsOperatorServer.prototype._runningPort = function (podId) {
256
+ return __awaiter(this, void 0, void 0, function () {
257
+ var pod, running;
258
+ return __generator(this, function (_a) {
259
+ switch (_a.label) {
260
+ case 0: return [4 /*yield*/, this.transport.get(podId)];
261
+ case 1:
262
+ pod = _a.sent();
263
+ if (!pod)
264
+ throw new Error("Pod \"".concat(podId, "\" not found"));
265
+ running = pod.instances.find(function (i) { return i.status === 'running'; });
266
+ if (!running)
267
+ throw new Error("No running instance in pod \"".concat(pod.name, "\""));
268
+ return [2 /*return*/, running.port];
269
+ }
270
+ });
271
+ });
272
+ };
176
273
  return PodsOperatorServer;
177
274
  }());
178
275
  exports.PodsOperatorServer = PodsOperatorServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowscale",
3
- "version": "2.1.0-beta.3",
3
+ "version": "2.1.0-beta.5",
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",