@ozaiya/openclaw-channel 0.10.4 → 0.10.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.
- package/dist/src/channel.js +2 -0
- package/dist/src/channel.js.map +1 -1
- package/dist/src/desktopContainer.d.ts +117 -0
- package/dist/src/desktopContainer.js +382 -0
- package/dist/src/desktopContainer.js.map +1 -0
- package/dist/src/desktopTool.d.ts +8 -0
- package/dist/src/desktopTool.js +112 -0
- package/dist/src/desktopTool.js.map +1 -0
- package/dist/src/gateway.js +470 -77
- package/dist/src/gateway.js.map +1 -1
- package/dist/src/wechatMonitor.d.ts +124 -0
- package/dist/src/wechatMonitor.js +617 -0
- package/dist/src/wechatMonitor.js.map +1 -0
- package/package.json +2 -1
package/dist/src/gateway.js
CHANGED
|
@@ -24,6 +24,8 @@ import _sodium from "libsodium-wrappers";
|
|
|
24
24
|
import { io as ioConnect } from "socket.io-client";
|
|
25
25
|
import { decryptBox } from "./crypto.js";
|
|
26
26
|
import { resolveOzaiyaSttConfig } from "./transcribeAudio.js";
|
|
27
|
+
import * as desktop from "./desktopContainer.js";
|
|
28
|
+
import { WeChatMonitorSession, getMonitor, getAllMonitors } from "./wechatMonitor.js";
|
|
27
29
|
let sodiumReady = false;
|
|
28
30
|
async function ensureSodium() {
|
|
29
31
|
if (!sodiumReady) {
|
|
@@ -328,8 +330,10 @@ export async function startGatewayMode(options) {
|
|
|
328
330
|
log?.warn(`[gateway] Socket.io connection error: ${String(err)}`);
|
|
329
331
|
startPolling();
|
|
330
332
|
});
|
|
331
|
-
//
|
|
332
|
-
const
|
|
333
|
+
// Screen connections (VNC): both dockerWs and relayWs per channel
|
|
334
|
+
const screenConnections = new Map();
|
|
335
|
+
// PTY terminal connections (pty + dedicated relay WS)
|
|
336
|
+
const terminalConnections = new Map();
|
|
333
337
|
socket.on("update", (payload) => {
|
|
334
338
|
const body = payload?.body;
|
|
335
339
|
if (!body?.t)
|
|
@@ -347,26 +351,7 @@ export async function startGatewayMode(options) {
|
|
|
347
351
|
}
|
|
348
352
|
onWebhookEvent(body.botId, webhookPayload);
|
|
349
353
|
}
|
|
350
|
-
|
|
351
|
-
// Server → Docker: relay VNC data from browser
|
|
352
|
-
const ws = sandboxConnections.get(body.channelId);
|
|
353
|
-
if (ws && ws.readyState === 1 /* OPEN */) {
|
|
354
|
-
// Handle both Buffer (binary) and string (base64) formats
|
|
355
|
-
const buf = Buffer.isBuffer(body.data) ? body.data : Buffer.from(body.data, "base64");
|
|
356
|
-
ws.send(buf);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
else if (body.t === "sandbox-screen-close" && body.channelId) {
|
|
360
|
-
// Server requests closing a sandbox screen channel
|
|
361
|
-
const ws = sandboxConnections.get(body.channelId);
|
|
362
|
-
if (ws) {
|
|
363
|
-
sandboxConnections.delete(body.channelId);
|
|
364
|
-
try {
|
|
365
|
-
ws.close();
|
|
366
|
-
}
|
|
367
|
-
catch { /* ignore */ }
|
|
368
|
-
}
|
|
369
|
-
}
|
|
354
|
+
// Screen data/close events removed — screen data now flows via dedicated WS pipe
|
|
370
355
|
});
|
|
371
356
|
// Initialize file browser sandbox to stateDir
|
|
372
357
|
const fileBrowser = await import("./fileBrowser.js");
|
|
@@ -375,99 +360,486 @@ export async function startGatewayMode(options) {
|
|
|
375
360
|
const sock = socket; // capture non-null for use in callbacks
|
|
376
361
|
socket.on("gateway-rpc", async (payload, ack) => {
|
|
377
362
|
try {
|
|
378
|
-
if (payload.method === 'list-
|
|
363
|
+
if (payload.method === 'list-screens') {
|
|
364
|
+
const screens = [];
|
|
365
|
+
if (desktop.isInContainer) {
|
|
366
|
+
// In-container: if NOVNC_HOST is set, a separate browser container is reachable
|
|
367
|
+
if (process.env.NOVNC_HOST) {
|
|
368
|
+
screens.push({ id: 'local', type: 'sandbox', name: 'Browser' });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// On host: detect sandbox browser via Docker
|
|
373
|
+
try {
|
|
374
|
+
const dockerEnvLocal = { ...process.env, PATH: `${process.env.PATH}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
375
|
+
let containerId = '';
|
|
376
|
+
try {
|
|
377
|
+
containerId = execSync("docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
378
|
+
}
|
|
379
|
+
catch { /* ignore */ }
|
|
380
|
+
if (!containerId) {
|
|
381
|
+
try {
|
|
382
|
+
containerId = execSync("docker ps -q --filter 'name=sandbox-browser' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
383
|
+
}
|
|
384
|
+
catch { /* ignore */ }
|
|
385
|
+
}
|
|
386
|
+
if (containerId) {
|
|
387
|
+
screens.push({ id: 'sandbox', type: 'sandbox', name: 'Browser Sandbox' });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch { /* ignore */ }
|
|
391
|
+
// Desktop containers on host
|
|
392
|
+
for (const c of desktop.listContainers()) {
|
|
393
|
+
if (c.status.startsWith('Up') || c.status === 'running') {
|
|
394
|
+
screens.push({ id: c.id, type: 'desktop', name: c.name.replace(/^ozaiya-desktop-/, '') });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
ack({ ok: true, result: { screens } });
|
|
399
|
+
}
|
|
400
|
+
else if (payload.method === 'list-files') {
|
|
379
401
|
const files = await fileBrowser.listDirectory(payload.params.path ?? '.');
|
|
380
402
|
ack({ ok: true, result: files });
|
|
381
403
|
}
|
|
382
404
|
else if (payload.method === 'sandbox-screen-open') {
|
|
383
|
-
// Open sandbox browser VNC relay — connect to Docker noVNC
|
|
405
|
+
// Open sandbox browser VNC relay — connect to Docker noVNC + relay WS back to server
|
|
384
406
|
const { channelId, botId } = payload.params;
|
|
385
407
|
log?.info(`[gateway] sandbox-screen-open channelId=${channelId} botId=${botId}`);
|
|
386
408
|
// Find the Docker sandbox container's noVNC port
|
|
387
409
|
let port;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
410
|
+
if (desktop.isInContainer) {
|
|
411
|
+
// In-container mode: connect to local noVNC
|
|
412
|
+
port = process.env.NOVNC_PORT || '6080';
|
|
413
|
+
}
|
|
414
|
+
else
|
|
391
415
|
try {
|
|
392
|
-
|
|
416
|
+
const dockerEnvLocal = { ...process.env, PATH: `${process.env.PATH}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
417
|
+
let output = '';
|
|
418
|
+
try {
|
|
419
|
+
output = execSync("docker port $(docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1) 6080 2>/dev/null", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
output = execSync("docker port sandbox-browser 6080 2>/dev/null", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
423
|
+
}
|
|
424
|
+
const lastLine = output.split('\n').pop() || output;
|
|
425
|
+
port = lastLine.split(':').pop() || '6080';
|
|
393
426
|
}
|
|
394
427
|
catch {
|
|
395
|
-
|
|
428
|
+
log?.warn(`[gateway] sandbox-screen: no Docker sandbox container found`);
|
|
429
|
+
ack({ ok: false, error: 'No Docker sandbox container found' });
|
|
430
|
+
return;
|
|
396
431
|
}
|
|
397
|
-
const lastLine = output.split('\n').pop() || output;
|
|
398
|
-
port = lastLine.split(':').pop() || '6080';
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
log?.warn(`[gateway] sandbox-screen: no Docker sandbox container found`);
|
|
402
|
-
ack({ ok: false, error: 'No Docker sandbox container found' });
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
432
|
// Connect to noVNC websockify in the Docker container
|
|
406
|
-
|
|
433
|
+
const noVncHost = process.env.NOVNC_HOST || 'localhost';
|
|
434
|
+
let dockerWs;
|
|
407
435
|
try {
|
|
408
436
|
const WsModule = await import("ws");
|
|
409
437
|
const WsCtor = WsModule.default ?? WsModule;
|
|
410
|
-
|
|
411
|
-
|
|
438
|
+
dockerWs = new WsCtor(`ws://${noVncHost}:${port}/websockify`);
|
|
439
|
+
dockerWs.binaryType = "arraybuffer";
|
|
412
440
|
}
|
|
413
441
|
catch (err) {
|
|
414
442
|
log?.warn(`[gateway] sandbox-screen: failed to create WS: ${String(err)}`);
|
|
415
443
|
ack({ ok: false, error: `Failed to connect to noVNC: ${String(err)}` });
|
|
416
444
|
return;
|
|
417
445
|
}
|
|
418
|
-
|
|
419
|
-
|
|
446
|
+
// Open dedicated relay WS back to the server
|
|
447
|
+
const relayUrl = `${apiBaseUrl.replace(/^http/, 'ws')}/v1/ws-relay/${channelId}/ws?token=${encodeURIComponent(gatewayToken)}`;
|
|
448
|
+
let relayWs;
|
|
449
|
+
try {
|
|
450
|
+
const WsModule = await import("ws");
|
|
451
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
452
|
+
relayWs = new WsCtor(relayUrl);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
log?.warn(`[gateway] sandbox-screen: failed to create relay WS: ${String(err)}`);
|
|
456
|
+
try {
|
|
457
|
+
dockerWs.close();
|
|
458
|
+
}
|
|
459
|
+
catch { /* ignore */ }
|
|
460
|
+
ack({ ok: false, error: `Failed to connect relay WS: ${String(err)}` });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
screenConnections.set(channelId, { dockerWs, relayWs });
|
|
464
|
+
const cleanupScreen = (reason) => {
|
|
465
|
+
const entry = screenConnections.get(channelId);
|
|
466
|
+
if (!entry)
|
|
467
|
+
return;
|
|
468
|
+
screenConnections.delete(channelId);
|
|
469
|
+
try {
|
|
470
|
+
if (entry.dockerWs.readyState !== 3 /* CLOSED */)
|
|
471
|
+
entry.dockerWs.close();
|
|
472
|
+
}
|
|
473
|
+
catch { /* ignore */ }
|
|
474
|
+
try {
|
|
475
|
+
if (entry.relayWs.readyState !== 3 /* CLOSED */)
|
|
476
|
+
entry.relayWs.close();
|
|
477
|
+
}
|
|
478
|
+
catch { /* ignore */ }
|
|
479
|
+
log?.info(`[gateway] sandbox-screen: closed channelId=${channelId} reason=${reason}`);
|
|
480
|
+
};
|
|
481
|
+
dockerWs.on("open", () => {
|
|
420
482
|
log?.info(`[gateway] sandbox-screen: connected to Docker noVNC on port ${port}`);
|
|
421
483
|
});
|
|
422
|
-
// Docker
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
});
|
|
484
|
+
// Buffer Docker messages until relay WS is open.
|
|
485
|
+
// Docker noVNC sends the VNC handshake immediately on connect (local, fast),
|
|
486
|
+
// but relay WS (over internet) may not be open yet. Without buffering, the
|
|
487
|
+
// handshake is dropped and the VNC server times out waiting for a response.
|
|
488
|
+
const earlyDockerMsgs = [];
|
|
489
|
+
let relayOpen = false;
|
|
490
|
+
relayWs.on("open", () => {
|
|
491
|
+
log?.info(`[gateway] sandbox-screen: relay WS connected channelId=${channelId}`);
|
|
492
|
+
relayOpen = true;
|
|
493
|
+
for (const msg of earlyDockerMsgs) {
|
|
494
|
+
if (relayWs.readyState === 1)
|
|
495
|
+
relayWs.send(msg);
|
|
496
|
+
}
|
|
497
|
+
earlyDockerMsgs.length = 0;
|
|
430
498
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
499
|
+
// Raw binary pipe: Docker noVNC ↔ relay WS (server ↔ browser)
|
|
500
|
+
dockerWs.on("message", (data) => {
|
|
501
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
502
|
+
if (relayOpen && relayWs.readyState === 1 /* OPEN */) {
|
|
503
|
+
relayWs.send(buf);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
earlyDockerMsgs.push(buf);
|
|
507
|
+
}
|
|
435
508
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
509
|
+
relayWs.on("message", (data) => {
|
|
510
|
+
if (dockerWs.readyState === 1 /* OPEN */) {
|
|
511
|
+
dockerWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
512
|
+
}
|
|
440
513
|
});
|
|
514
|
+
dockerWs.on("close", () => cleanupScreen('docker-ws-close'));
|
|
515
|
+
dockerWs.on("error", (err) => cleanupScreen(`docker-ws-error(${err.message})`));
|
|
516
|
+
relayWs.on("close", () => cleanupScreen('relay-ws-close'));
|
|
517
|
+
relayWs.on("error", (err) => cleanupScreen(`relay-ws-error(${err.message})`));
|
|
441
518
|
ack({ ok: true });
|
|
442
519
|
}
|
|
443
520
|
else if (payload.method === 'sandbox-screen-password') {
|
|
444
521
|
// Return VNC password from Docker container env (no WS connection)
|
|
445
522
|
let password = '';
|
|
523
|
+
if (desktop.isInContainer) {
|
|
524
|
+
password = process.env.VNC_PASSWORD || process.env.OPENCLAW_BROWSER_NOVNC_PASSWORD || '';
|
|
525
|
+
}
|
|
526
|
+
else
|
|
527
|
+
try {
|
|
528
|
+
const dockerEnvLocal = { ...process.env, PATH: `${process.env.PATH}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
529
|
+
let containerId = '';
|
|
530
|
+
try {
|
|
531
|
+
containerId = execSync("docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
532
|
+
}
|
|
533
|
+
catch { /* ignore */ }
|
|
534
|
+
if (!containerId) {
|
|
535
|
+
try {
|
|
536
|
+
containerId = execSync("docker ps -q --filter 'name=sandbox-browser' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
537
|
+
}
|
|
538
|
+
catch { /* ignore */ }
|
|
539
|
+
}
|
|
540
|
+
if (containerId) {
|
|
541
|
+
const envOutput = execSync(`docker inspect ${containerId} -f '{{range .Config.Env}}{{println .}}{{end}}'`, { timeout: 5000, env: dockerEnvLocal }).toString();
|
|
542
|
+
for (const line of envOutput.split('\n')) {
|
|
543
|
+
if (line.startsWith('OPENCLAW_BROWSER_NOVNC_PASSWORD=')) {
|
|
544
|
+
password = line.slice('OPENCLAW_BROWSER_NOVNC_PASSWORD='.length).trim();
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch { /* ignore */ }
|
|
551
|
+
ack({ ok: true, result: { password } });
|
|
552
|
+
// ── Desktop container lifecycle ─────────────────────────────────────
|
|
553
|
+
}
|
|
554
|
+
else if (payload.method === 'desktop-container-create') {
|
|
555
|
+
ack({ ok: true, result: desktop.createContainer(payload.params) });
|
|
556
|
+
}
|
|
557
|
+
else if (payload.method === 'desktop-container-list') {
|
|
558
|
+
ack({ ok: true, result: desktop.listContainers() });
|
|
559
|
+
}
|
|
560
|
+
else if (payload.method === 'desktop-container-start') {
|
|
561
|
+
ack({ ok: true, result: desktop.startContainer(payload.params.id) });
|
|
562
|
+
}
|
|
563
|
+
else if (payload.method === 'desktop-container-stop') {
|
|
564
|
+
ack({ ok: true, result: desktop.stopContainer(payload.params.id) });
|
|
565
|
+
}
|
|
566
|
+
else if (payload.method === 'desktop-container-remove') {
|
|
567
|
+
ack({ ok: true, result: desktop.removeContainer(payload.params.id) });
|
|
568
|
+
}
|
|
569
|
+
else if (payload.method === 'desktop-container-logs') {
|
|
570
|
+
ack({ ok: true, result: { logs: desktop.getContainerLogs(payload.params.id, payload.params.tail) } });
|
|
571
|
+
}
|
|
572
|
+
else if (payload.method === 'desktop-container-launch-app') {
|
|
573
|
+
ack({ ok: true, result: desktop.launchApp(payload.params.id, payload.params.app, payload.params.appArgs) });
|
|
574
|
+
// ── Desktop AT-SPI proxy ────────────────────────────────────────────
|
|
575
|
+
}
|
|
576
|
+
else if (payload.method === 'desktop-atspi-health') {
|
|
577
|
+
ack({ ok: true, result: desktop.atspiHealth(payload.params.agentPort) });
|
|
578
|
+
}
|
|
579
|
+
else if (payload.method === 'desktop-atspi-apps') {
|
|
580
|
+
ack({ ok: true, result: desktop.atspiApps(payload.params.agentPort) });
|
|
581
|
+
}
|
|
582
|
+
else if (payload.method === 'desktop-atspi-tree') {
|
|
583
|
+
ack({ ok: true, result: desktop.atspiTree(payload.params.agentPort, { app: payload.params.app, maxDepth: payload.params.maxDepth }) });
|
|
584
|
+
}
|
|
585
|
+
else if (payload.method === 'desktop-atspi-find') {
|
|
586
|
+
const { agentPort, ...criteria } = payload.params;
|
|
587
|
+
ack({ ok: true, result: desktop.atspiFind(agentPort, criteria) });
|
|
588
|
+
}
|
|
589
|
+
else if (payload.method === 'desktop-atspi-action') {
|
|
590
|
+
ack({ ok: true, result: desktop.atspiAction(payload.params.agentPort, payload.params.path, payload.params.action) });
|
|
591
|
+
}
|
|
592
|
+
else if (payload.method === 'desktop-atspi-type') {
|
|
593
|
+
ack({ ok: true, result: desktop.atspiType(payload.params.agentPort, payload.params.path, payload.params.text) });
|
|
594
|
+
}
|
|
595
|
+
else if (payload.method === 'desktop-atspi-key') {
|
|
596
|
+
ack({ ok: true, result: desktop.atspiKey(payload.params.agentPort, payload.params.key, payload.params.modifiers) });
|
|
597
|
+
}
|
|
598
|
+
else if (payload.method === 'desktop-atspi-click') {
|
|
599
|
+
ack({ ok: true, result: desktop.atspiClick(payload.params.agentPort, payload.params.x, payload.params.y, payload.params.button) });
|
|
600
|
+
}
|
|
601
|
+
else if (payload.method === 'desktop-atspi-screenshot') {
|
|
602
|
+
ack({ ok: true, result: { data: desktop.atspiScreenshot(payload.params.agentPort, { bounds: payload.params.bounds, quality: payload.params.quality }) } });
|
|
603
|
+
// ── Desktop screen VNC relay ────────────────────────────────────────
|
|
604
|
+
}
|
|
605
|
+
else if (payload.method === 'desktop-clipboard-type') {
|
|
606
|
+
ack({ ok: true, result: desktop.clipboardType(payload.params.id, payload.params.text) });
|
|
607
|
+
}
|
|
608
|
+
else if (payload.method === 'desktop-xdotool-key') {
|
|
609
|
+
ack({ ok: true, result: desktop.xdotoolKey(payload.params.id, payload.params.keys) });
|
|
610
|
+
}
|
|
611
|
+
else if (payload.method === 'desktop-screen-open') {
|
|
612
|
+
const { channelId, containerId } = payload.params;
|
|
613
|
+
log?.info(`[gateway] desktop-screen-open channelId=${channelId} containerId=${containerId}`);
|
|
614
|
+
// Find the noVNC port for the desktop container
|
|
615
|
+
const noVncPort = desktop.isInContainer
|
|
616
|
+
? parseInt(process.env.NOVNC_PORT || "6080", 10)
|
|
617
|
+
: desktop.findContainerPort(containerId, 6080);
|
|
618
|
+
if (!noVncPort) {
|
|
619
|
+
ack({ ok: false, error: "Desktop container noVNC port not found" });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const noVncHost = process.env.NOVNC_HOST || 'localhost';
|
|
623
|
+
let dockerWs;
|
|
624
|
+
try {
|
|
625
|
+
const WsModule = await import("ws");
|
|
626
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
627
|
+
dockerWs = new WsCtor(`ws://${noVncHost}:${noVncPort}/websockify`);
|
|
628
|
+
dockerWs.binaryType = "arraybuffer";
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
ack({ ok: false, error: `Failed to connect to noVNC: ${String(err)}` });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// Open dedicated relay WS back to the server
|
|
635
|
+
const desktopRelayUrl = `${apiBaseUrl.replace(/^http/, 'ws')}/v1/ws-relay/${channelId}/ws?token=${encodeURIComponent(gatewayToken)}`;
|
|
636
|
+
let relayWs;
|
|
446
637
|
try {
|
|
447
|
-
const
|
|
448
|
-
|
|
638
|
+
const WsModule = await import("ws");
|
|
639
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
640
|
+
relayWs = new WsCtor(desktopRelayUrl);
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
try {
|
|
644
|
+
dockerWs.close();
|
|
645
|
+
}
|
|
646
|
+
catch { /* ignore */ }
|
|
647
|
+
ack({ ok: false, error: `Failed to connect relay WS: ${String(err)}` });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
screenConnections.set(channelId, { dockerWs, relayWs });
|
|
651
|
+
const cleanupDesktop = (reason) => {
|
|
652
|
+
const entry = screenConnections.get(channelId);
|
|
653
|
+
if (!entry)
|
|
654
|
+
return;
|
|
655
|
+
screenConnections.delete(channelId);
|
|
656
|
+
try {
|
|
657
|
+
if (entry.dockerWs.readyState !== 3 /* CLOSED */)
|
|
658
|
+
entry.dockerWs.close();
|
|
659
|
+
}
|
|
660
|
+
catch { /* ignore */ }
|
|
449
661
|
try {
|
|
450
|
-
|
|
662
|
+
if (entry.relayWs.readyState !== 3 /* CLOSED */)
|
|
663
|
+
entry.relayWs.close();
|
|
451
664
|
}
|
|
452
665
|
catch { /* ignore */ }
|
|
453
|
-
|
|
666
|
+
log?.info(`[gateway] desktop-screen: closed channelId=${channelId} reason=${reason}`);
|
|
667
|
+
};
|
|
668
|
+
dockerWs.on("open", () => {
|
|
669
|
+
log?.info(`[gateway] desktop-screen: connected to noVNC on port ${noVncPort}`);
|
|
670
|
+
});
|
|
671
|
+
// Buffer Docker messages until relay WS is open (same race fix as sandbox).
|
|
672
|
+
const earlyDesktopMsgs = [];
|
|
673
|
+
let desktopRelayOpen = false;
|
|
674
|
+
relayWs.on("open", () => {
|
|
675
|
+
log?.info(`[gateway] desktop-screen: relay WS connected channelId=${channelId}`);
|
|
676
|
+
desktopRelayOpen = true;
|
|
677
|
+
for (const msg of earlyDesktopMsgs) {
|
|
678
|
+
if (relayWs.readyState === 1)
|
|
679
|
+
relayWs.send(msg);
|
|
680
|
+
}
|
|
681
|
+
earlyDesktopMsgs.length = 0;
|
|
682
|
+
});
|
|
683
|
+
// Raw binary pipe: Docker noVNC ↔ relay WS (server ↔ browser)
|
|
684
|
+
dockerWs.on("message", (data) => {
|
|
685
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
686
|
+
if (desktopRelayOpen && relayWs.readyState === 1 /* OPEN */) {
|
|
687
|
+
relayWs.send(buf);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
earlyDesktopMsgs.push(buf);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
relayWs.on("message", (data) => {
|
|
694
|
+
if (dockerWs.readyState === 1 /* OPEN */) {
|
|
695
|
+
dockerWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
dockerWs.on("close", () => cleanupDesktop('docker-ws-close'));
|
|
699
|
+
dockerWs.on("error", (err) => cleanupDesktop(`docker-ws-error(${err.message})`));
|
|
700
|
+
relayWs.on("close", () => cleanupDesktop('relay-ws-close'));
|
|
701
|
+
relayWs.on("error", (err) => cleanupDesktop(`relay-ws-error(${err.message})`));
|
|
702
|
+
ack({ ok: true });
|
|
703
|
+
}
|
|
704
|
+
else if (payload.method === 'desktop-screen-password') {
|
|
705
|
+
const { containerId } = payload.params;
|
|
706
|
+
ack({ ok: true, result: { password: desktop.getVncPassword(containerId) } });
|
|
707
|
+
// ── Bot terminal PTY (dedicated WS pipe) ─────────────────────────────
|
|
708
|
+
}
|
|
709
|
+
else if (payload.method === 'bot-terminal-open') {
|
|
710
|
+
const { channelId, containerId } = payload.params;
|
|
711
|
+
log?.info(`[gateway] bot-terminal-open channelId=${channelId} containerId=${containerId ?? 'self'}`);
|
|
712
|
+
try {
|
|
713
|
+
const nodePty = await import("node-pty");
|
|
714
|
+
const cols = 80;
|
|
715
|
+
const rows = 24;
|
|
716
|
+
const env = { TERM: 'xterm-256color', ...process.env };
|
|
717
|
+
let ptyProcess;
|
|
718
|
+
if (desktop.isInContainer) {
|
|
719
|
+
ptyProcess = nodePty.spawn('/bin/bash', ['-l'], { cols, rows, cwd: '/', env });
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
const dockerEnvLocal = { ...env, PATH: `${process.env.PATH || ''}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
723
|
+
let targetContainer = containerId;
|
|
724
|
+
if (!targetContainer) {
|
|
725
|
+
try {
|
|
726
|
+
targetContainer = execSync("docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
727
|
+
}
|
|
728
|
+
catch { /* ignore */ }
|
|
729
|
+
if (!targetContainer) {
|
|
730
|
+
try {
|
|
731
|
+
targetContainer = execSync("docker ps -q --filter 'name=sandbox-browser' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
732
|
+
}
|
|
733
|
+
catch { /* ignore */ }
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!targetContainer) {
|
|
737
|
+
ack({ ok: false, error: 'No Docker container found for terminal' });
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
ptyProcess = nodePty.spawn('docker', ['exec', '-it', targetContainer, '/bin/bash'], {
|
|
741
|
+
cols, rows, cwd: '/', env: dockerEnvLocal,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// Open dedicated WS back to the server relay endpoint
|
|
745
|
+
const relayUrl = `${apiBaseUrl.replace(/^http/, 'ws')}/v1/ws-relay/${channelId}/ws?token=${encodeURIComponent(gatewayToken)}`;
|
|
746
|
+
const WsModule = await import("ws");
|
|
747
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
748
|
+
const relayWs = new WsCtor(relayUrl);
|
|
749
|
+
terminalConnections.set(channelId, { pty: ptyProcess, ws: relayWs });
|
|
750
|
+
const cleanupTerminal = (reason) => {
|
|
751
|
+
const entry = terminalConnections.get(channelId);
|
|
752
|
+
if (!entry)
|
|
753
|
+
return;
|
|
754
|
+
terminalConnections.delete(channelId);
|
|
454
755
|
try {
|
|
455
|
-
|
|
756
|
+
entry.pty.kill();
|
|
456
757
|
}
|
|
457
758
|
catch { /* ignore */ }
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
759
|
+
try {
|
|
760
|
+
if (entry.ws.readyState !== 3 /* CLOSED */)
|
|
761
|
+
entry.ws.close();
|
|
762
|
+
}
|
|
763
|
+
catch { /* ignore */ }
|
|
764
|
+
log?.info(`[gateway] bot-terminal: closed channelId=${channelId} reason=${reason}`);
|
|
765
|
+
};
|
|
766
|
+
relayWs.on("open", () => {
|
|
767
|
+
log?.info(`[gateway] bot-terminal: relay WS connected channelId=${channelId}`);
|
|
768
|
+
});
|
|
769
|
+
// PTY → relay WS (binary)
|
|
770
|
+
ptyProcess.onData((data) => {
|
|
771
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
772
|
+
relayWs.send(Buffer.from(data));
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
// Relay WS → PTY (text input or JSON control messages)
|
|
776
|
+
relayWs.on("message", (raw) => {
|
|
777
|
+
const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
778
|
+
const str = buf.toString();
|
|
779
|
+
// Check for JSON control messages (resize)
|
|
780
|
+
try {
|
|
781
|
+
const msg = JSON.parse(str);
|
|
782
|
+
if (msg.type === 'resize' && typeof msg.cols === 'number' && typeof msg.rows === 'number') {
|
|
783
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
784
|
+
return;
|
|
465
785
|
}
|
|
466
786
|
}
|
|
467
|
-
|
|
787
|
+
catch {
|
|
788
|
+
// Not JSON — treat as terminal input
|
|
789
|
+
}
|
|
790
|
+
ptyProcess.write(str);
|
|
791
|
+
});
|
|
792
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
793
|
+
// Send pty_exit control message before closing
|
|
794
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
795
|
+
relayWs.send(JSON.stringify({ type: 'pty_exit', exitCode }));
|
|
796
|
+
}
|
|
797
|
+
cleanupTerminal(`pty-exit(${exitCode})`);
|
|
798
|
+
});
|
|
799
|
+
relayWs.on("close", () => cleanupTerminal('relay-ws-close'));
|
|
800
|
+
relayWs.on("error", (err) => cleanupTerminal(`relay-ws-error(${err.message})`));
|
|
801
|
+
ack({ ok: true });
|
|
468
802
|
}
|
|
469
|
-
catch {
|
|
470
|
-
|
|
803
|
+
catch (err) {
|
|
804
|
+
log?.warn(`[gateway] bot-terminal-open failed: ${String(err)}`);
|
|
805
|
+
ack({ ok: false, error: `Failed to open terminal: ${String(err)}` });
|
|
806
|
+
}
|
|
807
|
+
// ── WeChat monitor ──────────────────────────────────────────────────
|
|
808
|
+
}
|
|
809
|
+
else if (payload.method === 'wechat-monitor-start') {
|
|
810
|
+
const { containerId, ...monitorConfig } = payload.params;
|
|
811
|
+
const existing = getMonitor(containerId);
|
|
812
|
+
if (existing) {
|
|
813
|
+
ack({ ok: true, result: existing.getStatus() });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const monitor = new WeChatMonitorSession(containerId, {
|
|
817
|
+
...monitorConfig,
|
|
818
|
+
onNewMessage: (event) => {
|
|
819
|
+
sock.emit("wechat-monitor-event", event);
|
|
820
|
+
},
|
|
821
|
+
onError: (error) => {
|
|
822
|
+
log?.warn(`[wechat-monitor] error: ${error.message}`);
|
|
823
|
+
},
|
|
824
|
+
}, log);
|
|
825
|
+
await monitor.start();
|
|
826
|
+
ack({ ok: true, result: monitor.getStatus() });
|
|
827
|
+
}
|
|
828
|
+
else if (payload.method === 'wechat-monitor-stop') {
|
|
829
|
+
const { containerId } = payload.params;
|
|
830
|
+
const monitor = getMonitor(containerId);
|
|
831
|
+
if (monitor) {
|
|
832
|
+
monitor.stop();
|
|
833
|
+
ack({ ok: true, result: { stopped: true } });
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
ack({ ok: true, result: { stopped: false, reason: "not running" } });
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
else if (payload.method === 'wechat-monitor-status') {
|
|
840
|
+
const { containerId } = payload.params;
|
|
841
|
+
const monitor = getMonitor(containerId);
|
|
842
|
+
ack({ ok: true, result: monitor ? monitor.getStatus() : { running: false, containerId } });
|
|
471
843
|
}
|
|
472
844
|
else {
|
|
473
845
|
ack({ ok: false, error: `Unknown RPC method: ${payload.method}` });
|
|
@@ -477,15 +849,36 @@ export async function startGatewayMode(options) {
|
|
|
477
849
|
ack({ ok: false, error: err instanceof Error ? err.message : 'RPC handler error' });
|
|
478
850
|
}
|
|
479
851
|
});
|
|
480
|
-
// Clean up all
|
|
852
|
+
// Clean up all screen connections, terminal connections, and monitors on socket disconnect
|
|
481
853
|
sock.on("disconnect", () => {
|
|
482
|
-
for (const [,
|
|
854
|
+
for (const [, entry] of screenConnections) {
|
|
855
|
+
try {
|
|
856
|
+
if (entry.dockerWs.readyState !== 3)
|
|
857
|
+
entry.dockerWs.close();
|
|
858
|
+
}
|
|
859
|
+
catch { /* ignore */ }
|
|
860
|
+
try {
|
|
861
|
+
if (entry.relayWs.readyState !== 3)
|
|
862
|
+
entry.relayWs.close();
|
|
863
|
+
}
|
|
864
|
+
catch { /* ignore */ }
|
|
865
|
+
}
|
|
866
|
+
screenConnections.clear();
|
|
867
|
+
for (const [, entry] of terminalConnections) {
|
|
483
868
|
try {
|
|
484
|
-
|
|
869
|
+
entry.pty.kill();
|
|
485
870
|
}
|
|
486
871
|
catch { /* ignore */ }
|
|
872
|
+
try {
|
|
873
|
+
if (entry.ws.readyState !== 3)
|
|
874
|
+
entry.ws.close();
|
|
875
|
+
}
|
|
876
|
+
catch { /* ignore */ }
|
|
877
|
+
}
|
|
878
|
+
terminalConnections.clear();
|
|
879
|
+
for (const [, monitor] of getAllMonitors()) {
|
|
880
|
+
monitor.stop();
|
|
487
881
|
}
|
|
488
|
-
sandboxConnections.clear();
|
|
489
882
|
});
|
|
490
883
|
}
|
|
491
884
|
catch (err) {
|