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.
- package/dist/operator-server.d.ts +16 -13
- package/dist/operator-server.js +148 -51
- package/package.json +1 -1
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import type { NodePodsTransport } from './node-transport';
|
|
2
2
|
/**
|
|
3
|
-
* PodsOperatorServer — exposes a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
}
|
package/dist/operator-server.js
CHANGED
|
@@ -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
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
-
*
|
|
88
|
-
*
|
|
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,
|
|
104
|
-
var
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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 =
|
|
130
|
-
|
|
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
|
-
|
|
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 =
|
|
140
|
-
if (!pod)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
215
|
+
case 15:
|
|
150
216
|
res.writeHead(404);
|
|
151
217
|
res.end('Not found');
|
|
152
|
-
return [3 /*break*/,
|
|
153
|
-
case
|
|
154
|
-
err_1 =
|
|
218
|
+
return [3 /*break*/, 17];
|
|
219
|
+
case 16:
|
|
220
|
+
err_1 = _j.sent();
|
|
155
221
|
console.error('[PodsOperatorServer] Error handling request:', err_1);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|