Gendle commited on
Commit
07df96b
·
verified ·
1 Parent(s): 6916105

Upload image-proxy.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. image-proxy.js +293 -293
image-proxy.js CHANGED
@@ -1,293 +1,293 @@
1
- /**
2
- * Image Proxy + BFF Reverse Proxy
3
- *
4
- * 在 BFF (hermes-web-ui) 前端增加一层轻量代理:
5
- * - /images/ → 列出所有已生成图片 (HTML 页面)
6
- * - /images/<file> → 直接下载/预览图片
7
- * - 其他所有请求 → 透传给 BFF (含 WebSocket)
8
- *
9
- * 端口: 7860 (HF Spaces 对外端口)
10
- * BFF: 7861 (内部端口, 仅本代理访问)
11
- * 图片目录: /data/.hermes/image_cache (主目录)
12
- * /data/cover-image (baoyu-cover-image 输出目录)
13
- */
14
-
15
- const http = require('http');
16
- const fs = require('fs');
17
- const path = require('path');
18
- const net = require('net');
19
-
20
- const BFF_HOST = '127.0.0.1';
21
- const BFF_PORT = parseInt(process.env.BFF_PORT || '7861', 10);
22
- const LISTEN_PORT = parseInt(process.env.LISTEN_PORT || '7860', 10);
23
- const IMAGE_DIR = process.env.IMAGE_DIR || '/data/.hermes/image_cache';
24
-
25
- // 额外的图片搜索路径(baoyu-cover-image 等技能的输出目录)
26
- const EXTRA_IMAGE_DIRS = [
27
- '/data/cover-image',
28
- '/data/.hermes/image_cache',
29
- ];
30
-
31
- const MIME_TYPES = {
32
- '.png': 'image/png',
33
- '.jpg': 'image/jpeg',
34
- '.jpeg': 'image/jpeg',
35
- '.gif': 'image/gif',
36
- '.webp': 'image/webp',
37
- '.svg': 'image/svg+xml',
38
- '.txt': 'text/plain',
39
- '.json': 'application/json',
40
- '.md': 'text/markdown',
41
- };
42
-
43
- // ==================== 图片文件服务 ====================
44
-
45
- function serveImageList(res) {
46
- const allImageFiles = [];
47
- let dirsScanned = 0;
48
- const totalDirs = EXTRA_IMAGE_DIRS.length;
49
-
50
- function checkComplete() {
51
- dirsScanned++;
52
- if (dirsScanned < totalDirs) return;
53
-
54
- const uniqueFiles = Array.from(new Map(allImageFiles.map(f => [f.path, f])).values());
55
- uniqueFiles.sort((a, b) => b.mtime - a.mtime);
56
-
57
- const html = buildImageListHtml(uniqueFiles);
58
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
59
- res.end(html);
60
- }
61
-
62
- EXTRA_IMAGE_DIRS.forEach(dir => {
63
- function scanDir(currentDir, relativePath, callback) {
64
- fs.readdir(currentDir, { withFileTypes: true }, (err, entries) => {
65
- if (err) {
66
- callback();
67
- return;
68
- }
69
-
70
- let pending = entries.length;
71
- if (pending === 0) {
72
- callback();
73
- return;
74
- }
75
-
76
- entries.forEach(entry => {
77
- const fullPath = path.join(currentDir, entry.name);
78
- const relPath = path.join(relativePath, entry.name);
79
-
80
- if (entry.isDirectory()) {
81
- scanDir(fullPath, relPath, () => {
82
- pending--;
83
- if (pending === 0) callback();
84
- });
85
- } else if (/\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(entry.name)) {
86
- try {
87
- const stat = fs.statSync(fullPath);
88
- allImageFiles.push({
89
- name: entry.name,
90
- path: fullPath,
91
- relPath: relPath,
92
- dir: dir,
93
- size: stat.size,
94
- mtime: stat.mtime
95
- });
96
- } catch (e) {}
97
- pending--;
98
- if (pending === 0) callback();
99
- } else {
100
- pending--;
101
- if (pending === 0) callback();
102
- }
103
- });
104
- });
105
- }
106
-
107
- scanDir(dir, '', () => {
108
- checkComplete();
109
- });
110
- });
111
- }
112
-
113
- function buildImageListHtml(imageFiles) {
114
- const html = `<!DOCTYPE html>
115
- <html lang="zh-CN">
116
- <head>
117
- <meta charset="utf-8">
118
- <meta name="viewport" content="width=device-width, initial-scale=1">
119
- <title>🖼️ Image Cache - Hermes Agent</title>
120
- <style>
121
- * { box-sizing: border-box; margin: 0; padding: 0; }
122
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
123
- background: #0f0f23; color: #e0e0e0; padding: 2em; }
124
- h1 { color: #7eb8da; margin-bottom: 1em; font-size: 1.5em; }
125
- .card { background: #1a1a3e; border-radius: 12px; padding: 1.5em;
126
- margin-bottom: 1.5em; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
127
- .card h3 { color: #9dd6e8; margin-bottom: 0.8em; font-size: 1.1em; }
128
- .card img { max-width: 100%; border-radius: 8px; cursor: pointer;
129
- transition: transform .2s; }
130
- .card img:hover { transform: scale(1.02); }
131
- .actions { margin-top: 0.8em; display: flex; gap: 1em; flex-wrap: wrap; }
132
- .actions a { color: #7eb8da; text-decoration: none; padding: 0.4em 1em;
133
- border: 1px solid #7eb8da; border-radius: 6px; font-size: 0.9em;
134
- transition: background .2s; }
135
- .actions a:hover { background: #7eb8da22; }
136
- .meta { color: #888; font-size: 0.85em; margin-top: 0.5em; }
137
- .path { color: #666; font-size: 0.8em; margin-top: 0.3em; }
138
- .empty { text-align: center; padding: 3em; color: #888; }
139
- .empty p { margin-top: 1em; font-size: 0.95em; }
140
- </style>
141
- </head>
142
- <body>
143
- <h1>🖼️ Image Cache</h1>
144
- ${
145
- imageFiles.length === 0
146
- ? `<div class="empty"><p style="font-size:2em">📭</p><p>暂无图片���让 agent 生成图片后将自动出现在此。</p>
147
- <p>提示: 让 agent 使用 baoyu-imagine 技能,并将图片保存到 /data/.hermes/image_cache/</p></div>`
148
- : imageFiles
149
- .map((f) => {
150
- const sizeMB = (f.size / 1024 / 1024).toFixed(2);
151
- const mtime = f.mtime.toISOString().replace('T', ' ').slice(0, 19);
152
- return `<div class="card">
153
- <h3>${f.name}</h3>
154
- <div class="path">${f.relPath}</div>
155
- <img src="/images/${encodeURIComponent(f.relPath)}" alt="${f.name}" loading="lazy" />
156
- <div class="meta">${sizeMB} MB · ${mtime}</div>
157
- <div class="actions">
158
- <a href="/images/${encodeURIComponent(f.relPath)}" download="${f.name}">⬇️ 下载</a>
159
- <a href="/images/${encodeURIComponent(f.relPath)}" target="_blank">🔍 原始大小</a>
160
- </div>
161
- </div>`;
162
- })
163
- .join('\n')
164
- }
165
- </body></html>`;
166
- return html;
167
- }
168
-
169
- function serveImage(urlPath, res) {
170
- const relativePath = decodeURIComponent(urlPath.slice('/images/'.length));
171
-
172
- // Try each image directory in order
173
- function tryDir(index) {
174
- if (index >= EXTRA_IMAGE_DIRS.length) {
175
- res.writeHead(404, { 'Content-Type': 'text/plain' });
176
- res.end('Not found');
177
- return;
178
- }
179
-
180
- const dir = EXTRA_IMAGE_DIRS[index];
181
- const filePath = path.join(dir, relativePath);
182
- const resolved = path.resolve(filePath);
183
-
184
- // Security: prevent directory traversal
185
- const imageRoot = path.resolve(dir);
186
- if (!resolved.startsWith(imageRoot + path.sep) && resolved !== imageRoot) {
187
- tryDir(index + 1);
188
- return;
189
- }
190
-
191
- fs.stat(resolved, (err, stat) => {
192
- if (err || !stat.isFile()) {
193
- tryDir(index + 1);
194
- return;
195
- }
196
-
197
- const ext = path.extname(resolved).toLowerCase();
198
- const contentType = MIME_TYPES[ext] || 'application/octet-stream';
199
-
200
- res.writeHead(200, {
201
- 'Content-Type': contentType,
202
- 'Content-Length': stat.size,
203
- 'Cache-Control': 'public, max-age=3600',
204
- 'Content-Disposition': `inline; filename="${path.basename(resolved)}"`,
205
- });
206
- fs.createReadStream(resolved).pipe(res);
207
- });
208
- }
209
-
210
- tryDir(0);
211
- }
212
-
213
- // ==================== HTTP 反向代理 ====================
214
-
215
- function proxyHttpRequest(clientReq, clientRes) {
216
- const options = {
217
- hostname: BFF_HOST,
218
- port: BFF_PORT,
219
- path: clientReq.url,
220
- method: clientReq.method,
221
- headers: { ...clientReq.headers, host: `${BFF_HOST}:${BFF_PORT}` },
222
- };
223
-
224
- const bffReq = http.request(options, (bffRes) => {
225
- clientRes.writeHead(bffRes.statusCode, bffRes.headers);
226
- bffRes.pipe(clientRes, { end: true });
227
- });
228
-
229
- bffReq.on('error', () => {
230
- if (!clientRes.headersSent) {
231
- clientRes.writeHead(502, { 'Content-Type': 'text/plain' });
232
- clientRes.end('Bad Gateway: BFF server unavailable');
233
- }
234
- });
235
-
236
- clientReq.pipe(bffReq, { end: true });
237
- }
238
-
239
- // ==================== WebSocket 反向代理 ====================
240
-
241
- function proxyWebSocket(clientReq, clientSocket, clientHead) {
242
- const bffSocket = net.connect(BFF_PORT, BFF_HOST, () => {
243
- // 重新构造原始 HTTP Upgrade 请求发给 BFF
244
- let rawRequest = `${clientReq.method} ${clientReq.url} HTTP/${clientReq.httpVersion}\r\n`;
245
- for (let i = 0; i < clientReq.rawHeaders.length; i += 2) {
246
- rawRequest += `${clientReq.rawHeaders[i]}: ${clientReq.rawHeaders[i + 1]}\r\n`;
247
- }
248
- rawRequest += '\r\n';
249
-
250
- bffSocket.write(rawRequest);
251
- if (clientHead && clientHead.length) {
252
- bffSocket.write(clientHead);
253
- }
254
-
255
- // 双向管道: BFF ↔ Client
256
- bffSocket.pipe(clientSocket);
257
- clientSocket.pipe(bffSocket);
258
- });
259
-
260
- const cleanup = () => {
261
- try { bffSocket.destroy(); } catch (_) {}
262
- try { clientSocket.destroy(); } catch (_) {}
263
- };
264
-
265
- bffSocket.on('error', cleanup);
266
- clientSocket.on('error', cleanup);
267
- clientSocket.on('close', () => { try { bffSocket.end(); } catch (_) {} });
268
- bffSocket.on('close', () => { try { clientSocket.end(); } catch (_) {} });
269
- }
270
-
271
- // ==================== 主服务器 ====================
272
-
273
- const server = http.createServer((clientReq, clientRes) => {
274
- // 图片文件服务
275
- if (clientReq.url === '/images' || clientReq.url === '/images/') {
276
- return serveImageList(clientRes);
277
- }
278
- if (clientReq.url.startsWith('/images/')) {
279
- return serveImage(clientReq.url, clientRes);
280
- }
281
-
282
- // 其他请求透传给 BFF
283
- proxyHttpRequest(clientReq, clientRes);
284
- });
285
-
286
- // WebSocket 透传
287
- server.on('upgrade', proxyWebSocket);
288
-
289
- server.listen(LISTEN_PORT, () => {
290
- console.log(`🖼️ Image proxy listening on :${LISTEN_PORT}`);
291
- console.log(`📷 Images: http://localhost:${LISTEN_PORT}/images/`);
292
- console.log(`tunnel: http://${BFF_HOST}:${BFF_PORT}`);
293
- });
 
