@ozaiya/openclaw-channel 0.10.3 → 0.10.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/src/channel.js +6 -2
- 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 +441 -77
- package/dist/src/gateway.js.map +1 -1
- package/dist/src/imageGeneration.d.ts +15 -11
- package/dist/src/imageGeneration.js +99 -34
- package/dist/src/imageGeneration.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,457 @@ 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
|
-
|
|
423
|
-
|
|
424
|
-
ws.on("message", (data) => {
|
|
425
|
-
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
426
|
-
sock.volatile.emit("sandbox-screen-data", {
|
|
427
|
-
channelId,
|
|
428
|
-
data: buf,
|
|
429
|
-
});
|
|
484
|
+
relayWs.on("open", () => {
|
|
485
|
+
log?.info(`[gateway] sandbox-screen: relay WS connected channelId=${channelId}`);
|
|
430
486
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
487
|
+
// Raw binary pipe: Docker noVNC ↔ relay WS (server ↔ browser)
|
|
488
|
+
dockerWs.on("message", (data) => {
|
|
489
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
490
|
+
relayWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
491
|
+
}
|
|
435
492
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
493
|
+
relayWs.on("message", (data) => {
|
|
494
|
+
if (dockerWs.readyState === 1 /* OPEN */) {
|
|
495
|
+
dockerWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
496
|
+
}
|
|
440
497
|
});
|
|
498
|
+
dockerWs.on("close", () => cleanupScreen('docker-ws-close'));
|
|
499
|
+
dockerWs.on("error", (err) => cleanupScreen(`docker-ws-error(${err.message})`));
|
|
500
|
+
relayWs.on("close", () => cleanupScreen('relay-ws-close'));
|
|
501
|
+
relayWs.on("error", (err) => cleanupScreen(`relay-ws-error(${err.message})`));
|
|
441
502
|
ack({ ok: true });
|
|
442
503
|
}
|
|
443
504
|
else if (payload.method === 'sandbox-screen-password') {
|
|
444
505
|
// Return VNC password from Docker container env (no WS connection)
|
|
445
506
|
let password = '';
|
|
507
|
+
if (desktop.isInContainer) {
|
|
508
|
+
password = process.env.VNC_PASSWORD || process.env.OPENCLAW_BROWSER_NOVNC_PASSWORD || '';
|
|
509
|
+
}
|
|
510
|
+
else
|
|
511
|
+
try {
|
|
512
|
+
const dockerEnvLocal = { ...process.env, PATH: `${process.env.PATH}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
513
|
+
let containerId = '';
|
|
514
|
+
try {
|
|
515
|
+
containerId = execSync("docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
516
|
+
}
|
|
517
|
+
catch { /* ignore */ }
|
|
518
|
+
if (!containerId) {
|
|
519
|
+
try {
|
|
520
|
+
containerId = execSync("docker ps -q --filter 'name=sandbox-browser' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
521
|
+
}
|
|
522
|
+
catch { /* ignore */ }
|
|
523
|
+
}
|
|
524
|
+
if (containerId) {
|
|
525
|
+
const envOutput = execSync(`docker inspect ${containerId} -f '{{range .Config.Env}}{{println .}}{{end}}'`, { timeout: 5000, env: dockerEnvLocal }).toString();
|
|
526
|
+
for (const line of envOutput.split('\n')) {
|
|
527
|
+
if (line.startsWith('OPENCLAW_BROWSER_NOVNC_PASSWORD=')) {
|
|
528
|
+
password = line.slice('OPENCLAW_BROWSER_NOVNC_PASSWORD='.length).trim();
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch { /* ignore */ }
|
|
535
|
+
ack({ ok: true, result: { password } });
|
|
536
|
+
// ── Desktop container lifecycle ─────────────────────────────────────
|
|
537
|
+
}
|
|
538
|
+
else if (payload.method === 'desktop-container-create') {
|
|
539
|
+
ack({ ok: true, result: desktop.createContainer(payload.params) });
|
|
540
|
+
}
|
|
541
|
+
else if (payload.method === 'desktop-container-list') {
|
|
542
|
+
ack({ ok: true, result: desktop.listContainers() });
|
|
543
|
+
}
|
|
544
|
+
else if (payload.method === 'desktop-container-start') {
|
|
545
|
+
ack({ ok: true, result: desktop.startContainer(payload.params.id) });
|
|
546
|
+
}
|
|
547
|
+
else if (payload.method === 'desktop-container-stop') {
|
|
548
|
+
ack({ ok: true, result: desktop.stopContainer(payload.params.id) });
|
|
549
|
+
}
|
|
550
|
+
else if (payload.method === 'desktop-container-remove') {
|
|
551
|
+
ack({ ok: true, result: desktop.removeContainer(payload.params.id) });
|
|
552
|
+
}
|
|
553
|
+
else if (payload.method === 'desktop-container-logs') {
|
|
554
|
+
ack({ ok: true, result: { logs: desktop.getContainerLogs(payload.params.id, payload.params.tail) } });
|
|
555
|
+
}
|
|
556
|
+
else if (payload.method === 'desktop-container-launch-app') {
|
|
557
|
+
ack({ ok: true, result: desktop.launchApp(payload.params.id, payload.params.app, payload.params.appArgs) });
|
|
558
|
+
// ── Desktop AT-SPI proxy ────────────────────────────────────────────
|
|
559
|
+
}
|
|
560
|
+
else if (payload.method === 'desktop-atspi-health') {
|
|
561
|
+
ack({ ok: true, result: desktop.atspiHealth(payload.params.agentPort) });
|
|
562
|
+
}
|
|
563
|
+
else if (payload.method === 'desktop-atspi-apps') {
|
|
564
|
+
ack({ ok: true, result: desktop.atspiApps(payload.params.agentPort) });
|
|
565
|
+
}
|
|
566
|
+
else if (payload.method === 'desktop-atspi-tree') {
|
|
567
|
+
ack({ ok: true, result: desktop.atspiTree(payload.params.agentPort, { app: payload.params.app, maxDepth: payload.params.maxDepth }) });
|
|
568
|
+
}
|
|
569
|
+
else if (payload.method === 'desktop-atspi-find') {
|
|
570
|
+
const { agentPort, ...criteria } = payload.params;
|
|
571
|
+
ack({ ok: true, result: desktop.atspiFind(agentPort, criteria) });
|
|
572
|
+
}
|
|
573
|
+
else if (payload.method === 'desktop-atspi-action') {
|
|
574
|
+
ack({ ok: true, result: desktop.atspiAction(payload.params.agentPort, payload.params.path, payload.params.action) });
|
|
575
|
+
}
|
|
576
|
+
else if (payload.method === 'desktop-atspi-type') {
|
|
577
|
+
ack({ ok: true, result: desktop.atspiType(payload.params.agentPort, payload.params.path, payload.params.text) });
|
|
578
|
+
}
|
|
579
|
+
else if (payload.method === 'desktop-atspi-key') {
|
|
580
|
+
ack({ ok: true, result: desktop.atspiKey(payload.params.agentPort, payload.params.key, payload.params.modifiers) });
|
|
581
|
+
}
|
|
582
|
+
else if (payload.method === 'desktop-atspi-click') {
|
|
583
|
+
ack({ ok: true, result: desktop.atspiClick(payload.params.agentPort, payload.params.x, payload.params.y, payload.params.button) });
|
|
584
|
+
}
|
|
585
|
+
else if (payload.method === 'desktop-atspi-screenshot') {
|
|
586
|
+
ack({ ok: true, result: { data: desktop.atspiScreenshot(payload.params.agentPort, { bounds: payload.params.bounds, quality: payload.params.quality }) } });
|
|
587
|
+
// ── Desktop screen VNC relay ────────────────────────────────────────
|
|
588
|
+
}
|
|
589
|
+
else if (payload.method === 'desktop-clipboard-type') {
|
|
590
|
+
ack({ ok: true, result: desktop.clipboardType(payload.params.id, payload.params.text) });
|
|
591
|
+
}
|
|
592
|
+
else if (payload.method === 'desktop-xdotool-key') {
|
|
593
|
+
ack({ ok: true, result: desktop.xdotoolKey(payload.params.id, payload.params.keys) });
|
|
594
|
+
}
|
|
595
|
+
else if (payload.method === 'desktop-screen-open') {
|
|
596
|
+
const { channelId, containerId } = payload.params;
|
|
597
|
+
log?.info(`[gateway] desktop-screen-open channelId=${channelId} containerId=${containerId}`);
|
|
598
|
+
// Find the noVNC port for the desktop container
|
|
599
|
+
const noVncPort = desktop.isInContainer
|
|
600
|
+
? parseInt(process.env.NOVNC_PORT || "6080", 10)
|
|
601
|
+
: desktop.findContainerPort(containerId, 6080);
|
|
602
|
+
if (!noVncPort) {
|
|
603
|
+
ack({ ok: false, error: "Desktop container noVNC port not found" });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const noVncHost = process.env.NOVNC_HOST || 'localhost';
|
|
607
|
+
let dockerWs;
|
|
608
|
+
try {
|
|
609
|
+
const WsModule = await import("ws");
|
|
610
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
611
|
+
dockerWs = new WsCtor(`ws://${noVncHost}:${noVncPort}/websockify`);
|
|
612
|
+
dockerWs.binaryType = "arraybuffer";
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
ack({ ok: false, error: `Failed to connect to noVNC: ${String(err)}` });
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Open dedicated relay WS back to the server
|
|
619
|
+
const desktopRelayUrl = `${apiBaseUrl.replace(/^http/, 'ws')}/v1/ws-relay/${channelId}/ws?token=${encodeURIComponent(gatewayToken)}`;
|
|
620
|
+
let relayWs;
|
|
446
621
|
try {
|
|
447
|
-
const
|
|
448
|
-
|
|
622
|
+
const WsModule = await import("ws");
|
|
623
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
624
|
+
relayWs = new WsCtor(desktopRelayUrl);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
449
627
|
try {
|
|
450
|
-
|
|
628
|
+
dockerWs.close();
|
|
451
629
|
}
|
|
452
630
|
catch { /* ignore */ }
|
|
453
|
-
|
|
631
|
+
ack({ ok: false, error: `Failed to connect relay WS: ${String(err)}` });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
screenConnections.set(channelId, { dockerWs, relayWs });
|
|
635
|
+
const cleanupDesktop = (reason) => {
|
|
636
|
+
const entry = screenConnections.get(channelId);
|
|
637
|
+
if (!entry)
|
|
638
|
+
return;
|
|
639
|
+
screenConnections.delete(channelId);
|
|
640
|
+
try {
|
|
641
|
+
if (entry.dockerWs.readyState !== 3 /* CLOSED */)
|
|
642
|
+
entry.dockerWs.close();
|
|
643
|
+
}
|
|
644
|
+
catch { /* ignore */ }
|
|
645
|
+
try {
|
|
646
|
+
if (entry.relayWs.readyState !== 3 /* CLOSED */)
|
|
647
|
+
entry.relayWs.close();
|
|
648
|
+
}
|
|
649
|
+
catch { /* ignore */ }
|
|
650
|
+
log?.info(`[gateway] desktop-screen: closed channelId=${channelId} reason=${reason}`);
|
|
651
|
+
};
|
|
652
|
+
dockerWs.on("open", () => {
|
|
653
|
+
log?.info(`[gateway] desktop-screen: connected to noVNC on port ${noVncPort}`);
|
|
654
|
+
});
|
|
655
|
+
relayWs.on("open", () => {
|
|
656
|
+
log?.info(`[gateway] desktop-screen: relay WS connected channelId=${channelId}`);
|
|
657
|
+
});
|
|
658
|
+
// Raw binary pipe: Docker noVNC ↔ relay WS (server ↔ browser)
|
|
659
|
+
dockerWs.on("message", (data) => {
|
|
660
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
661
|
+
relayWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
relayWs.on("message", (data) => {
|
|
665
|
+
if (dockerWs.readyState === 1 /* OPEN */) {
|
|
666
|
+
dockerWs.send(Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
dockerWs.on("close", () => cleanupDesktop('docker-ws-close'));
|
|
670
|
+
dockerWs.on("error", (err) => cleanupDesktop(`docker-ws-error(${err.message})`));
|
|
671
|
+
relayWs.on("close", () => cleanupDesktop('relay-ws-close'));
|
|
672
|
+
relayWs.on("error", (err) => cleanupDesktop(`relay-ws-error(${err.message})`));
|
|
673
|
+
ack({ ok: true });
|
|
674
|
+
}
|
|
675
|
+
else if (payload.method === 'desktop-screen-password') {
|
|
676
|
+
const { containerId } = payload.params;
|
|
677
|
+
ack({ ok: true, result: { password: desktop.getVncPassword(containerId) } });
|
|
678
|
+
// ── Bot terminal PTY (dedicated WS pipe) ─────────────────────────────
|
|
679
|
+
}
|
|
680
|
+
else if (payload.method === 'bot-terminal-open') {
|
|
681
|
+
const { channelId, containerId } = payload.params;
|
|
682
|
+
log?.info(`[gateway] bot-terminal-open channelId=${channelId} containerId=${containerId ?? 'self'}`);
|
|
683
|
+
try {
|
|
684
|
+
const nodePty = await import("node-pty");
|
|
685
|
+
const cols = 80;
|
|
686
|
+
const rows = 24;
|
|
687
|
+
const env = { TERM: 'xterm-256color', ...process.env };
|
|
688
|
+
let ptyProcess;
|
|
689
|
+
if (desktop.isInContainer) {
|
|
690
|
+
ptyProcess = nodePty.spawn('/bin/bash', ['-l'], { cols, rows, cwd: '/', env });
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
const dockerEnvLocal = { ...env, PATH: `${process.env.PATH || ''}:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin` };
|
|
694
|
+
let targetContainer = containerId;
|
|
695
|
+
if (!targetContainer) {
|
|
696
|
+
try {
|
|
697
|
+
targetContainer = execSync("docker ps -q --filter 'label=openclaw.sandboxBrowser=1' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
698
|
+
}
|
|
699
|
+
catch { /* ignore */ }
|
|
700
|
+
if (!targetContainer) {
|
|
701
|
+
try {
|
|
702
|
+
targetContainer = execSync("docker ps -q --filter 'name=sandbox-browser' | head -1", { timeout: 5000, env: dockerEnvLocal }).toString().trim();
|
|
703
|
+
}
|
|
704
|
+
catch { /* ignore */ }
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (!targetContainer) {
|
|
708
|
+
ack({ ok: false, error: 'No Docker container found for terminal' });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
ptyProcess = nodePty.spawn('docker', ['exec', '-it', targetContainer, '/bin/bash'], {
|
|
712
|
+
cols, rows, cwd: '/', env: dockerEnvLocal,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
// Open dedicated WS back to the server relay endpoint
|
|
716
|
+
const relayUrl = `${apiBaseUrl.replace(/^http/, 'ws')}/v1/ws-relay/${channelId}/ws?token=${encodeURIComponent(gatewayToken)}`;
|
|
717
|
+
const WsModule = await import("ws");
|
|
718
|
+
const WsCtor = WsModule.default ?? WsModule;
|
|
719
|
+
const relayWs = new WsCtor(relayUrl);
|
|
720
|
+
terminalConnections.set(channelId, { pty: ptyProcess, ws: relayWs });
|
|
721
|
+
const cleanupTerminal = (reason) => {
|
|
722
|
+
const entry = terminalConnections.get(channelId);
|
|
723
|
+
if (!entry)
|
|
724
|
+
return;
|
|
725
|
+
terminalConnections.delete(channelId);
|
|
454
726
|
try {
|
|
455
|
-
|
|
727
|
+
entry.pty.kill();
|
|
456
728
|
}
|
|
457
729
|
catch { /* ignore */ }
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
730
|
+
try {
|
|
731
|
+
if (entry.ws.readyState !== 3 /* CLOSED */)
|
|
732
|
+
entry.ws.close();
|
|
733
|
+
}
|
|
734
|
+
catch { /* ignore */ }
|
|
735
|
+
log?.info(`[gateway] bot-terminal: closed channelId=${channelId} reason=${reason}`);
|
|
736
|
+
};
|
|
737
|
+
relayWs.on("open", () => {
|
|
738
|
+
log?.info(`[gateway] bot-terminal: relay WS connected channelId=${channelId}`);
|
|
739
|
+
});
|
|
740
|
+
// PTY → relay WS (binary)
|
|
741
|
+
ptyProcess.onData((data) => {
|
|
742
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
743
|
+
relayWs.send(Buffer.from(data));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
// Relay WS → PTY (text input or JSON control messages)
|
|
747
|
+
relayWs.on("message", (raw) => {
|
|
748
|
+
const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
749
|
+
const str = buf.toString();
|
|
750
|
+
// Check for JSON control messages (resize)
|
|
751
|
+
try {
|
|
752
|
+
const msg = JSON.parse(str);
|
|
753
|
+
if (msg.type === 'resize' && typeof msg.cols === 'number' && typeof msg.rows === 'number') {
|
|
754
|
+
ptyProcess.resize(msg.cols, msg.rows);
|
|
755
|
+
return;
|
|
465
756
|
}
|
|
466
757
|
}
|
|
467
|
-
|
|
758
|
+
catch {
|
|
759
|
+
// Not JSON — treat as terminal input
|
|
760
|
+
}
|
|
761
|
+
ptyProcess.write(str);
|
|
762
|
+
});
|
|
763
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
764
|
+
// Send pty_exit control message before closing
|
|
765
|
+
if (relayWs.readyState === 1 /* OPEN */) {
|
|
766
|
+
relayWs.send(JSON.stringify({ type: 'pty_exit', exitCode }));
|
|
767
|
+
}
|
|
768
|
+
cleanupTerminal(`pty-exit(${exitCode})`);
|
|
769
|
+
});
|
|
770
|
+
relayWs.on("close", () => cleanupTerminal('relay-ws-close'));
|
|
771
|
+
relayWs.on("error", (err) => cleanupTerminal(`relay-ws-error(${err.message})`));
|
|
772
|
+
ack({ ok: true });
|
|
468
773
|
}
|
|
469
|
-
catch {
|
|
470
|
-
|
|
774
|
+
catch (err) {
|
|
775
|
+
log?.warn(`[gateway] bot-terminal-open failed: ${String(err)}`);
|
|
776
|
+
ack({ ok: false, error: `Failed to open terminal: ${String(err)}` });
|
|
777
|
+
}
|
|
778
|
+
// ── WeChat monitor ──────────────────────────────────────────────────
|
|
779
|
+
}
|
|
780
|
+
else if (payload.method === 'wechat-monitor-start') {
|
|
781
|
+
const { containerId, ...monitorConfig } = payload.params;
|
|
782
|
+
const existing = getMonitor(containerId);
|
|
783
|
+
if (existing) {
|
|
784
|
+
ack({ ok: true, result: existing.getStatus() });
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const monitor = new WeChatMonitorSession(containerId, {
|
|
788
|
+
...monitorConfig,
|
|
789
|
+
onNewMessage: (event) => {
|
|
790
|
+
sock.emit("wechat-monitor-event", event);
|
|
791
|
+
},
|
|
792
|
+
onError: (error) => {
|
|
793
|
+
log?.warn(`[wechat-monitor] error: ${error.message}`);
|
|
794
|
+
},
|
|
795
|
+
}, log);
|
|
796
|
+
await monitor.start();
|
|
797
|
+
ack({ ok: true, result: monitor.getStatus() });
|
|
798
|
+
}
|
|
799
|
+
else if (payload.method === 'wechat-monitor-stop') {
|
|
800
|
+
const { containerId } = payload.params;
|
|
801
|
+
const monitor = getMonitor(containerId);
|
|
802
|
+
if (monitor) {
|
|
803
|
+
monitor.stop();
|
|
804
|
+
ack({ ok: true, result: { stopped: true } });
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
ack({ ok: true, result: { stopped: false, reason: "not running" } });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else if (payload.method === 'wechat-monitor-status') {
|
|
811
|
+
const { containerId } = payload.params;
|
|
812
|
+
const monitor = getMonitor(containerId);
|
|
813
|
+
ack({ ok: true, result: monitor ? monitor.getStatus() : { running: false, containerId } });
|
|
471
814
|
}
|
|
472
815
|
else {
|
|
473
816
|
ack({ ok: false, error: `Unknown RPC method: ${payload.method}` });
|
|
@@ -477,15 +820,36 @@ export async function startGatewayMode(options) {
|
|
|
477
820
|
ack({ ok: false, error: err instanceof Error ? err.message : 'RPC handler error' });
|
|
478
821
|
}
|
|
479
822
|
});
|
|
480
|
-
// Clean up all
|
|
823
|
+
// Clean up all screen connections, terminal connections, and monitors on socket disconnect
|
|
481
824
|
sock.on("disconnect", () => {
|
|
482
|
-
for (const [,
|
|
825
|
+
for (const [, entry] of screenConnections) {
|
|
483
826
|
try {
|
|
484
|
-
|
|
827
|
+
if (entry.dockerWs.readyState !== 3)
|
|
828
|
+
entry.dockerWs.close();
|
|
485
829
|
}
|
|
486
830
|
catch { /* ignore */ }
|
|
831
|
+
try {
|
|
832
|
+
if (entry.relayWs.readyState !== 3)
|
|
833
|
+
entry.relayWs.close();
|
|
834
|
+
}
|
|
835
|
+
catch { /* ignore */ }
|
|
836
|
+
}
|
|
837
|
+
screenConnections.clear();
|
|
838
|
+
for (const [, entry] of terminalConnections) {
|
|
839
|
+
try {
|
|
840
|
+
entry.pty.kill();
|
|
841
|
+
}
|
|
842
|
+
catch { /* ignore */ }
|
|
843
|
+
try {
|
|
844
|
+
if (entry.ws.readyState !== 3)
|
|
845
|
+
entry.ws.close();
|
|
846
|
+
}
|
|
847
|
+
catch { /* ignore */ }
|
|
848
|
+
}
|
|
849
|
+
terminalConnections.clear();
|
|
850
|
+
for (const [, monitor] of getAllMonitors()) {
|
|
851
|
+
monitor.stop();
|
|
487
852
|
}
|
|
488
|
-
sandboxConnections.clear();
|
|
489
853
|
});
|
|
490
854
|
}
|
|
491
855
|
catch (err) {
|