@elizaos/plugin-browser 2.0.0-beta.1 → 2.0.3-beta.3
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/LICENSE +21 -0
- package/README.md +106 -64
- package/dist/actions/browser-autofill-login.d.ts.map +1 -1
- package/dist/actions/browser-autofill-login.js.map +1 -1
- package/dist/actions/browser.d.ts +5 -6
- package/dist/actions/browser.d.ts.map +1 -1
- package/dist/actions/browser.js +312 -60
- package/dist/actions/browser.js.map +1 -1
- package/dist/actions/manage-browser-bridge.d.ts.map +1 -1
- package/dist/actions/manage-browser-bridge.js +10 -14
- package/dist/actions/manage-browser-bridge.js.map +1 -1
- package/dist/actions/wait-for-url-predicate.d.ts +34 -0
- package/dist/actions/wait-for-url-predicate.d.ts.map +1 -0
- package/dist/actions/wait-for-url-predicate.js +33 -0
- package/dist/actions/wait-for-url-predicate.js.map +1 -0
- package/dist/actions/wait-for-url.d.ts +64 -0
- package/dist/actions/wait-for-url.d.ts.map +1 -0
- package/dist/actions/wait-for-url.js +89 -0
- package/dist/actions/wait-for-url.js.map +1 -0
- package/dist/bridge-policy.d.ts +10 -0
- package/dist/bridge-policy.d.ts.map +1 -0
- package/dist/bridge-policy.js +37 -0
- package/dist/bridge-policy.js.map +1 -0
- package/dist/bridge-readiness.d.ts +16 -0
- package/dist/bridge-readiness.d.ts.map +1 -0
- package/dist/bridge-readiness.js +82 -0
- package/dist/bridge-readiness.js.map +1 -0
- package/dist/bridge-records.d.ts +9 -0
- package/dist/bridge-records.d.ts.map +1 -0
- package/dist/bridge-records.js +37 -0
- package/dist/bridge-records.js.map +1 -0
- package/dist/browser-capture-hooks.d.ts +9 -0
- package/dist/browser-capture-hooks.d.ts.map +1 -0
- package/dist/browser-capture-hooks.js +15 -0
- package/dist/browser-capture-hooks.js.map +1 -0
- package/dist/browser-service.d.ts +22 -4
- package/dist/browser-service.d.ts.map +1 -1
- package/dist/browser-service.js +63 -15
- package/dist/browser-service.js.map +1 -1
- package/dist/browser-workspace-hooks.d.ts +14 -0
- package/dist/browser-workspace-hooks.d.ts.map +1 -0
- package/dist/browser-workspace-hooks.js +15 -0
- package/dist/browser-workspace-hooks.js.map +1 -0
- package/dist/companion-auth.d.ts +34 -0
- package/dist/companion-auth.d.ts.map +1 -0
- package/dist/companion-auth.js +98 -0
- package/dist/companion-auth.js.map +1 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -12
- package/dist/index.js.map +1 -1
- package/dist/message-adapter.d.ts +9 -0
- package/dist/message-adapter.d.ts.map +1 -0
- package/dist/message-adapter.js +104 -0
- package/dist/message-adapter.js.map +1 -0
- package/dist/packaging.d.ts.map +1 -1
- package/dist/packaging.js +2 -0
- package/dist/packaging.js.map +1 -1
- package/dist/parity/browser-matrix.d.ts +45 -0
- package/dist/parity/browser-matrix.d.ts.map +1 -0
- package/dist/parity/browser-matrix.js +361 -0
- package/dist/parity/browser-matrix.js.map +1 -0
- package/dist/parity/index.d.ts +5 -0
- package/dist/parity/index.d.ts.map +1 -0
- package/dist/parity/index.js +13 -0
- package/dist/parity/index.js.map +1 -0
- package/dist/password-manager-bridge.d.ts +50 -0
- package/dist/password-manager-bridge.d.ts.map +1 -0
- package/dist/password-manager-bridge.js +437 -0
- package/dist/password-manager-bridge.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +8 -4
- package/dist/plugin.js.map +1 -1
- package/dist/providers/workspace.d.ts +1 -1
- package/dist/providers/workspace.js.map +1 -1
- package/dist/routes/bridge.d.ts.map +1 -1
- package/dist/routes/bridge.js +63 -14
- package/dist/routes/bridge.js.map +1 -1
- package/dist/routes/workspace-setup.d.ts.map +1 -1
- package/dist/routes/workspace-setup.js +1 -1
- package/dist/routes/workspace-setup.js.map +1 -1
- package/dist/routes/workspace.d.ts +1 -2
- package/dist/routes/workspace.d.ts.map +1 -1
- package/dist/routes/workspace.js +104 -4
- package/dist/routes/workspace.js.map +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js.map +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js.map +1 -1
- package/dist/targets/bridge-target.d.ts +1 -1
- package/dist/targets/bridge-target.d.ts.map +1 -1
- package/dist/targets/bridge-target.js.map +1 -1
- package/dist/targets/stagehand-target.d.ts +3 -0
- package/dist/targets/stagehand-target.d.ts.map +1 -0
- package/dist/targets/stagehand-target.js +187 -0
- package/dist/targets/stagehand-target.js.map +1 -0
- package/dist/workspace/browser-capture.d.ts +1 -1
- package/dist/workspace/browser-capture.d.ts.map +1 -1
- package/dist/workspace/browser-capture.js +33 -1
- package/dist/workspace/browser-capture.js.map +1 -1
- package/dist/workspace/browser-workspace-desktop.d.ts +1 -1
- package/dist/workspace/browser-workspace-desktop.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-desktop.js +66 -30
- package/dist/workspace/browser-workspace-desktop.js.map +1 -1
- package/dist/workspace/browser-workspace-errors.d.ts +62 -0
- package/dist/workspace/browser-workspace-errors.d.ts.map +1 -0
- package/dist/workspace/browser-workspace-errors.js +69 -0
- package/dist/workspace/browser-workspace-errors.js.map +1 -0
- package/dist/workspace/browser-workspace-forms.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-forms.js +1 -1
- package/dist/workspace/browser-workspace-forms.js.map +1 -1
- package/dist/workspace/browser-workspace-helpers.d.ts +7 -0
- package/dist/workspace/browser-workspace-helpers.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-helpers.js +64 -6
- package/dist/workspace/browser-workspace-helpers.js.map +1 -1
- package/dist/workspace/browser-workspace-network.d.ts +1 -1
- package/dist/workspace/browser-workspace-network.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-types.d.ts +15 -0
- package/dist/workspace/browser-workspace-types.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-types.js.map +1 -1
- package/dist/workspace/browser-workspace-web.d.ts.map +1 -1
- package/dist/workspace/browser-workspace-web.js +34 -93
- package/dist/workspace/browser-workspace-web.js.map +1 -1
- package/dist/workspace/browser-workspace.d.ts +1 -1
- package/dist/workspace/browser-workspace.d.ts.map +1 -1
- package/dist/workspace/browser-workspace.js +9 -4
- package/dist/workspace/browser-workspace.js.map +1 -1
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/index.js.map +1 -1
- package/package.json +29 -7
- package/registry-entry.json +75 -0
- package/dist/actions/browser-autofill-login.d.js +0 -1
- package/dist/actions/browser-autofill-login.d.js.map +0 -1
- package/dist/actions/browser.d.js +0 -1
- package/dist/actions/browser.d.js.map +0 -1
- package/dist/actions/manage-browser-bridge.d.js +0 -1
- package/dist/actions/manage-browser-bridge.d.js.map +0 -1
- package/dist/ambient-jsdom.d.js +0 -1
- package/dist/ambient-jsdom.d.js.map +0 -1
- package/dist/browser-service.d.js +0 -1
- package/dist/browser-service.d.js.map +0 -1
- package/dist/contracts.d.js +0 -1
- package/dist/contracts.d.js.map +0 -1
- package/dist/index.d.js +0 -21
- package/dist/index.d.js.map +0 -1
- package/dist/lifeops-session-contracts.d.js +0 -1
- package/dist/lifeops-session-contracts.d.js.map +0 -1
- package/dist/packaging.d.js +0 -1
- package/dist/packaging.d.js.map +0 -1
- package/dist/plugin.d.js +0 -1
- package/dist/plugin.d.js.map +0 -1
- package/dist/providers/workspace.d.js +0 -1
- package/dist/providers/workspace.d.js.map +0 -1
- package/dist/routes/bridge.d.js +0 -1
- package/dist/routes/bridge.d.js.map +0 -1
- package/dist/routes/workspace-account-gate.d.js +0 -1
- package/dist/routes/workspace-account-gate.d.js.map +0 -1
- package/dist/routes/workspace-setup.d.js +0 -1
- package/dist/routes/workspace-setup.d.js.map +0 -1
- package/dist/routes/workspace.d.js +0 -1
- package/dist/routes/workspace.d.js.map +0 -1
- package/dist/schema.d.js +0 -1
- package/dist/schema.d.js.map +0 -1
- package/dist/service.d.js +0 -1
- package/dist/service.d.js.map +0 -1
- package/dist/targets/bridge-target.d.js +0 -1
- package/dist/targets/bridge-target.d.js.map +0 -1
- package/dist/workspace/browser-capture.d.js +0 -1
- package/dist/workspace/browser-capture.d.js.map +0 -1
- package/dist/workspace/browser-workspace-desktop.d.js +0 -1
- package/dist/workspace/browser-workspace-desktop.d.js.map +0 -1
- package/dist/workspace/browser-workspace-elements.d.js +0 -1
- package/dist/workspace/browser-workspace-elements.d.js.map +0 -1
- package/dist/workspace/browser-workspace-forms.d.js +0 -1
- package/dist/workspace/browser-workspace-forms.d.js.map +0 -1
- package/dist/workspace/browser-workspace-helpers.d.js +0 -1
- package/dist/workspace/browser-workspace-helpers.d.js.map +0 -1
- package/dist/workspace/browser-workspace-jsdom.d.js +0 -1
- package/dist/workspace/browser-workspace-jsdom.d.js.map +0 -1
- package/dist/workspace/browser-workspace-network.d.js +0 -1
- package/dist/workspace/browser-workspace-network.d.js.map +0 -1
- package/dist/workspace/browser-workspace-snapshots.d.js +0 -1
- package/dist/workspace/browser-workspace-snapshots.d.js.map +0 -1
- package/dist/workspace/browser-workspace-state.d.js +0 -1
- package/dist/workspace/browser-workspace-state.d.js.map +0 -1
- package/dist/workspace/browser-workspace-types.d.js +0 -1
- package/dist/workspace/browser-workspace-types.d.js.map +0 -1
- package/dist/workspace/browser-workspace-web.d.js +0 -1
- package/dist/workspace/browser-workspace-web.d.js.map +0 -1
- package/dist/workspace/browser-workspace.d.js +0 -11
- package/dist/workspace/browser-workspace.d.js.map +0 -1
- package/dist/workspace/index.d.js +0 -3
- package/dist/workspace/index.d.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { logger } from "@elizaos/core";
|
|
6
|
+
const pluginSrcDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const STAGEHAND_COMMAND_URL_ENV = [
|
|
8
|
+
"ELIZA_BROWSER_STAGEHAND_COMMAND_URL",
|
|
9
|
+
"STAGEHAND_BROWSER_COMMAND_URL",
|
|
10
|
+
"ELIZA_STAGEHAND_COMMAND_URL"
|
|
11
|
+
];
|
|
12
|
+
const STAGEHAND_BASE_URL_ENV = [
|
|
13
|
+
"ELIZA_BROWSER_STAGEHAND_URL",
|
|
14
|
+
"STAGEHAND_SERVER_URL",
|
|
15
|
+
"ELIZA_STAGEHAND_SERVER_URL"
|
|
16
|
+
];
|
|
17
|
+
const STAGEHAND_AUTO_SETUP_ENV = "ELIZA_BROWSER_STAGEHAND_AUTO_SETUP";
|
|
18
|
+
const STAGEHAND_ALLOW_MOBILE_ENV = "ELIZA_BROWSER_ALLOW_STAGEHAND_ON_MOBILE";
|
|
19
|
+
async function maybeCreateStagehandTarget(env = process.env) {
|
|
20
|
+
if (isDisabled(env.ELIZA_BROWSER_STAGEHAND_ENABLED)) return null;
|
|
21
|
+
const mobile = isMobileRuntime(env);
|
|
22
|
+
if (mobile && !isEnabled(env[STAGEHAND_ALLOW_MOBILE_ENV])) {
|
|
23
|
+
logger.debug(
|
|
24
|
+
"[BrowserService] stagehand target not registered on mobile; using the app browser surface instead"
|
|
25
|
+
);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!isDisabled(env[STAGEHAND_AUTO_SETUP_ENV])) {
|
|
29
|
+
ensureLocalStagehandServer(env);
|
|
30
|
+
}
|
|
31
|
+
const commandUrl = resolveStagehandCommandUrl(env);
|
|
32
|
+
if (!commandUrl) {
|
|
33
|
+
logger.debug(
|
|
34
|
+
"[BrowserService] stagehand target not registered; set ELIZA_BROWSER_STAGEHAND_COMMAND_URL or STAGEHAND_SERVER_URL to enable it"
|
|
35
|
+
);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: "stagehand",
|
|
40
|
+
name: "Stagehand Browser",
|
|
41
|
+
description: "Fallback Stagehand/Playwright browser backend reached through a local or remote stagehand command endpoint.",
|
|
42
|
+
kind: "stagehand",
|
|
43
|
+
priority: 10,
|
|
44
|
+
score: ({ mobile: mobileContext }) => mobileContext ? null : 10,
|
|
45
|
+
available: async () => probeStagehand(commandUrl, env),
|
|
46
|
+
execute: async (command) => executeStagehandCommand(commandUrl, command)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function resolveStagehandCommandUrl(env) {
|
|
50
|
+
for (const key of STAGEHAND_COMMAND_URL_ENV) {
|
|
51
|
+
const value = normalizeUrl(env[key]);
|
|
52
|
+
if (value) return value;
|
|
53
|
+
}
|
|
54
|
+
for (const key of STAGEHAND_BASE_URL_ENV) {
|
|
55
|
+
const value = normalizeUrl(env[key]);
|
|
56
|
+
if (value) return new URL("/api/browser-command", value).toString();
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
async function probeStagehand(_commandUrl, env) {
|
|
61
|
+
const healthUrl = normalizeUrl(env.ELIZA_BROWSER_STAGEHAND_HEALTH_URL);
|
|
62
|
+
if (!healthUrl) return true;
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(healthUrl, { method: "GET" });
|
|
65
|
+
return response.ok;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function executeStagehandCommand(commandUrl, command) {
|
|
71
|
+
const response = await fetch(commandUrl, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "content-type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ command })
|
|
75
|
+
});
|
|
76
|
+
const body = await response.json().catch(() => null);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const message = body && typeof body === "object" && "error" in body ? String(body.error) : `Stagehand command endpoint returned HTTP ${response.status}`;
|
|
79
|
+
throw new Error(message);
|
|
80
|
+
}
|
|
81
|
+
return normalizeStagehandResult(command, body);
|
|
82
|
+
}
|
|
83
|
+
function normalizeStagehandResult(command, body) {
|
|
84
|
+
if (body && typeof body === "object") {
|
|
85
|
+
const record = body;
|
|
86
|
+
const result = record.result && typeof record.result === "object" ? record.result : record;
|
|
87
|
+
return {
|
|
88
|
+
...result,
|
|
89
|
+
mode: "cloud",
|
|
90
|
+
subaction: command.subaction
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
mode: "cloud",
|
|
95
|
+
subaction: command.subaction,
|
|
96
|
+
value: body
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function ensureLocalStagehandServer(env) {
|
|
100
|
+
const stagehandDir = findStagehandDir(env);
|
|
101
|
+
if (!stagehandDir) return false;
|
|
102
|
+
const stagehandIndex = path.join(stagehandDir, "dist", "index.js");
|
|
103
|
+
if (fs.existsSync(stagehandIndex)) return true;
|
|
104
|
+
const stagehandSrc = path.join(stagehandDir, "src", "index.ts");
|
|
105
|
+
if (!fs.existsSync(stagehandSrc)) return false;
|
|
106
|
+
try {
|
|
107
|
+
if (!fs.existsSync(path.join(stagehandDir, "node_modules"))) {
|
|
108
|
+
execSync("bun install --ignore-scripts", {
|
|
109
|
+
cwd: stagehandDir,
|
|
110
|
+
stdio: "ignore",
|
|
111
|
+
timeout: 6e4
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const localTsc = path.join(stagehandDir, "node_modules", ".bin", "tsc");
|
|
115
|
+
if (fs.existsSync(localTsc)) {
|
|
116
|
+
execFileSync(localTsc, [], {
|
|
117
|
+
cwd: stagehandDir,
|
|
118
|
+
stdio: "ignore",
|
|
119
|
+
timeout: 6e4
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
execFileSync("bunx", ["tsc"], {
|
|
123
|
+
cwd: stagehandDir,
|
|
124
|
+
stdio: "ignore",
|
|
125
|
+
timeout: 6e4
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
logger.info("[BrowserService] stagehand-server built successfully");
|
|
129
|
+
return fs.existsSync(stagehandIndex);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
132
|
+
logger.debug(
|
|
133
|
+
`[BrowserService] stagehand-server auto-setup failed: ${message}`
|
|
134
|
+
);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function findStagehandDir(env) {
|
|
139
|
+
const configured = env.ELIZA_BROWSER_STAGEHAND_DIR?.trim();
|
|
140
|
+
const candidates = [
|
|
141
|
+
configured,
|
|
142
|
+
...ancestorPaths(pluginSrcDir).flatMap((root) => [
|
|
143
|
+
path.join(root, "stagehand-server"),
|
|
144
|
+
path.join(root, "plugins", "plugin-browser", "stagehand-server"),
|
|
145
|
+
path.join(root, "eliza", "plugins", "plugin-browser", "stagehand-server")
|
|
146
|
+
])
|
|
147
|
+
].filter((candidate) => Boolean(candidate));
|
|
148
|
+
for (const candidate of candidates) {
|
|
149
|
+
const dir = path.resolve(candidate);
|
|
150
|
+
if (fs.existsSync(path.join(dir, "dist", "index.js")) || fs.existsSync(path.join(dir, "src", "index.ts"))) {
|
|
151
|
+
return dir;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function ancestorPaths(start) {
|
|
157
|
+
const ancestors = [];
|
|
158
|
+
let current = path.resolve(start);
|
|
159
|
+
while (true) {
|
|
160
|
+
ancestors.push(current);
|
|
161
|
+
const parent = path.dirname(current);
|
|
162
|
+
if (parent === current) return ancestors;
|
|
163
|
+
current = parent;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function normalizeUrl(value) {
|
|
167
|
+
if (!value?.trim()) return null;
|
|
168
|
+
try {
|
|
169
|
+
return new URL(value.trim()).toString();
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isEnabled(value) {
|
|
175
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
176
|
+
}
|
|
177
|
+
function isDisabled(value) {
|
|
178
|
+
return value === "0" || value?.toLowerCase() === "false";
|
|
179
|
+
}
|
|
180
|
+
function isMobileRuntime(env) {
|
|
181
|
+
const platform = (env.ELIZA_MOBILE_PLATFORM ?? env.ELIZA_PLATFORM ?? env.CAPACITOR_PLATFORM ?? "").toLowerCase();
|
|
182
|
+
return platform === "ios" || platform === "android" || platform === "mobile";
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
maybeCreateStagehandTarget
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=stagehand-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/targets/stagehand-target.ts"],"sourcesContent":["import { execFileSync, execSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { logger } from \"@elizaos/core\";\nimport type { BrowserTarget } from \"../browser-service.js\";\nimport type {\n BrowserWorkspaceCommand,\n BrowserWorkspaceCommandResult,\n} from \"../workspace/browser-workspace-types.js\";\n\nconst pluginSrcDir = path.dirname(fileURLToPath(import.meta.url));\n\nconst STAGEHAND_COMMAND_URL_ENV = [\n \"ELIZA_BROWSER_STAGEHAND_COMMAND_URL\",\n \"STAGEHAND_BROWSER_COMMAND_URL\",\n \"ELIZA_STAGEHAND_COMMAND_URL\",\n] as const;\n\nconst STAGEHAND_BASE_URL_ENV = [\n \"ELIZA_BROWSER_STAGEHAND_URL\",\n \"STAGEHAND_SERVER_URL\",\n \"ELIZA_STAGEHAND_SERVER_URL\",\n] as const;\n\nconst STAGEHAND_AUTO_SETUP_ENV = \"ELIZA_BROWSER_STAGEHAND_AUTO_SETUP\";\nconst STAGEHAND_ALLOW_MOBILE_ENV = \"ELIZA_BROWSER_ALLOW_STAGEHAND_ON_MOBILE\";\n\nexport async function maybeCreateStagehandTarget(\n env: NodeJS.ProcessEnv = process.env,\n): Promise<BrowserTarget | null> {\n if (isDisabled(env.ELIZA_BROWSER_STAGEHAND_ENABLED)) return null;\n\n const mobile = isMobileRuntime(env);\n if (mobile && !isEnabled(env[STAGEHAND_ALLOW_MOBILE_ENV])) {\n logger.debug(\n \"[BrowserService] stagehand target not registered on mobile; using the app browser surface instead\",\n );\n return null;\n }\n\n if (!isDisabled(env[STAGEHAND_AUTO_SETUP_ENV])) {\n ensureLocalStagehandServer(env);\n }\n\n const commandUrl = resolveStagehandCommandUrl(env);\n if (!commandUrl) {\n logger.debug(\n \"[BrowserService] stagehand target not registered; set ELIZA_BROWSER_STAGEHAND_COMMAND_URL or STAGEHAND_SERVER_URL to enable it\",\n );\n return null;\n }\n\n return {\n id: \"stagehand\",\n name: \"Stagehand Browser\",\n description:\n \"Fallback Stagehand/Playwright browser backend reached through a local or remote stagehand command endpoint.\",\n kind: \"stagehand\",\n priority: 10,\n score: ({ mobile: mobileContext }) => (mobileContext ? null : 10),\n available: async () => probeStagehand(commandUrl, env),\n execute: async (command) => executeStagehandCommand(commandUrl, command),\n };\n}\n\nfunction resolveStagehandCommandUrl(env: NodeJS.ProcessEnv): string | null {\n for (const key of STAGEHAND_COMMAND_URL_ENV) {\n const value = normalizeUrl(env[key]);\n if (value) return value;\n }\n for (const key of STAGEHAND_BASE_URL_ENV) {\n const value = normalizeUrl(env[key]);\n if (value) return new URL(\"/api/browser-command\", value).toString();\n }\n return null;\n}\n\nasync function probeStagehand(\n _commandUrl: string,\n env: NodeJS.ProcessEnv,\n): Promise<boolean> {\n const healthUrl = normalizeUrl(env.ELIZA_BROWSER_STAGEHAND_HEALTH_URL);\n if (!healthUrl) return true;\n try {\n const response = await fetch(healthUrl, { method: \"GET\" });\n return response.ok;\n } catch {\n return false;\n }\n}\n\nasync function executeStagehandCommand(\n commandUrl: string,\n command: BrowserWorkspaceCommand,\n): Promise<BrowserWorkspaceCommandResult> {\n const response = await fetch(commandUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ command }),\n });\n const body = await response.json().catch(() => null);\n if (!response.ok) {\n const message =\n body && typeof body === \"object\" && \"error\" in body\n ? String((body as { error?: unknown }).error)\n : `Stagehand command endpoint returned HTTP ${response.status}`;\n throw new Error(message);\n }\n return normalizeStagehandResult(command, body);\n}\n\nfunction normalizeStagehandResult(\n command: BrowserWorkspaceCommand,\n body: unknown,\n): BrowserWorkspaceCommandResult {\n if (body && typeof body === \"object\") {\n const record = body as {\n result?: unknown;\n mode?: unknown;\n subaction?: unknown;\n value?: unknown;\n };\n const result =\n record.result && typeof record.result === \"object\"\n ? (record.result as BrowserWorkspaceCommandResult)\n : (record as BrowserWorkspaceCommandResult);\n return {\n ...result,\n mode: \"cloud\",\n subaction: command.subaction,\n };\n }\n return {\n mode: \"cloud\",\n subaction: command.subaction,\n value: body,\n };\n}\n\nfunction ensureLocalStagehandServer(env: NodeJS.ProcessEnv): boolean {\n const stagehandDir = findStagehandDir(env);\n if (!stagehandDir) return false;\n\n const stagehandIndex = path.join(stagehandDir, \"dist\", \"index.js\");\n if (fs.existsSync(stagehandIndex)) return true;\n\n const stagehandSrc = path.join(stagehandDir, \"src\", \"index.ts\");\n if (!fs.existsSync(stagehandSrc)) return false;\n\n try {\n if (!fs.existsSync(path.join(stagehandDir, \"node_modules\"))) {\n execSync(\"bun install --ignore-scripts\", {\n cwd: stagehandDir,\n stdio: \"ignore\",\n timeout: 60_000,\n });\n }\n const localTsc = path.join(stagehandDir, \"node_modules\", \".bin\", \"tsc\");\n if (fs.existsSync(localTsc)) {\n execFileSync(localTsc, [], {\n cwd: stagehandDir,\n stdio: \"ignore\",\n timeout: 60_000,\n });\n } else {\n execFileSync(\"bunx\", [\"tsc\"], {\n cwd: stagehandDir,\n stdio: \"ignore\",\n timeout: 60_000,\n });\n }\n logger.info(\"[BrowserService] stagehand-server built successfully\");\n return fs.existsSync(stagehandIndex);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.debug(\n `[BrowserService] stagehand-server auto-setup failed: ${message}`,\n );\n return false;\n }\n}\n\nfunction findStagehandDir(env: NodeJS.ProcessEnv): string | null {\n const configured = env.ELIZA_BROWSER_STAGEHAND_DIR?.trim();\n const candidates = [\n configured,\n ...ancestorPaths(pluginSrcDir).flatMap((root) => [\n path.join(root, \"stagehand-server\"),\n path.join(root, \"plugins\", \"plugin-browser\", \"stagehand-server\"),\n path.join(root, \"eliza\", \"plugins\", \"plugin-browser\", \"stagehand-server\"),\n ]),\n ].filter((candidate): candidate is string => Boolean(candidate));\n\n for (const candidate of candidates) {\n const dir = path.resolve(candidate);\n if (\n fs.existsSync(path.join(dir, \"dist\", \"index.js\")) ||\n fs.existsSync(path.join(dir, \"src\", \"index.ts\"))\n ) {\n return dir;\n }\n }\n return null;\n}\n\nfunction ancestorPaths(start: string): string[] {\n const ancestors: string[] = [];\n let current = path.resolve(start);\n while (true) {\n ancestors.push(current);\n const parent = path.dirname(current);\n if (parent === current) return ancestors;\n current = parent;\n }\n}\n\nfunction normalizeUrl(value: string | undefined): string | null {\n if (!value?.trim()) return null;\n try {\n return new URL(value.trim()).toString();\n } catch {\n return null;\n }\n}\n\nfunction isEnabled(value: string | undefined): boolean {\n return value === \"1\" || value?.toLowerCase() === \"true\";\n}\n\nfunction isDisabled(value: string | undefined): boolean {\n return value === \"0\" || value?.toLowerCase() === \"false\";\n}\n\nfunction isMobileRuntime(env: NodeJS.ProcessEnv): boolean {\n const platform = (\n env.ELIZA_MOBILE_PLATFORM ??\n env.ELIZA_PLATFORM ??\n env.CAPACITOR_PLATFORM ??\n \"\"\n ).toLowerCase();\n return platform === \"ios\" || platform === \"android\" || platform === \"mobile\";\n}\n"],"mappings":"AAAA,SAAS,cAAc,gBAAgB;AACvC,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AAOvB,MAAM,eAAe,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEhE,MAAM,4BAA4B;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,2BAA2B;AACjC,MAAM,6BAA6B;AAEnC,eAAsB,2BACpB,MAAyB,QAAQ,KACF;AAC/B,MAAI,WAAW,IAAI,+BAA+B,EAAG,QAAO;AAE5D,QAAM,SAAS,gBAAgB,GAAG;AAClC,MAAI,UAAU,CAAC,UAAU,IAAI,0BAA0B,CAAC,GAAG;AACzD,WAAO;AAAA,MACL;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,WAAW,IAAI,wBAAwB,CAAC,GAAG;AAC9C,+BAA2B,GAAG;AAAA,EAChC;AAEA,QAAM,aAAa,2BAA2B,GAAG;AACjD,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,MACL;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,aACE;AAAA,IACF,MAAM;AAAA,IACN,UAAU;AAAA,IACV,OAAO,CAAC,EAAE,QAAQ,cAAc,MAAO,gBAAgB,OAAO;AAAA,IAC9D,WAAW,YAAY,eAAe,YAAY,GAAG;AAAA,IACrD,SAAS,OAAO,YAAY,wBAAwB,YAAY,OAAO;AAAA,EACzE;AACF;AAEA,SAAS,2BAA2B,KAAuC;AACzE,aAAW,OAAO,2BAA2B;AAC3C,UAAM,QAAQ,aAAa,IAAI,GAAG,CAAC;AACnC,QAAI,MAAO,QAAO;AAAA,EACpB;AACA,aAAW,OAAO,wBAAwB;AACxC,UAAM,QAAQ,aAAa,IAAI,GAAG,CAAC;AACnC,QAAI,MAAO,QAAO,IAAI,IAAI,wBAAwB,KAAK,EAAE,SAAS;AAAA,EACpE;AACA,SAAO;AACT;AAEA,eAAe,eACb,aACA,KACkB;AAClB,QAAM,YAAY,aAAa,IAAI,kCAAkC;AACrE,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,WAAW,EAAE,QAAQ,MAAM,CAAC;AACzD,WAAO,SAAS;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,wBACb,YACA,SACwC;AACxC,QAAM,WAAW,MAAM,MAAM,YAAY;AAAA,IACvC,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,EAAE,QAAQ,CAAC;AAAA,EAClC,CAAC;AACD,QAAM,OAAO,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,IAAI;AACnD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UACJ,QAAQ,OAAO,SAAS,YAAY,WAAW,OAC3C,OAAQ,KAA6B,KAAK,IAC1C,4CAA4C,SAAS,MAAM;AACjE,UAAM,IAAI,MAAM,OAAO;AAAA,EACzB;AACA,SAAO,yBAAyB,SAAS,IAAI;AAC/C;AAEA,SAAS,yBACP,SACA,MAC+B;AAC/B,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,SAAS;AAMf,UAAM,SACJ,OAAO,UAAU,OAAO,OAAO,WAAW,WACrC,OAAO,SACP;AACP,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,MACN,WAAW,QAAQ;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,WAAW,QAAQ;AAAA,IACnB,OAAO;AAAA,EACT;AACF;AAEA,SAAS,2BAA2B,KAAiC;AACnE,QAAM,eAAe,iBAAiB,GAAG;AACzC,MAAI,CAAC,aAAc,QAAO;AAE1B,QAAM,iBAAiB,KAAK,KAAK,cAAc,QAAQ,UAAU;AACjE,MAAI,GAAG,WAAW,cAAc,EAAG,QAAO;AAE1C,QAAM,eAAe,KAAK,KAAK,cAAc,OAAO,UAAU;AAC9D,MAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO;AAEzC,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,KAAK,KAAK,cAAc,cAAc,CAAC,GAAG;AAC3D,eAAS,gCAAgC;AAAA,QACvC,KAAK;AAAA,QACL,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AACA,UAAM,WAAW,KAAK,KAAK,cAAc,gBAAgB,QAAQ,KAAK;AACtE,QAAI,GAAG,WAAW,QAAQ,GAAG;AAC3B,mBAAa,UAAU,CAAC,GAAG;AAAA,QACzB,KAAK;AAAA,QACL,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAAA,IACH,OAAO;AACL,mBAAa,QAAQ,CAAC,KAAK,GAAG;AAAA,QAC5B,KAAK;AAAA,QACL,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AAAA,IACH;AACA,WAAO,KAAK,sDAAsD;AAClE,WAAO,GAAG,WAAW,cAAc;AAAA,EACrC,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO;AAAA,MACL,wDAAwD,OAAO;AAAA,IACjE;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,KAAuC;AAC/D,QAAM,aAAa,IAAI,6BAA6B,KAAK;AACzD,QAAM,aAAa;AAAA,IACjB;AAAA,IACA,GAAG,cAAc,YAAY,EAAE,QAAQ,CAAC,SAAS;AAAA,MAC/C,KAAK,KAAK,MAAM,kBAAkB;AAAA,MAClC,KAAK,KAAK,MAAM,WAAW,kBAAkB,kBAAkB;AAAA,MAC/D,KAAK,KAAK,MAAM,SAAS,WAAW,kBAAkB,kBAAkB;AAAA,IAC1E,CAAC;AAAA,EACH,EAAE,OAAO,CAAC,cAAmC,QAAQ,SAAS,CAAC;AAE/D,aAAW,aAAa,YAAY;AAClC,UAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,QACE,GAAG,WAAW,KAAK,KAAK,KAAK,QAAQ,UAAU,CAAC,KAChD,GAAG,WAAW,KAAK,KAAK,KAAK,OAAO,UAAU,CAAC,GAC/C;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAyB;AAC9C,QAAM,YAAsB,CAAC;AAC7B,MAAI,UAAU,KAAK,QAAQ,KAAK;AAChC,SAAO,MAAM;AACX,cAAU,KAAK,OAAO;AACtB,UAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS,QAAO;AAC/B,cAAU;AAAA,EACZ;AACF;AAEA,SAAS,aAAa,OAA0C;AAC9D,MAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,KAAK,CAAC,EAAE,SAAS;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,UAAU,OAAoC;AACrD,SAAO,UAAU,OAAO,OAAO,YAAY,MAAM;AACnD;AAEA,SAAS,WAAW,OAAoC;AACtD,SAAO,UAAU,OAAO,OAAO,YAAY,MAAM;AACnD;AAEA,SAAS,gBAAgB,KAAiC;AACxD,QAAM,YACJ,IAAI,yBACJ,IAAI,kBACJ,IAAI,sBACJ,IACA,YAAY;AACd,SAAO,aAAa,SAAS,aAAa,aAAa,aAAa;AACtE;","names":[]}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Visual parity with the desktop shell:
|
|
10
10
|
* - Appends `?popout` to the URL so the app renders StreamView directly
|
|
11
|
-
* (
|
|
11
|
+
* (without onboarding, auth gates, or navigation chrome).
|
|
12
12
|
* - Enables SwiftShader for WebGL so VRM avatar renders identically.
|
|
13
13
|
* - Seeds localStorage with overlay layout, theme, and avatar index so
|
|
14
14
|
* the first rendered frame matches the configured appearance.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-capture.d.ts","sourceRoot":"","sources":["../../src/workspace/browser-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"browser-capture.d.ts","sourceRoot":"","sources":["../../src/workspace/browser-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAoDH,oDAAoD;AACpD,eAAO,MAAM,UAAU,QAA2C,CAAC;AAEnE,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,+BAA+B,IAAI,MAAM,CAExD;AAED,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,iBAoHrE;AAED,wBAAsB,kBAAkB,kBAevC;AAED,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
|
|
@@ -3,7 +3,39 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
5
|
import { logger } from "@elizaos/core";
|
|
6
|
-
|
|
6
|
+
function resolveChromePath() {
|
|
7
|
+
if (process.platform === "darwin") {
|
|
8
|
+
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
9
|
+
}
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
const candidates = [
|
|
12
|
+
join(
|
|
13
|
+
process.env.PROGRAMFILES ?? "C:/Program Files",
|
|
14
|
+
"Google",
|
|
15
|
+
"Chrome",
|
|
16
|
+
"Application",
|
|
17
|
+
"chrome.exe"
|
|
18
|
+
),
|
|
19
|
+
join(
|
|
20
|
+
process.env["PROGRAMFILES(X86)"] ?? "C:/Program Files (x86)",
|
|
21
|
+
"Google",
|
|
22
|
+
"Chrome",
|
|
23
|
+
"Application",
|
|
24
|
+
"chrome.exe"
|
|
25
|
+
),
|
|
26
|
+
join(
|
|
27
|
+
process.env.LOCALAPPDATA ?? "",
|
|
28
|
+
"Google",
|
|
29
|
+
"Chrome",
|
|
30
|
+
"Application",
|
|
31
|
+
"chrome.exe"
|
|
32
|
+
)
|
|
33
|
+
];
|
|
34
|
+
return candidates.find((p) => existsSync(p)) ?? candidates[0];
|
|
35
|
+
}
|
|
36
|
+
return "/usr/bin/google-chrome-stable";
|
|
37
|
+
}
|
|
38
|
+
const CHROME_PATH = resolveChromePath();
|
|
7
39
|
let activeBrowser = null;
|
|
8
40
|
let activeCaptureLoop = null;
|
|
9
41
|
let stopSignal = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/workspace/browser-capture.ts"],"sourcesContent":["/**\n * Headless browser capture — opens the StreamView in headless Chrome and\n * saves screenshots to a temp file. FFmpeg reads the temp file using\n * -loop 1 to continuously re-read the latest frame.\n *\n * This approach avoids the pipe bottleneck — FFmpeg reads at its own\n * pace while the browser updates the file independently.\n *\n * Visual parity with the desktop shell:\n * - Appends `?popout` to the URL so the app renders StreamView directly\n * (skips onboarding, auth gates, navigation chrome).\n * - Enables SwiftShader for WebGL so VRM avatar renders identically.\n * - Seeds localStorage with overlay layout, theme, and avatar index so\n * the first rendered frame matches the configured appearance.\n * - Uses `waitUntil: \"networkidle0\"` to ensure all assets load before capture.\n * - Keeps CSS animations/transitions enabled for visual parity.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { logger } from \"@elizaos/core\";\n\nconst CHROME_PATH =\n process.platform === \"darwin\"\n ? \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"\n : process.platform === \"win32\"\n ? \"C:\\\\Program Files\\\\Google Chrome\\\\Application\\\\chrome.exe\"\n : \"/usr/bin/google-chrome-stable\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet activeBrowser: any | null = null;\nlet activeCaptureLoop: Promise<void> | null = null;\nlet stopSignal = false;\n\n/** Path to the temp frame file that FFmpeg reads */\nexport const FRAME_FILE = join(tmpdir(), \"eliza-stream-frame.jpg\");\n\nexport interface BrowserCaptureConfig {\n url: string;\n width?: number;\n height?: number;\n fps?: number;\n quality?: number;\n /** Optional overlay layout JSON to seed into localStorage before page load. */\n overlayLayout?: string;\n /** Theme name to apply (e.g. \"eliza\", \"haxor\", \"psycho\"). */\n theme?: string;\n /** Avatar VRM index (1–8). */\n avatarIndex?: number;\n /** Destination ID — seeds the destination-specific localStorage key. */\n destinationId?: string;\n}\n\nexport function getBrowserCaptureExecutablePath(): string {\n return CHROME_PATH;\n}\n\nexport function isBrowserCaptureSupported(): boolean {\n return existsSync(CHROME_PATH);\n}\n\n/**\n * Ensure the URL includes the `?popout` parameter so the app renders only\n * StreamView, skipping startup gates and navigation chrome.\n */\nfunction ensurePopoutUrl(raw: string): string {\n try {\n const u = new URL(raw);\n // Handle both query and hash-based routing\n if (u.hash?.includes(\"?\")) {\n if (!u.hash.includes(\"popout\")) {\n u.hash = `${u.hash}&popout`;\n }\n } else if (u.hash) {\n u.hash = `${u.hash}?popout`;\n } else if (!u.searchParams.has(\"popout\")) {\n u.searchParams.set(\"popout\", \"\");\n }\n return u.toString();\n } catch {\n // Fallback: just append\n const sep = raw.includes(\"?\") ? \"&\" : \"?\";\n return `${raw}${sep}popout`;\n }\n}\n\nexport async function startBrowserCapture(config: BrowserCaptureConfig) {\n if (activeBrowser) {\n logger.info(\"[browser-capture] Already running\");\n return;\n }\n\n if (!isBrowserCaptureSupported()) {\n throw new Error(\n `Google Chrome not found at ${CHROME_PATH}. Install Chrome or update browser-capture before enabling screen capture.`,\n );\n }\n\n const { url, width = 1280, height = 720, fps = 4, quality = 70 } = config;\n const captureUrl = ensurePopoutUrl(url);\n\n stopSignal = false;\n logger.info(`[browser-capture] Launching headless Chrome to ${captureUrl}`);\n\n const { default: puppeteer } = await import(\"puppeteer-core\");\n const browser = await puppeteer.launch({\n executablePath: CHROME_PATH,\n headless: true,\n args: [\n `--window-size=${width},${height}`,\n \"--no-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-extensions\",\n \"--mute-audio\",\n // WebGL / SwiftShader — required for VRM avatar rendering parity\n \"--use-gl=swiftshader\",\n \"--enable-webgl\",\n \"--ignore-gpu-blocklist\",\n ],\n });\n\n activeBrowser = browser;\n\n const page = await browser.newPage();\n await page.setViewport({ width, height, deviceScaleFactor: 1 });\n\n // Seed localStorage before navigation so the first render matches the desktop shell.\n // Keys must match exactly what the React app reads:\n // - \"eliza:theme\" → ThemeName\n // - \"eliza_avatar_index\" → VRM index (1–8)\n // - \"eliza.stream.overlay-layout.v1[.destId]\" → OverlayLayout JSON\n await page.evaluateOnNewDocument(\n (\n overlayLayout: string | undefined,\n theme: string | undefined,\n avatarIndex: number | undefined,\n destinationId: string | undefined,\n ) => {\n if (overlayLayout) {\n // Seed both global and destination-specific keys so the hook\n // resolves correctly regardless of when activeDestination loads.\n localStorage.setItem(\"eliza.stream.overlay-layout.v1\", overlayLayout);\n if (destinationId) {\n localStorage.setItem(\n `eliza.stream.overlay-layout.v1.${destinationId}`,\n overlayLayout,\n );\n }\n }\n if (theme) {\n localStorage.setItem(\"eliza:theme\", theme);\n }\n if (avatarIndex != null) {\n localStorage.setItem(\"eliza_avatar_index\", String(avatarIndex));\n }\n },\n config.overlayLayout,\n config.theme,\n config.avatarIndex,\n config.destinationId,\n );\n\n // Use networkidle0 so fonts, VRM models, and preview images finish loading\n await page.goto(captureUrl, {\n waitUntil: \"networkidle0\",\n timeout: 60_000,\n });\n\n logger.info(`[browser-capture] Page loaded, writing frames to ${FRAME_FILE}`);\n\n let frameCount = 0;\n const frameIntervalMs = Math.max(100, Math.round(1000 / Math.max(1, fps)));\n activeCaptureLoop = (async () => {\n while (!stopSignal) {\n try {\n await page.screenshot({\n path: FRAME_FILE,\n quality,\n type: \"jpeg\",\n });\n frameCount += 1;\n if (frameCount % 20 === 0) {\n logger.debug(`[browser-capture] ${frameCount} frames written`);\n }\n } catch (error) {\n if (!stopSignal) {\n logger.warn(\n `[browser-capture] frame capture failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n }\n if (!stopSignal) {\n await sleep(frameIntervalMs);\n }\n }\n })();\n\n logger.info(\n `[browser-capture] Screenshot loop active (${fps} fps), saving to ${FRAME_FILE}`,\n );\n}\n\nexport async function stopBrowserCapture() {\n stopSignal = true;\n if (activeCaptureLoop) {\n try {\n await activeCaptureLoop;\n } catch {}\n activeCaptureLoop = null;\n }\n if (activeBrowser) {\n try {\n await activeBrowser.close();\n } catch {}\n activeBrowser = null;\n }\n logger.info(\"[browser-capture] Stopped\");\n}\n\nexport function isBrowserCaptureRunning(): boolean {\n return activeBrowser !== null;\n}\n\nexport function hasFrameFile(): boolean {\n return existsSync(FRAME_FILE);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/workspace/browser-capture.ts"],"sourcesContent":["/**\n * Headless browser capture — opens the StreamView in headless Chrome and\n * saves screenshots to a temp file. FFmpeg reads the temp file using\n * -loop 1 to continuously re-read the latest frame.\n *\n * This approach avoids the pipe bottleneck — FFmpeg reads at its own\n * pace while the browser updates the file independently.\n *\n * Visual parity with the desktop shell:\n * - Appends `?popout` to the URL so the app renders StreamView directly\n * (without onboarding, auth gates, or navigation chrome).\n * - Enables SwiftShader for WebGL so VRM avatar renders identically.\n * - Seeds localStorage with overlay layout, theme, and avatar index so\n * the first rendered frame matches the configured appearance.\n * - Uses `waitUntil: \"networkidle0\"` to ensure all assets load before capture.\n * - Keeps CSS animations/transitions enabled for visual parity.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { logger } from \"@elizaos/core\";\nimport type { Browser } from \"puppeteer-core\";\n\nfunction resolveChromePath(): string {\n if (process.platform === \"darwin\") {\n return \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\";\n }\n if (process.platform === \"win32\") {\n // Chrome installs under Google/Chrome/Application (a subdirectory), not a\n // \"Google Chrome\" folder, and may be 64-bit, 32-bit, or per-user. Probe the\n // standard locations and pick the first that exists. (join normalizes the\n // forward-slash fallbacks to the Windows separator.)\n const candidates = [\n join(\n process.env.PROGRAMFILES ?? \"C:/Program Files\",\n \"Google\",\n \"Chrome\",\n \"Application\",\n \"chrome.exe\",\n ),\n join(\n process.env[\"PROGRAMFILES(X86)\"] ?? \"C:/Program Files (x86)\",\n \"Google\",\n \"Chrome\",\n \"Application\",\n \"chrome.exe\",\n ),\n join(\n process.env.LOCALAPPDATA ?? \"\",\n \"Google\",\n \"Chrome\",\n \"Application\",\n \"chrome.exe\",\n ),\n ];\n return candidates.find((p) => existsSync(p)) ?? candidates[0];\n }\n return \"/usr/bin/google-chrome-stable\";\n}\n\nconst CHROME_PATH = resolveChromePath();\n\nlet activeBrowser: Browser | null = null;\nlet activeCaptureLoop: Promise<void> | null = null;\nlet stopSignal = false;\n\n/** Path to the temp frame file that FFmpeg reads */\nexport const FRAME_FILE = join(tmpdir(), \"eliza-stream-frame.jpg\");\n\nexport interface BrowserCaptureConfig {\n url: string;\n width?: number;\n height?: number;\n fps?: number;\n quality?: number;\n /** Optional overlay layout JSON to seed into localStorage before page load. */\n overlayLayout?: string;\n /** Theme name to apply (e.g. \"eliza\", \"haxor\", \"psycho\"). */\n theme?: string;\n /** Avatar VRM index (1–8). */\n avatarIndex?: number;\n /** Destination ID — seeds the destination-specific localStorage key. */\n destinationId?: string;\n}\n\nexport function getBrowserCaptureExecutablePath(): string {\n return CHROME_PATH;\n}\n\nexport function isBrowserCaptureSupported(): boolean {\n return existsSync(CHROME_PATH);\n}\n\n/**\n * Ensure the URL includes the `?popout` parameter so the app renders only\n * StreamView without startup gates or navigation chrome.\n */\nfunction ensurePopoutUrl(raw: string): string {\n try {\n const u = new URL(raw);\n // Handle both query and hash-based routing\n if (u.hash?.includes(\"?\")) {\n if (!u.hash.includes(\"popout\")) {\n u.hash = `${u.hash}&popout`;\n }\n } else if (u.hash) {\n u.hash = `${u.hash}?popout`;\n } else if (!u.searchParams.has(\"popout\")) {\n u.searchParams.set(\"popout\", \"\");\n }\n return u.toString();\n } catch {\n // Fallback: just append\n const sep = raw.includes(\"?\") ? \"&\" : \"?\";\n return `${raw}${sep}popout`;\n }\n}\n\nexport async function startBrowserCapture(config: BrowserCaptureConfig) {\n if (activeBrowser) {\n logger.info(\"[browser-capture] Already running\");\n return;\n }\n\n if (!isBrowserCaptureSupported()) {\n throw new Error(\n `Google Chrome not found at ${CHROME_PATH}. Install Chrome or update browser-capture before enabling screen capture.`,\n );\n }\n\n const { url, width = 1280, height = 720, fps = 4, quality = 70 } = config;\n const captureUrl = ensurePopoutUrl(url);\n\n stopSignal = false;\n logger.info(`[browser-capture] Launching headless Chrome to ${captureUrl}`);\n\n const { default: puppeteer } = await import(\"puppeteer-core\");\n const browser = await puppeteer.launch({\n executablePath: CHROME_PATH,\n headless: true,\n args: [\n `--window-size=${width},${height}`,\n \"--no-sandbox\",\n \"--disable-dev-shm-usage\",\n \"--disable-extensions\",\n \"--mute-audio\",\n // WebGL / SwiftShader — required for VRM avatar rendering parity\n \"--use-gl=swiftshader\",\n \"--enable-webgl\",\n \"--ignore-gpu-blocklist\",\n ],\n });\n\n activeBrowser = browser;\n\n const page = await browser.newPage();\n await page.setViewport({ width, height, deviceScaleFactor: 1 });\n\n // Seed localStorage before navigation so the first render matches the desktop shell.\n // Keys must match exactly what the React app reads:\n // - \"eliza:theme\" → ThemeName\n // - \"eliza_avatar_index\" → VRM index (1–8)\n // - \"eliza.stream.overlay-layout.v1[.destId]\" → OverlayLayout JSON\n await page.evaluateOnNewDocument(\n (\n overlayLayout: string | undefined,\n theme: string | undefined,\n avatarIndex: number | undefined,\n destinationId: string | undefined,\n ) => {\n if (overlayLayout) {\n // Seed both global and destination-specific keys so the hook\n // resolves correctly regardless of when activeDestination loads.\n localStorage.setItem(\"eliza.stream.overlay-layout.v1\", overlayLayout);\n if (destinationId) {\n localStorage.setItem(\n `eliza.stream.overlay-layout.v1.${destinationId}`,\n overlayLayout,\n );\n }\n }\n if (theme) {\n localStorage.setItem(\"eliza:theme\", theme);\n }\n if (avatarIndex != null) {\n localStorage.setItem(\"eliza_avatar_index\", String(avatarIndex));\n }\n },\n config.overlayLayout,\n config.theme,\n config.avatarIndex,\n config.destinationId,\n );\n\n // Use networkidle0 so fonts, VRM models, and preview images finish loading\n await page.goto(captureUrl, {\n waitUntil: \"networkidle0\",\n timeout: 60_000,\n });\n\n logger.info(`[browser-capture] Page loaded, writing frames to ${FRAME_FILE}`);\n\n let frameCount = 0;\n const frameIntervalMs = Math.max(100, Math.round(1000 / Math.max(1, fps)));\n activeCaptureLoop = (async () => {\n while (!stopSignal) {\n try {\n await page.screenshot({\n path: FRAME_FILE,\n quality,\n type: \"jpeg\",\n });\n frameCount += 1;\n if (frameCount % 20 === 0) {\n logger.debug(`[browser-capture] ${frameCount} frames written`);\n }\n } catch (error) {\n if (!stopSignal) {\n logger.warn(\n `[browser-capture] frame capture failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n }\n if (!stopSignal) {\n await sleep(frameIntervalMs);\n }\n }\n })();\n\n logger.info(\n `[browser-capture] Screenshot loop active (${fps} fps), saving to ${FRAME_FILE}`,\n );\n}\n\nexport async function stopBrowserCapture() {\n stopSignal = true;\n if (activeCaptureLoop) {\n try {\n await activeCaptureLoop;\n } catch {}\n activeCaptureLoop = null;\n }\n if (activeBrowser) {\n try {\n await activeBrowser.close();\n } catch {}\n activeBrowser = null;\n }\n logger.info(\"[browser-capture] Stopped\");\n}\n\nexport function isBrowserCaptureRunning(): boolean {\n return activeBrowser !== null;\n}\n\nexport function hasFrameFile(): boolean {\n return existsSync(FRAME_FILE);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -7,7 +7,7 @@ export declare function evaluateBrowserWorkspaceTab(request: EvaluateBrowserWork
|
|
|
7
7
|
export declare function snapshotBrowserWorkspaceTab(id: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
8
8
|
data: string;
|
|
9
9
|
}>;
|
|
10
|
-
export declare function createDesktopBrowserWorkspaceCommandScript(command: BrowserWorkspaceCommand): string;
|
|
10
|
+
export declare function createDesktopBrowserWorkspaceCommandScript(command: BrowserWorkspaceCommand, env?: NodeJS.ProcessEnv): string;
|
|
11
11
|
export declare function createDesktopBrowserWorkspaceUtilityScript(command: BrowserWorkspaceCommand): string;
|
|
12
12
|
export declare function executeDesktopBrowserWorkspaceUtilityCommand(command: BrowserWorkspaceCommand, env: NodeJS.ProcessEnv): Promise<BrowserWorkspaceCommandResult>;
|
|
13
13
|
export declare function getDesktopBrowserWorkspaceSnapshotRecord(command: BrowserWorkspaceCommand, env: NodeJS.ProcessEnv): Promise<BrowserWorkspaceSnapshotRecord>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-workspace-desktop.d.ts","sourceRoot":"","sources":["../../src/workspace/browser-workspace-desktop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-workspace-desktop.d.ts","sourceRoot":"","sources":["../../src/workspace/browser-workspace-desktop.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,4BAA4B,EAC5B,uBAAuB,EACvB,6BAA6B,EAE7B,8BAA8B,EAC9B,mBAAmB,EACnB,kCAAkC,EACnC,MAAM,8BAA8B,CAAC;AAsBtC,wBAAgB,mCAAmC,CACjD,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,4BAA4B,GAAG,IAAI,CAUrC;AAED,wBAAgB,kCAAkC,CAChD,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAET;AAED,wBAAgB,qCAAqC,IAAI,MAAM,CAE9D;AAED,wBAAsB,uBAAuB,CAAC,CAAC,EAC7C,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,CAAC,CAAC,CAsCZ;AAED,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,kCAAkC,EAC3C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,OAAO,CAAC,CAyBlB;AAED,wBAAsB,2BAA2B,CAC/C,EAAE,EAAE,MAAM,EACV,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAc3B;AAsBD,wBAAgB,0CAA0C,CACxD,OAAO,EAAE,uBAAuB,EAChC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CA0wBR;AAED,wBAAgB,0CAA0C,CACxD,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CAibR;AAED,wBAAsB,4CAA4C,CAChE,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,6BAA6B,CAAC,CAwCxC;AAED,wBAAsB,wCAAwC,CAC5D,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,8BAA8B,CAAC,CA0CzC;AAED,wBAAsB,sCAAsC,CAC1D,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAwClC;AAED,wBAAsB,uCAAuC,CAC3D,OAAO,EAAE,uBAAuB,EAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wBAAsB,wCAAwC,CAC5D,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,6BAA6B,CAAC,CA+DxC;AAID,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,mBAAmB,EAAE,GAC1B,mBAAmB,GAAG,IAAI,CAgB5B;AAED,wBAAsB,yCAAyC,CAC7D,OAAO,EAAE,uBAAuB,EAChC,GAAG,EAAE,MAAM,CAAC,UAAU,GACrB,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createBrowserWorkspaceError } from "./browser-workspace-errors.js";
|
|
1
2
|
import {
|
|
2
3
|
assertBrowserWorkspaceConnectorSecretsNotExported,
|
|
4
|
+
assertBrowserWorkspaceUserScriptAllowed,
|
|
3
5
|
createBrowserWorkspaceCommandTargetError,
|
|
4
6
|
DEFAULT_TIMEOUT_MS,
|
|
7
|
+
isBrowserWorkspaceUserScriptAllowed,
|
|
5
8
|
normalizeEnvValue,
|
|
6
9
|
resolveBrowserWorkspaceCommandElementRefs
|
|
7
10
|
} from "./browser-workspace-helpers.js";
|
|
@@ -24,13 +27,13 @@ async function readErrorBody(response) {
|
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
function resolveBrowserWorkspaceBridgeConfig(env = process.env) {
|
|
27
|
-
const baseUrl = normalizeEnvValue(env.ELIZA_BROWSER_WORKSPACE_URL)
|
|
30
|
+
const baseUrl = normalizeEnvValue(env.ELIZA_BROWSER_WORKSPACE_URL);
|
|
28
31
|
if (!baseUrl) {
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
31
34
|
return {
|
|
32
35
|
baseUrl: baseUrl.replace(/\/{1,1024}$/, ""),
|
|
33
|
-
token: normalizeEnvValue(env.ELIZA_BROWSER_WORKSPACE_TOKEN)
|
|
36
|
+
token: normalizeEnvValue(env.ELIZA_BROWSER_WORKSPACE_TOKEN)
|
|
34
37
|
};
|
|
35
38
|
}
|
|
36
39
|
function isBrowserWorkspaceBridgeConfigured(env = process.env) {
|
|
@@ -42,7 +45,11 @@ function getBrowserWorkspaceUnavailableMessage() {
|
|
|
42
45
|
async function requestBrowserWorkspace(path, init, env = process.env) {
|
|
43
46
|
const config = resolveBrowserWorkspaceBridgeConfig(env);
|
|
44
47
|
if (!config) {
|
|
45
|
-
throw
|
|
48
|
+
throw createBrowserWorkspaceError(
|
|
49
|
+
"desktop_only",
|
|
50
|
+
"desktop_bridge",
|
|
51
|
+
getBrowserWorkspaceUnavailableMessage()
|
|
52
|
+
);
|
|
46
53
|
}
|
|
47
54
|
const headers = new Headers(init?.headers ?? {});
|
|
48
55
|
headers.set("Accept", "application/json");
|
|
@@ -59,15 +66,22 @@ async function requestBrowserWorkspace(path, init, env = process.env) {
|
|
|
59
66
|
});
|
|
60
67
|
if (!response.ok) {
|
|
61
68
|
const details = await readErrorBody(response);
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
const message = `Browser workspace request failed (${response.status})${details ? `: ${details}` : ""}`;
|
|
70
|
+
throw createBrowserWorkspaceError(
|
|
71
|
+
response.status === 404 ? "tab_not_found" : "command_failed",
|
|
72
|
+
path,
|
|
73
|
+
message,
|
|
74
|
+
details || void 0,
|
|
75
|
+
response.status
|
|
64
76
|
);
|
|
65
77
|
}
|
|
66
78
|
return await response.json();
|
|
67
79
|
}
|
|
68
80
|
async function evaluateBrowserWorkspaceTab(request, env = process.env) {
|
|
69
81
|
if (!isBrowserWorkspaceBridgeConfigured(env)) {
|
|
70
|
-
throw
|
|
82
|
+
throw createBrowserWorkspaceError(
|
|
83
|
+
"desktop_only",
|
|
84
|
+
"eval",
|
|
71
85
|
"Eliza browser workspace eval is only available in the desktop app."
|
|
72
86
|
);
|
|
73
87
|
}
|
|
@@ -89,7 +103,9 @@ async function evaluateBrowserWorkspaceTab(request, env = process.env) {
|
|
|
89
103
|
}
|
|
90
104
|
async function snapshotBrowserWorkspaceTab(id, env = process.env) {
|
|
91
105
|
if (!isBrowserWorkspaceBridgeConfigured(env)) {
|
|
92
|
-
throw
|
|
106
|
+
throw createBrowserWorkspaceError(
|
|
107
|
+
"desktop_only",
|
|
108
|
+
"snapshot",
|
|
93
109
|
"Eliza browser workspace snapshot is only available in the desktop app."
|
|
94
110
|
);
|
|
95
111
|
}
|
|
@@ -99,7 +115,25 @@ async function snapshotBrowserWorkspaceTab(id, env = process.env) {
|
|
|
99
115
|
env
|
|
100
116
|
);
|
|
101
117
|
}
|
|
102
|
-
function
|
|
118
|
+
function desktopBrowserWorkspaceWaitScriptBranch(env) {
|
|
119
|
+
if (isBrowserWorkspaceUserScriptAllowed(env)) {
|
|
120
|
+
return `
|
|
121
|
+
if (command.script) {
|
|
122
|
+
const fn = new Function("document", "window", "location", "return (" + command.script + ");");
|
|
123
|
+
if (fn(document, window, location)) {
|
|
124
|
+
resolve({ ok: true, script: true });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}`;
|
|
128
|
+
}
|
|
129
|
+
return `
|
|
130
|
+
if (command.script) {
|
|
131
|
+
reject(new Error("Browser workspace wait script is disabled (GHSA-mhhr-9ph9-64j7)."));
|
|
132
|
+
return;
|
|
133
|
+
}`;
|
|
134
|
+
}
|
|
135
|
+
function createDesktopBrowserWorkspaceCommandScript(command, env = process.env) {
|
|
136
|
+
const waitScriptBranch = desktopBrowserWorkspaceWaitScriptBranch(env);
|
|
103
137
|
return `
|
|
104
138
|
(() => {
|
|
105
139
|
const command = ${JSON.stringify(command)};
|
|
@@ -730,24 +764,23 @@ function createDesktopBrowserWorkspaceCommandScript(command) {
|
|
|
730
764
|
const deadline = Date.now() + (Number(command.timeoutMs) || 4000);
|
|
731
765
|
const check = () => {
|
|
732
766
|
try {
|
|
733
|
-
if (command.selector
|
|
767
|
+
if (command.selector) {
|
|
734
768
|
const found = findTarget();
|
|
735
769
|
const visible =
|
|
736
|
-
command.state === "
|
|
737
|
-
? found
|
|
738
|
-
:
|
|
770
|
+
command.state === "hidden"
|
|
771
|
+
? !found || !isVisible(found)
|
|
772
|
+
: found && isVisible(found);
|
|
739
773
|
if (visible) {
|
|
740
774
|
resolve({ ok: true, selector: command.selector, state: command.state || "visible" });
|
|
741
775
|
return;
|
|
742
776
|
}
|
|
743
777
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
(command.state === "
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return;
|
|
778
|
+
if (command.findBy) {
|
|
779
|
+
const found = findSemantic();
|
|
780
|
+
if (command.state === "hidden" ? !found : found) {
|
|
781
|
+
resolve({ findBy: command.findBy, ok: true });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
751
784
|
}
|
|
752
785
|
if (command.text && normalize(document.body?.textContent).includes(command.text)) {
|
|
753
786
|
resolve({ ok: true, text: command.text });
|
|
@@ -757,13 +790,7 @@ function createDesktopBrowserWorkspaceCommandScript(command) {
|
|
|
757
790
|
resolve({ ok: true, url: location.href });
|
|
758
791
|
return;
|
|
759
792
|
}
|
|
760
|
-
|
|
761
|
-
const fn = new Function("document", "window", "location", "return (" + command.script + ");");
|
|
762
|
-
if (fn(document, window, location)) {
|
|
763
|
-
resolve({ ok: true, script: true });
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
793
|
+
${waitScriptBranch}
|
|
767
794
|
if (Date.now() >= deadline) {
|
|
768
795
|
reject(new Error("Timed out waiting for browser workspace condition."));
|
|
769
796
|
return;
|
|
@@ -1471,16 +1498,25 @@ async function loadDesktopBrowserWorkspaceSessionState(command, payload, env) {
|
|
|
1471
1498
|
);
|
|
1472
1499
|
}
|
|
1473
1500
|
async function executeDesktopBrowserWorkspaceDomCommand(command, env) {
|
|
1501
|
+
assertBrowserWorkspaceUserScriptAllowed(
|
|
1502
|
+
command.script,
|
|
1503
|
+
"wait",
|
|
1504
|
+
"desktop",
|
|
1505
|
+
env
|
|
1506
|
+
);
|
|
1474
1507
|
const id = await resolveDesktopBrowserWorkspaceTargetTabId(command, env);
|
|
1475
1508
|
const startedAt = Date.now();
|
|
1476
1509
|
command = resolveBrowserWorkspaceCommandElementRefs(command, "desktop", id);
|
|
1477
1510
|
const result = await evaluateBrowserWorkspaceTab(
|
|
1478
1511
|
{
|
|
1479
1512
|
id,
|
|
1480
|
-
script: createDesktopBrowserWorkspaceCommandScript(
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1513
|
+
script: createDesktopBrowserWorkspaceCommandScript(
|
|
1514
|
+
{
|
|
1515
|
+
...command,
|
|
1516
|
+
id
|
|
1517
|
+
},
|
|
1518
|
+
env
|
|
1519
|
+
)
|
|
1484
1520
|
},
|
|
1485
1521
|
env
|
|
1486
1522
|
);
|