tao-shen Claude Opus 4.6 commited on
Commit
c0338ad
Β·
1 Parent(s): 5f20dc1

feat: serve pixel office animation at / with static asset serving

Browse files

- Add static file serving to a2a-proxy.cjs (/, /static/*, /admin)
- Generate index.html from electron-standalone.html at Docker build time
- Copy frontend directory into Docker image
- Add officeName to /status response for frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. .gitignore +1 -0
  2. Dockerfile +6 -2
  3. scripts/a2a-proxy.cjs +79 -1
.gitignore CHANGED
@@ -9,3 +9,4 @@ node_modules/
9
  # ζ—₯εΏ—δΈŽδΈ΄ζ—Ά
10
  *.log
11
  .DS_Store
 
 
9
  # ζ—₯εΏ—δΈŽδΈ΄ζ—Ά
10
  *.log
11
  .DS_Store
12
+ frontend/index.html
Dockerfile CHANGED
@@ -60,10 +60,14 @@ RUN echo "[build][layer2.5] Cloning A2A gateway extension..." && START=$(date +%
60
  && echo "[build] A2A gateway installed: $(ls node_modules | wc -l) packages" \
61
  && echo "[build][layer2.5] A2A gateway: $(($(date +%s) - START))s"
62
 
63
- # ── Layer 3 (node): Scripts + Config ──────────────────────────────────────────
64
  COPY --chown=node:node scripts /home/node/scripts
 
65
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
66
- RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py
 
 
 
67
 
68
  ENV NODE_ENV=production
69
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
 
60
  && echo "[build] A2A gateway installed: $(ls node_modules | wc -l) packages" \
61
  && echo "[build][layer2.5] A2A gateway: $(($(date +%s) - START))s"
62
 
63
+ # ── Layer 3 (node): Scripts + Config + Frontend ──────────────────────────────
64
  COPY --chown=node:node scripts /home/node/scripts
65
+ COPY --chown=node:node frontend /home/node/frontend
66
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
67
+ RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py \
68
+ && VERSION_TS=$(date +%s) \
69
+ && sed "s/{{VERSION_TIMESTAMP}}/${VERSION_TS}/g" /home/node/frontend/electron-standalone.html > /home/node/frontend/index.html \
70
+ && echo "[build] Frontend index.html generated (timestamp=${VERSION_TS})"
71
 
72
  ENV NODE_ENV=production
73
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
scripts/a2a-proxy.cjs CHANGED
@@ -2,6 +2,10 @@
2
  * a2a-proxy.cjs β€” Reverse proxy on port 7860
3
  *
4
  * Routes:
 
 
 
 
5
  * /.well-known/* β†’ A2A gateway (port 18800)
6
  * /a2a/* β†’ A2A gateway (port 18800)
7
  * /api/state β†’ local state JSON (for Office frontend polling)
@@ -12,6 +16,57 @@
12
 
13
  const http = require('http');
14
  const url = require('url');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const LISTEN_PORT = 7860;
17
  const OPENCLAW_PORT = 7861;
@@ -218,7 +273,10 @@ const server = http.createServer((req, res) => {
218
  'Content-Type': 'application/json',
219
  'Access-Control-Allow-Origin': '*'
220
  });
221
- return res.end(JSON.stringify(currentState));
 
 
 
222
  }
223
 
224
  // Agents endpoint β€” merge OpenClaw agents with remote agents
@@ -240,6 +298,26 @@ const server = http.createServer((req, res) => {
240
  return;
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  // Everything else β†’ OpenClaw
244
  proxyRequest(req, res, OPENCLAW_PORT);
245
  });
 
2
  * a2a-proxy.cjs β€” Reverse proxy on port 7860
3
  *
4
  * Routes:
5
+ * / β†’ pixel office animation (frontend/index.html)
6
+ * /static/* β†’ static assets (frontend/)
7
+ * /admin β†’ OpenClaw control UI (port 7861)
8
+ * /admin/* β†’ OpenClaw control UI (port 7861)
9
  * /.well-known/* β†’ A2A gateway (port 18800)
10
  * /a2a/* β†’ A2A gateway (port 18800)
11
  * /api/state β†’ local state JSON (for Office frontend polling)
 
16
 
17
  const http = require('http');
18
  const url = require('url');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ // Frontend directory (try /home/node/frontend first, then relative)
23
+ const FRONTEND_DIR = fs.existsSync('/home/node/frontend')
24
+ ? '/home/node/frontend'
25
+ : path.join(__dirname, '..', 'frontend');
26
+
27
+ const MIME_TYPES = {
28
+ '.html': 'text/html; charset=utf-8',
29
+ '.js': 'application/javascript',
30
+ '.css': 'text/css',
31
+ '.json': 'application/json',
32
+ '.png': 'image/png',
33
+ '.jpg': 'image/jpeg',
34
+ '.jpeg': 'image/jpeg',
35
+ '.webp': 'image/webp',
36
+ '.gif': 'image/gif',
37
+ '.svg': 'image/svg+xml',
38
+ '.woff2': 'font/woff2',
39
+ '.woff': 'font/woff',
40
+ '.ttf': 'font/ttf',
41
+ '.ico': 'image/x-icon',
42
+ '.mp3': 'audio/mpeg',
43
+ '.ogg': 'audio/ogg',
44
+ '.md': 'text/markdown; charset=utf-8',
45
+ };
46
+
47
+ function serveStaticFile(res, filePath) {
48
+ // Prevent directory traversal
49
+ const resolved = path.resolve(filePath);
50
+ if (!resolved.startsWith(path.resolve(FRONTEND_DIR))) {
51
+ res.writeHead(403);
52
+ return res.end('Forbidden');
53
+ }
54
+ fs.readFile(resolved, (err, data) => {
55
+ if (err) {
56
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
57
+ return res.end('Not Found');
58
+ }
59
+ const ext = path.extname(resolved).toLowerCase();
60
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
61
+ const cacheControl = (ext === '.html') ? 'no-cache' : 'public, max-age=86400';
62
+ res.writeHead(200, {
63
+ 'Content-Type': contentType,
64
+ 'Cache-Control': cacheControl,
65
+ 'Access-Control-Allow-Origin': '*'
66
+ });
67
+ res.end(data);
68
+ });
69
+ }
70
 
71
  const LISTEN_PORT = 7860;
72
  const OPENCLAW_PORT = 7861;
 
273
  'Content-Type': 'application/json',
274
  'Access-Control-Allow-Origin': '*'
275
  });
276
+ return res.end(JSON.stringify({
277
+ ...currentState,
278
+ officeName: `${AGENT_NAME}'s Office`
279
+ }));
280
  }
281
 
282
  // Agents endpoint β€” merge OpenClaw agents with remote agents
 
298
  return;
299
  }
300
 
301
+ // Serve index.html at /
302
+ if (pathname === '/' && req.method === 'GET') {
303
+ const indexPath = path.join(FRONTEND_DIR, 'index.html');
304
+ return serveStaticFile(res, indexPath);
305
+ }
306
+
307
+ // Serve static assets at /static/*
308
+ if (pathname.startsWith('/static/')) {
309
+ const assetPath = path.join(FRONTEND_DIR, pathname.slice('/static/'.length).split('?')[0]);
310
+ return serveStaticFile(res, assetPath);
311
+ }
312
+
313
+ // Admin panel β†’ proxy to OpenClaw UI directly
314
+ if (pathname === '/admin' || pathname === '/admin/') {
315
+ const token = process.env.GATEWAY_TOKEN || '';
316
+ // Rewrite to OpenClaw root with token
317
+ req.url = token ? `/?token=${token}` : '/';
318
+ return proxyRequest(req, res, OPENCLAW_PORT);
319
+ }
320
+
321
  // Everything else β†’ OpenClaw
322
  proxyRequest(req, res, OPENCLAW_PORT);
323
  });