@ozaiya/openclaw-channel 0.10.16 → 0.10.17
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.
|
@@ -2,33 +2,37 @@
|
|
|
2
2
|
* Records the bot's sandbox/desktop display (Xvfb :1) to an MP4 while a task
|
|
3
3
|
* runs, so the chat app can replay the task as synchronized video + steps.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Three modes, chosen automatically:
|
|
6
|
+
* - "docker" (host gateway): the recorder runs on the host and `docker exec`s
|
|
7
|
+
* ffmpeg into the browser/desktop container, then `docker cp`s the file out.
|
|
8
|
+
* - "http" (compose gateway, e.g. test-two): the agent runs INSIDE a container
|
|
9
|
+
* that has no display — the display lives in a sibling browser container. We
|
|
10
|
+
* can't x11grab it, so the browser container runs a tiny recording-control
|
|
11
|
+
* HTTP server (POST /start, POST /stop -> mp4 bytes) reachable at NOVNC_HOST.
|
|
12
|
+
* - "self" (single-container deployments): record the local :1 directly.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
* container exists, or ffmpeg isn't present in the image, startRecording()
|
|
11
|
-
* returns null and the task simply has no video (steps-only replay).
|
|
14
|
+
* Best-effort throughout: any failure -> null -> the task has steps-only replay.
|
|
12
15
|
*/
|
|
13
16
|
import { type ChildProcess } from "node:child_process";
|
|
14
17
|
export interface RecordingHandle {
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
mode: "self" | "docker" | "http";
|
|
19
|
+
/** docker/self mode: the container id ("self") and in-container mp4 path. */
|
|
20
|
+
containerId?: string;
|
|
21
|
+
remotePath?: string;
|
|
22
|
+
proc?: ChildProcess | null;
|
|
23
|
+
/** http mode: base URL of the browser container's recording-control server. */
|
|
24
|
+
controlUrl?: string;
|
|
18
25
|
/** Date.now() at recording start — the t=0 baseline for step offsets. */
|
|
19
26
|
startedAt: number;
|
|
20
|
-
proc: ChildProcess | null;
|
|
21
27
|
display: string;
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
|
-
* Start recording
|
|
25
|
-
*
|
|
26
|
-
* "steps-only, no video").
|
|
30
|
+
* Start recording. Returns a handle, or null when nothing is recordable
|
|
31
|
+
* (caller treats null as "steps-only, no video").
|
|
27
32
|
*/
|
|
28
33
|
export declare function startRecording(taskId: string): RecordingHandle | null;
|
|
29
34
|
/**
|
|
30
|
-
* Stop recording
|
|
31
|
-
* MP4 bytes + duration. Returns null on any failure.
|
|
35
|
+
* Stop recording and return the MP4 bytes + duration. Returns null on any failure.
|
|
32
36
|
*/
|
|
33
37
|
export declare function stopAndExtractRecording(handle: RecordingHandle): Promise<{
|
|
34
38
|
mp4: Buffer;
|
|
@@ -2,26 +2,29 @@
|
|
|
2
2
|
* Records the bot's sandbox/desktop display (Xvfb :1) to an MP4 while a task
|
|
3
3
|
* runs, so the chat app can replay the task as synchronized video + steps.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Three modes, chosen automatically:
|
|
6
|
+
* - "docker" (host gateway): the recorder runs on the host and `docker exec`s
|
|
7
|
+
* ffmpeg into the browser/desktop container, then `docker cp`s the file out.
|
|
8
|
+
* - "http" (compose gateway, e.g. test-two): the agent runs INSIDE a container
|
|
9
|
+
* that has no display — the display lives in a sibling browser container. We
|
|
10
|
+
* can't x11grab it, so the browser container runs a tiny recording-control
|
|
11
|
+
* HTTP server (POST /start, POST /stop -> mp4 bytes) reachable at NOVNC_HOST.
|
|
12
|
+
* - "self" (single-container deployments): record the local :1 directly.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
* container exists, or ffmpeg isn't present in the image, startRecording()
|
|
11
|
-
* returns null and the task simply has no video (steps-only replay).
|
|
14
|
+
* Best-effort throughout: any failure -> null -> the task has steps-only replay.
|
|
12
15
|
*/
|
|
13
16
|
import { execSync, spawn } from "node:child_process";
|
|
14
17
|
import * as fs from "node:fs";
|
|
15
18
|
import * as os from "node:os";
|
|
16
19
|
import * as path from "node:path";
|
|
17
20
|
import { docker, dockerEnv, dockerExec, isInContainer } from "./desktopContainer.js";
|
|
21
|
+
const RECORD_CONTROL_PORT = process.env.RECORD_CONTROL_PORT || "8091";
|
|
18
22
|
/** Find a running container whose display we can record (sandbox browser, then desktop). */
|
|
19
23
|
function findRecordableContainer() {
|
|
20
|
-
if (isInContainer)
|
|
21
|
-
return "self";
|
|
22
24
|
const filters = [
|
|
23
25
|
"docker ps -q --filter 'label=openclaw.sandboxBrowser=1'",
|
|
24
26
|
"docker ps -q --filter 'name=sandbox-browser'",
|
|
27
|
+
"docker ps -q --filter 'ancestor=openclaw-sandbox-browser:bookworm-slim'",
|
|
25
28
|
"docker ps -q --filter 'label=ozaiya.desktop=1'",
|
|
26
29
|
];
|
|
27
30
|
for (const f of filters) {
|
|
@@ -36,7 +39,6 @@ function findRecordableContainer() {
|
|
|
36
39
|
}
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
39
|
-
/** Whether ffmpeg is available inside the container (or on the host, when in-container). */
|
|
40
42
|
function hasFfmpeg(containerId) {
|
|
41
43
|
try {
|
|
42
44
|
if (containerId === "self") {
|
|
@@ -54,89 +56,93 @@ function hasFfmpeg(containerId) {
|
|
|
54
56
|
function sanitize(taskId) {
|
|
55
57
|
return taskId.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
56
58
|
}
|
|
59
|
+
function ffArgs(display, out) {
|
|
60
|
+
return [
|
|
61
|
+
"-y", "-f", "x11grab", "-draw_mouse", "1", "-framerate", "8",
|
|
62
|
+
"-i", display, "-codec:v", "libx264", "-preset", "veryfast",
|
|
63
|
+
"-pix_fmt", "yuv420p", "-movflags", "+faststart", out,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
57
66
|
/**
|
|
58
|
-
* Start recording
|
|
59
|
-
*
|
|
60
|
-
* "steps-only, no video").
|
|
67
|
+
* Start recording. Returns a handle, or null when nothing is recordable
|
|
68
|
+
* (caller treats null as "steps-only, no video").
|
|
61
69
|
*/
|
|
62
70
|
export function startRecording(taskId) {
|
|
63
|
-
const
|
|
71
|
+
const display = process.env.RECORD_DISPLAY || ":1";
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
// Compose mode: agent container + separate browser container (NOVNC_HOST set).
|
|
74
|
+
// The browser container hosts the recording-control server.
|
|
75
|
+
if (isInContainer && process.env.NOVNC_HOST) {
|
|
76
|
+
const controlUrl = `http://${process.env.NOVNC_HOST}:${RECORD_CONTROL_PORT}`;
|
|
77
|
+
// Fire-and-forget; if the control server is absent the POST just fails.
|
|
78
|
+
fetch(`${controlUrl}/start`, { method: "POST", signal: AbortSignal.timeout(8000) }).catch(() => { });
|
|
79
|
+
return { mode: "http", controlUrl, startedAt, display };
|
|
80
|
+
}
|
|
81
|
+
const containerId = findRecordableContainer() ?? (isInContainer ? "self" : null);
|
|
64
82
|
if (!containerId)
|
|
65
83
|
return null;
|
|
66
84
|
if (!hasFfmpeg(containerId))
|
|
67
85
|
return null;
|
|
68
|
-
const display = process.env.RECORD_DISPLAY || ":1";
|
|
69
86
|
const remotePath = `/tmp/rec-${sanitize(taskId)}.mp4`;
|
|
70
|
-
const
|
|
71
|
-
"-y",
|
|
72
|
-
"-f", "x11grab",
|
|
73
|
-
"-draw_mouse", "1",
|
|
74
|
-
"-framerate", "8",
|
|
75
|
-
"-i", display,
|
|
76
|
-
"-codec:v", "libx264",
|
|
77
|
-
"-preset", "veryfast",
|
|
78
|
-
"-pix_fmt", "yuv420p",
|
|
79
|
-
"-movflags", "+faststart",
|
|
80
|
-
remotePath,
|
|
81
|
-
];
|
|
87
|
+
const args = ffArgs(display, remotePath);
|
|
82
88
|
let proc;
|
|
83
89
|
if (containerId === "self") {
|
|
84
|
-
proc = spawn("ffmpeg",
|
|
90
|
+
proc = spawn("ffmpeg", args, { env: process.env, stdio: "ignore" });
|
|
85
91
|
}
|
|
86
92
|
else {
|
|
87
|
-
|
|
88
|
-
proc = spawn("docker", ["exec", containerId, "ffmpeg", ...ffArgs], { env: dockerEnv, stdio: "ignore" });
|
|
93
|
+
proc = spawn("docker", ["exec", containerId, "ffmpeg", ...args], { env: dockerEnv, stdio: "ignore" });
|
|
89
94
|
}
|
|
90
95
|
proc.unref?.();
|
|
91
96
|
proc.on("error", () => { });
|
|
92
|
-
return { containerId
|
|
97
|
+
return { mode: containerId === "self" ? "self" : "docker", containerId, remotePath, proc, startedAt, display };
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
|
-
* Stop recording
|
|
96
|
-
* MP4 bytes + duration. Returns null on any failure.
|
|
100
|
+
* Stop recording and return the MP4 bytes + duration. Returns null on any failure.
|
|
97
101
|
*/
|
|
98
102
|
export async function stopAndExtractRecording(handle) {
|
|
103
|
+
const durationMs = Date.now() - handle.startedAt;
|
|
99
104
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
if (handle.mode === "http") {
|
|
106
|
+
const res = await fetch(`${handle.controlUrl}/stop`, { method: "POST", signal: AbortSignal.timeout(30_000) });
|
|
107
|
+
if (!res.ok)
|
|
108
|
+
return null;
|
|
109
|
+
const mp4 = Buffer.from(await res.arrayBuffer());
|
|
110
|
+
return mp4.length > 0 ? { mp4, durationMs } : null;
|
|
111
|
+
}
|
|
112
|
+
const cid = handle.containerId;
|
|
113
|
+
const remotePath = handle.remotePath;
|
|
114
|
+
// SIGINT ffmpeg INSIDE the container so it finalizes the moov atom.
|
|
115
|
+
const interrupt = `pkill -INT -f '${remotePath}'`;
|
|
102
116
|
try {
|
|
103
|
-
if (
|
|
117
|
+
if (cid === "self")
|
|
104
118
|
execSync(interrupt, { timeout: 5000, env: process.env });
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
dockerExec(handle.containerId, interrupt, 5000);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
/* maybe already exiting */
|
|
119
|
+
else
|
|
120
|
+
dockerExec(cid, interrupt, 5000);
|
|
112
121
|
}
|
|
113
|
-
|
|
122
|
+
catch { /* maybe already exiting */ }
|
|
114
123
|
await new Promise((r) => setTimeout(r, 1500));
|
|
115
124
|
let mp4;
|
|
116
|
-
if (
|
|
117
|
-
mp4 = fs.readFileSync(
|
|
125
|
+
if (cid === "self") {
|
|
126
|
+
mp4 = fs.readFileSync(remotePath);
|
|
118
127
|
}
|
|
119
128
|
else {
|
|
120
129
|
const hostTmp = path.join(os.tmpdir(), `ozaiya-rec-${handle.startedAt}.mp4`);
|
|
121
|
-
docker(`cp ${
|
|
130
|
+
docker(`cp ${cid}:${remotePath} ${hostTmp}`, 30_000);
|
|
122
131
|
mp4 = fs.readFileSync(hostTmp);
|
|
123
132
|
try {
|
|
124
133
|
fs.unlinkSync(hostTmp);
|
|
125
134
|
}
|
|
126
135
|
catch { /* ignore */ }
|
|
127
136
|
}
|
|
128
|
-
// Best-effort cleanup of the in-container file.
|
|
129
137
|
try {
|
|
130
|
-
const rm = `rm -f ${
|
|
131
|
-
if (
|
|
138
|
+
const rm = `rm -f ${remotePath}`;
|
|
139
|
+
if (cid === "self")
|
|
132
140
|
execSync(rm, { timeout: 5000 });
|
|
133
141
|
else
|
|
134
|
-
dockerExec(
|
|
142
|
+
dockerExec(cid, rm, 5000);
|
|
135
143
|
}
|
|
136
144
|
catch { /* ignore */ }
|
|
137
|
-
|
|
138
|
-
return null;
|
|
139
|
-
return { mp4, durationMs: Date.now() - handle.startedAt };
|
|
145
|
+
return mp4 && mp4.length > 0 ? { mp4, durationMs } : null;
|
|
140
146
|
}
|
|
141
147
|
catch {
|
|
142
148
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"desktopRecorder.js","sourceRoot":"","sources":["../../src/desktopRecorder.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"desktopRecorder.js","sourceRoot":"","sources":["../../src/desktopRecorder.ts"],"names":[],"mappings":"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"}
|