1
+ /**
2
+ * Image Proxy + BFF Reverse Proxy
3
+ *
4
+ * 在 BFF (hermes-web-ui) 前端增加一层轻量代理:
5
+ * - /images/ → 列出所有已生成图片 (HTML 页面)
6
+ * - /images/<file> → 直接下载/预览图片
7
+ * - 其他所有请求 → 透传给 BFF (含 WebSocket)
8
+ *
9
+ * 端口: 7860 (HF Spaces 对外端口)
10
+ * BFF: 7861 (内部端口, 仅本代理访问)
11
+ * 图片目录: /data/.hermes/image_cache (主目录)
12
+ * /data/cover-image (baoyu-cover-image 输出目录)
13
+ */
14
+
15
+ const http = require('http');
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const net = require('net');
19
+
20
+ const BFF_HOST = '127.0.0.1';
21
+ const BFF_PORT = parseInt(process.env.BFF_PORT || '7861', 10);
22
+ const LISTEN_PORT = parseInt(process.env.LISTEN_PORT || '7860', 10);
23
+ const IMAGE_DIR = process.env.IMAGE_DIR || '/data/.hermes/image_cache';
24
+
25
+ // 额外的图片搜索路径(baoyu-cover-image 等技能的输出目录)
26
+ const EXTRA_IMAGE_DIRS = [
27
+ '/data/cover-image',
28
+ '/data/.hermes/image_cache',
29
+ ];
30
+
31
+ const MIME_TYPES = {
32
+ '.png': 'image/png',
33
+ '.jpg': 'image/jpeg',
34
+ '.jpeg': 'image/jpeg',
35
+ '.gif': 'image/gif',
36
+ '.webp': 'image/webp',
37
+ '.svg': 'image/svg+xml',
38
+ '.txt': 'text/plain',
39
+ '.json': 'application/json',
40
+ '.md': 'text/markdown',
41
+ };
42
+
43
+ // ==================== 图片文件服务 ====================
44
+
45
+ function serveImageList(res) {
46
+ const allImageFiles = [];
47
+ let dirsScanned = 0;
48
+ const totalDirs = EXTRA_IMAGE_DIRS.length;
49
+
50
+ function checkComplete() {
51
+ dirsScanned++;
52
+ if (dirsScanned < totalDirs) return;
53
+
54
+ const uniqueFiles = Array.from(new Map(allImageFiles.map(f => [f.path, f])).values());
55
+ uniqueFiles.sort((a, b) => b.mtime - a.mtime);
56
+
57
+ const html = buildImageListHtml(uniqueFiles);
58
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
59
+ res.end(html);
60
+ }
61
+
62
+ EXTRA_IMAGE_DIRS.forEach(dir => {
63
+ function scanDir(currentDir, relativePath, callback) {
64
+ fs.readdir(currentDir, { withFileTypes: true }, (err, entries) => {
65
+ if (err) {
66
+ callback();
67
+ return;
68
+ }
69
+
70
+ let pending = entries.length;
71
+ if (pending === 0) {
72
+ callback();
73
+ return;
74
+ }
75
+
76
+ entries.forEach(entry => {
77
+ const fullPath = path.join(currentDir, entry.name);
78
+ const relPath = path.join(relativePath, entry.name);
79
+
80
+ if (entry.isDirectory()) {
81
+ scanDir(fullPath, relPath, () => {
82
+ pending--;
83
+ if (pending === 0) callback();
84
+ });
85
+ } else if (/\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(entry.name)) {
86
+ try {
87
+ const stat = fs.statSync(fullPath);
88
+ allImageFiles.push({
89
+ name: entry.name,
90
+ path: fullPath,
91
+ relPath: relPath,
92
+ dir: dir,
93
+ size: stat.size,
94
+ mtime: stat.mtime
95
+ });
96
+ } catch (e) {}
97
+ pending--;
98
+ if (pending === 0) callback();
99
+ } else {
100
+ pending--;
101
+ if (pending === 0) callback();
102
+ }
103
+ });
104
+ });
105
+ }
106
+
107
+ scanDir(dir, '', () => {
108
+ checkComplete();
109
+ });
110
+ });
111
+ }
112
+
113
+ function buildImageListHtml(imageFiles) {
114
+ const html = `<!DOCTYPE html>
115
+ <html lang="zh-CN">
116
+ <head>
117
+ <meta charset="utf-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1">
119
+ <title>🖼️ Image Cache - Hermes Agent</title>
120
+ <style>
121
+ * { box-sizing: border-box; margin: 0; padding: 0; }
122
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
123
+ background: #0f0f23; color: #e0e0e0; padding: 2em; }
124
+ h1 { color: #7eb8da; margin-bottom: 1em; font-size: 1.5em; }
125
+ .card { background: #1a1a3e; border-radius: 12px; padding: 1.5em;
126
+ margin-bottom: 1.5em; box-shadow: 0 4px 12px rgba(0,0,0,.3); }
127
+ .card h3 { color: #9dd6e8; margin-bottom: 0.8em; font-size: 1.1em; }
128
+ .card img { max-width: 100%; border-radius: 8px; cursor: pointer;
129
+ transition: transform .2s; }
130
+ .card img:hover { transform: scale(1.02); }
131
+ .actions { margin-top: 0.8em; display: flex; gap: 1em; flex-wrap: wrap; }
132
+ .actions a { color: #7eb8da; text-decoration: none; padding: 0.4em 1em;
133
+ border: 1px solid #7eb8da; border-radius: 6px; font-size: 0.9em;
134
+ transition: background .2s; }
135
+ .actions a:hover { background: #7eb8da22; }
136
+ .meta { color: #888; font-size: 0.85em; margin-top: 0.5em; }
137
+ .path { color: #666; font-size: 0.8em; margin-top: 0.3em; }
138
+ .empty { text-align: center; padding: 3em; color: #888; }
139
+ .empty p { margin-top: 1em; font-size: 0.95em; }
140
+ </style>
141
+ </head>
142
+ <body>
143
+ <h1>🖼️ Image Cache</h1>
144
+ ${
145
+ imageFiles.length === 0
146
+ ? `<div class="empty"><p style="font-size:2em">📭</p><p>暂无图片让 agent 生成图片后将自动出现在此。</p>
147
+ <p>提示: 让 agent 使用 baoyu-imagine 技能,并将图片保存到 /data/.hermes/image_cache/</p></div>`
148
+ : imageFiles
149
+ .map((f) => {
150
+ const sizeMB = (f.size / 1024 / 1024).toFixed(2);
151
+ const mtime = f.mtime.toISOString().replace('T', ' ').slice(0, 19);
152
+ return `<div class="card">
153
+ <h3>${f.name}</h3>
154
+ <div class="path">${f.relPath}</div>
155
+ <img src="/images/${encodeURIComponent(f.relPath)}" alt="${f.name}" loading="lazy" />
156
+ <div class="meta">${sizeMB} MB · ${mtime}</div>
157
+ <div class="actions">
158
+ <a href="/images/${encodeURIComponent(f.relPath)}" download="${f.name}">⬇️ 下载</a>
159
+ <a href="/images/${encodeURIComponent(f.relPath)}" target="_blank">🔍 原始大小</a>
160
+ </div>
161
+ </div>`;
162
+ })
163
+ .join('\n')
164
+ }
165
+ </body></html>`;
166
+ return html;
167
+ }
168
+
169
+ function serveImage(urlPath, res) {
170
+ const relativePath = decodeURIComponent(urlPath.slice('/images/'.length));
171
+
172
+ // Try each image directory in order
173
+ function tryDir(index) {
174
+ if (index >= EXTRA_IMAGE_DIRS.length) {
175
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
176
+ res.end('Not found');
177
+ return;
178
+ }
179
+
180
+ const dir = EXTRA_IMAGE_DIRS[index];
181
+ const filePath = path.join(dir, relativePath);
182
+ const resolved = path.resolve(filePath);
183
+
184
+ // Security: prevent directory traversal
185
+ const imageRoot = path.resolve(dir);
186
+ if (!resolved.startsWith(imageRoot + path.sep) && resolved !== imageRoot) {
187
+ tryDir(index + 1);
188
+ return;
189
+ }
190
+
191
+ fs.stat(resolved, (err, stat) => {
192
+ if (err || !stat.isFile()) {
193
+ tryDir(index + 1);
194
+ return;
195
+ }
196
+
197
+ const ext = path.extname(resolved).toLowerCase();
198
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
199
+
200
+ res.writeHead(200, {
201
+ 'Content-Type': contentType,
202
+ 'Content-Length': stat.size,
203
+ 'Cache-Control': 'public, max-age=3600',
204
+ 'Content-Disposition': `inline; filename="${path.basename(resolved)}"`,
205
+ });
206
+ fs.createReadStream(resolved).pipe(res);
207
+ });
208
+ }
209
+
210
+ tryDir(0);
211
+ }
212
+
213
+ // ==================== HTTP 反向代理 ====================
214
+
215
+ function proxyHttpRequest(clientReq, clientRes) {
216
+ const options = {
217
+ hostname: BFF_HOST,
218
+ port: BFF_PORT,
219
+ path: clientReq.url,
220
+ method: clientReq.method,
221
+ headers: { ...clientReq.headers, host: `${BFF_HOST}:${BFF_PORT}` },
222
+ };
223
+
224
+ const bffReq = http.request(options, (bffRes) => {
225
+ clientRes.writeHead(bffRes.statusCode, bffRes.headers);
226
+ bffRes.pipe(clientRes, { end: true });
227
+ });
228
+
229
+ bffReq.on('error', () => {
230
+ if (!clientRes.headersSent) {
231
+ clientRes.writeHead(502, { 'Content-Type': 'text/plain' });
232
+ clientRes.end('Bad Gateway: BFF server unavailable');
233
+ }
234
+ });
235
+
236
+ clientReq.pipe(bffReq, { end: true });
237
+ }
238
+
239
+ // ==================== WebSocket 反向代理 ====================
240
+
241
+ function proxyWebSocket(clientReq, clientSocket, clientHead) {
242
+ const bffSocket = net.connect(BFF_PORT, BFF_HOST, () => {
243
+ // 重新构造原始 HTTP Upgrade 请求发给 BFF
244
+ let rawRequest = `${clientReq.method} ${clientReq.url} HTTP/${clientReq.httpVersion}\r\n`;
245
+ for (let i = 0; i < clientReq.rawHeaders.length; i += 2) {
246
+ rawRequest += `${clientReq.rawHeaders[i]}: ${clientReq.rawHeaders[i + 1]}\r\n`;
247
+ }
248
+ rawRequest += '\r\n';
249
+
250
+ bffSocket.write(rawRequest);
251
+ if (clientHead && clientHead.length) {
252
+ bffSocket.write(clientHead);
253
+ }
254
+
255
+ // 双向管道: BFF ↔ Client
256
+ bffSocket.pipe(clientSocket);
257
+ clientSocket.pipe(bffSocket);
258
+ });
259
+
260
+ const cleanup = () => {
261
+ try { bffSocket.destroy(); } catch (_) {}
262
+ try { clientSocket.destroy(); } catch (_) {}
263
+ };
264
+
265
+ bffSocket.on('error', cleanup);
266
+ clientSocket.on('error', cleanup);
267
+ clientSocket.on('close', () => { try { bffSocket.end(); } catch (_) {} });
268
+ bffSocket.on('close', () => { try { clientSocket.end(); } catch (_) {} });
269
+ }
270
+
271
+ // ==================== 主服务器 ====================
272
+
273
+ const server = http.createServer((clientReq, clientRes) => {
274
+ // 图片文件服务
275
+ if (clientReq.url === '/images' || clientReq.url === '/images/') {
276
+ return serveImageList(clientRes);
277
+ }
278
+ if (clientReq.url.startsWith('/images/')) {
279
+ return serveImage(clientReq.url, clientRes);
280
+ }
281
+
282
+ // 其他请求透传给 BFF
283
+ proxyHttpRequest(clientReq, clientRes);
284
+ });
285
+
286
+ // WebSocket 透传
287
+ server.on('upgrade', proxyWebSocket);
288
+
289
+ server.listen(LISTEN_PORT, () => {
290
+ console.log(`🖼️ Image proxy listening on :${LISTEN_PORT}`);
291
+ console.log(`📷 Images: http://localhost:${LISTEN_PORT}/images/`);
292
+ console.log(`tunnel: http://${BFF_HOST}:${BFF_PORT}`);
293
+ });