@agenticmail/enterprise 0.5.78 → 0.5.80
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/chunk-7MILGDAA.js +2191 -0
- package/dist/chunk-7RNT4O5T.js +15198 -0
- package/dist/chunk-AGFOJCSB.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-GWUIYH7I.js +15035 -0
- package/dist/chunk-PZA7YOJE.js +898 -0
- package/dist/chunk-Q3V7VZFQ.js +2191 -0
- package/dist/chunk-RRFB6G6M.js +15198 -0
- package/dist/chunk-VX3VFMVB.js +409 -0
- package/dist/chunk-WRPZCOWC.js +898 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/agent-detail.js +313 -1
- package/dist/index.js +3 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-7HW4GX5L.js +48 -0
- package/dist/runtime-GYVO3NF3.js +47 -0
- package/dist/runtime-XXDCZZIK.js +48 -0
- package/dist/server-FMP4BFGW.js +12 -0
- package/dist/server-JRHDUNII.js +12 -0
- package/dist/server-VNW6G4GB.js +12 -0
- package/dist/setup-AANLREEL.js +20 -0
- package/dist/setup-O5FPRLK4.js +20 -0
- package/dist/setup-S4Z4PPIJ.js +20 -0
- package/package.json +15 -2
- package/src/agent-tools/common.ts +25 -0
- package/src/agent-tools/index.ts +4 -0
- package/src/agent-tools/schema/typebox.ts +25 -0
- package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
- package/src/agent-tools/tools/browser-tool.ts +388 -0
- package/src/agent-tools/tools/gateway.ts +126 -0
- package/src/agent-tools/tools/nodes-utils.ts +80 -0
- package/src/browser/bridge-auth-registry.ts +34 -0
- package/src/browser/bridge-server.ts +93 -0
- package/src/browser/cdp.helpers.ts +180 -0
- package/src/browser/cdp.ts +466 -0
- package/src/browser/chrome.executables.ts +625 -0
- package/src/browser/chrome.profile-decoration.ts +198 -0
- package/src/browser/chrome.ts +349 -0
- package/src/browser/client-actions-core.ts +259 -0
- package/src/browser/client-actions-observe.ts +184 -0
- package/src/browser/client-actions-state.ts +284 -0
- package/src/browser/client-actions-types.ts +16 -0
- package/src/browser/client-actions-url.ts +11 -0
- package/src/browser/client-actions.ts +4 -0
- package/src/browser/client-fetch.ts +253 -0
- package/src/browser/client.ts +337 -0
- package/src/browser/config.ts +296 -0
- package/src/browser/constants.ts +8 -0
- package/src/browser/control-auth.ts +94 -0
- package/src/browser/control-service.ts +81 -0
- package/src/browser/csrf.ts +87 -0
- package/src/browser/enterprise-compat.ts +518 -0
- package/src/browser/extension-relay.ts +834 -0
- package/src/browser/http-auth.ts +63 -0
- package/src/browser/navigation-guard.ts +50 -0
- package/src/browser/paths.ts +49 -0
- package/src/browser/profiles-service.ts +187 -0
- package/src/browser/profiles.ts +113 -0
- package/src/browser/proxy-files.ts +41 -0
- package/src/browser/pw-ai-module.ts +52 -0
- package/src/browser/pw-ai-state.ts +9 -0
- package/src/browser/pw-ai.ts +65 -0
- package/src/browser/pw-role-snapshot.ts +434 -0
- package/src/browser/pw-session.ts +810 -0
- package/src/browser/pw-tools-core.activity.ts +68 -0
- package/src/browser/pw-tools-core.downloads.ts +281 -0
- package/src/browser/pw-tools-core.interactions.ts +646 -0
- package/src/browser/pw-tools-core.responses.ts +124 -0
- package/src/browser/pw-tools-core.shared.ts +70 -0
- package/src/browser/pw-tools-core.snapshot.ts +213 -0
- package/src/browser/pw-tools-core.state.ts +209 -0
- package/src/browser/pw-tools-core.storage.ts +128 -0
- package/src/browser/pw-tools-core.trace.ts +37 -0
- package/src/browser/pw-tools-core.ts +8 -0
- package/src/browser/resolved-config-refresh.ts +59 -0
- package/src/browser/routes/agent.act.shared.ts +52 -0
- package/src/browser/routes/agent.act.ts +575 -0
- package/src/browser/routes/agent.debug.ts +149 -0
- package/src/browser/routes/agent.shared.ts +143 -0
- package/src/browser/routes/agent.snapshot.ts +333 -0
- package/src/browser/routes/agent.storage.ts +451 -0
- package/src/browser/routes/agent.ts +13 -0
- package/src/browser/routes/basic.ts +202 -0
- package/src/browser/routes/dispatcher.ts +126 -0
- package/src/browser/routes/index.ts +11 -0
- package/src/browser/routes/path-output.ts +1 -0
- package/src/browser/routes/tabs.ts +217 -0
- package/src/browser/routes/types.ts +26 -0
- package/src/browser/routes/utils.ts +73 -0
- package/src/browser/screenshot.ts +54 -0
- package/src/browser/server-context.ts +688 -0
- package/src/browser/server-context.types.ts +65 -0
- package/src/browser/server-lifecycle.ts +48 -0
- package/src/browser/server-middleware.ts +37 -0
- package/src/browser/server.ts +110 -0
- package/src/browser/target-id.ts +30 -0
- package/src/browser/trash.ts +21 -0
- package/src/dashboard/pages/agent-detail.js +313 -1
- package/src/engine/agent-routes.ts +46 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { fetchJson, fetchOk } from "./cdp.helpers.js";
|
|
4
|
+
import { appendCdpPath, createTargetViaCdp, normalizeCdpWsUrl } from "./cdp.js";
|
|
5
|
+
import {
|
|
6
|
+
isChromeCdpReady,
|
|
7
|
+
isChromeReachable,
|
|
8
|
+
launchOpenClawChrome,
|
|
9
|
+
resolveOpenClawUserDataDir,
|
|
10
|
+
stopOpenClawChrome,
|
|
11
|
+
} from "./chrome.js";
|
|
12
|
+
import type { ResolvedBrowserProfile } from "./config.js";
|
|
13
|
+
import { resolveProfile } from "./config.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureChromeExtensionRelayServer,
|
|
16
|
+
stopChromeExtensionRelayServer,
|
|
17
|
+
} from "./extension-relay.js";
|
|
18
|
+
import {
|
|
19
|
+
assertBrowserNavigationAllowed,
|
|
20
|
+
InvalidBrowserNavigationUrlError,
|
|
21
|
+
withBrowserNavigationPolicy,
|
|
22
|
+
} from "./navigation-guard.js";
|
|
23
|
+
import type { PwAiModule } from "./pw-ai-module.js";
|
|
24
|
+
import { getPwAiModule } from "./pw-ai-module.js";
|
|
25
|
+
import {
|
|
26
|
+
refreshResolvedBrowserConfigFromDisk,
|
|
27
|
+
resolveBrowserProfileWithHotReload,
|
|
28
|
+
} from "./resolved-config-refresh.js";
|
|
29
|
+
import type {
|
|
30
|
+
BrowserServerState,
|
|
31
|
+
BrowserRouteContext,
|
|
32
|
+
BrowserTab,
|
|
33
|
+
ContextOptions,
|
|
34
|
+
ProfileContext,
|
|
35
|
+
ProfileRuntimeState,
|
|
36
|
+
ProfileStatus,
|
|
37
|
+
} from "./server-context.types.js";
|
|
38
|
+
import { resolveTargetIdFromTabs } from "./target-id.js";
|
|
39
|
+
import { movePathToTrash } from "./trash.js";
|
|
40
|
+
import { SsrFBlockedError } from "./enterprise-compat.js";
|
|
41
|
+
|
|
42
|
+
export type {
|
|
43
|
+
BrowserRouteContext,
|
|
44
|
+
BrowserServerState,
|
|
45
|
+
BrowserTab,
|
|
46
|
+
ProfileContext,
|
|
47
|
+
ProfileRuntimeState,
|
|
48
|
+
ProfileStatus,
|
|
49
|
+
} from "./server-context.types.js";
|
|
50
|
+
|
|
51
|
+
export function listKnownProfileNames(state: BrowserServerState): string[] {
|
|
52
|
+
const names = new Set(Object.keys(state.resolved.profiles));
|
|
53
|
+
for (const name of state.profiles.keys()) {
|
|
54
|
+
names.add(name);
|
|
55
|
+
}
|
|
56
|
+
return [...names];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Normalize a CDP WebSocket URL to use the correct base URL.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeWsUrl(raw: string | undefined, cdpBaseUrl: string): string | undefined {
|
|
63
|
+
if (!raw) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return normalizeCdpWsUrl(raw, cdpBaseUrl);
|
|
68
|
+
} catch {
|
|
69
|
+
return raw;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a profile-scoped context for browser operations.
|
|
75
|
+
*/
|
|
76
|
+
function createProfileContext(
|
|
77
|
+
opts: ContextOptions,
|
|
78
|
+
profile: ResolvedBrowserProfile,
|
|
79
|
+
): ProfileContext {
|
|
80
|
+
const state = () => {
|
|
81
|
+
const current = opts.getState();
|
|
82
|
+
if (!current) {
|
|
83
|
+
throw new Error("Browser server not started");
|
|
84
|
+
}
|
|
85
|
+
return current;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const getProfileState = (): ProfileRuntimeState => {
|
|
89
|
+
const current = state();
|
|
90
|
+
let profileState = current.profiles.get(profile.name);
|
|
91
|
+
if (!profileState) {
|
|
92
|
+
profileState = { profile, running: null, lastTargetId: null };
|
|
93
|
+
current.profiles.set(profile.name, profileState);
|
|
94
|
+
}
|
|
95
|
+
return profileState;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const setProfileRunning = (running: ProfileRuntimeState["running"]) => {
|
|
99
|
+
const profileState = getProfileState();
|
|
100
|
+
profileState.running = running;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const listTabs = async (): Promise<BrowserTab[]> => {
|
|
104
|
+
// For remote profiles, use Playwright's persistent connection to avoid ephemeral sessions
|
|
105
|
+
if (!profile.cdpIsLoopback) {
|
|
106
|
+
const mod = await getPwAiModule({ mode: "strict" });
|
|
107
|
+
const listPagesViaPlaywright = (mod as Partial<PwAiModule> | null)?.listPagesViaPlaywright;
|
|
108
|
+
if (typeof listPagesViaPlaywright === "function") {
|
|
109
|
+
const pages = await listPagesViaPlaywright({ cdpUrl: profile.cdpUrl });
|
|
110
|
+
return pages.map((p) => ({
|
|
111
|
+
targetId: p.targetId,
|
|
112
|
+
title: p.title,
|
|
113
|
+
url: p.url,
|
|
114
|
+
type: p.type,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const raw = await fetchJson<
|
|
120
|
+
Array<{
|
|
121
|
+
id?: string;
|
|
122
|
+
title?: string;
|
|
123
|
+
url?: string;
|
|
124
|
+
webSocketDebuggerUrl?: string;
|
|
125
|
+
type?: string;
|
|
126
|
+
}>
|
|
127
|
+
>(appendCdpPath(profile.cdpUrl, "/json/list"));
|
|
128
|
+
return raw
|
|
129
|
+
.map((t) => ({
|
|
130
|
+
targetId: t.id ?? "",
|
|
131
|
+
title: t.title ?? "",
|
|
132
|
+
url: t.url ?? "",
|
|
133
|
+
wsUrl: normalizeWsUrl(t.webSocketDebuggerUrl, profile.cdpUrl),
|
|
134
|
+
type: t.type,
|
|
135
|
+
}))
|
|
136
|
+
.filter((t) => Boolean(t.targetId));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const openTab = async (url: string): Promise<BrowserTab> => {
|
|
140
|
+
const ssrfPolicyOpts = withBrowserNavigationPolicy(state().resolved.ssrfPolicy);
|
|
141
|
+
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
|
142
|
+
|
|
143
|
+
// For remote profiles, use Playwright's persistent connection to create tabs
|
|
144
|
+
// This ensures the tab persists beyond a single request
|
|
145
|
+
if (!profile.cdpIsLoopback) {
|
|
146
|
+
const mod = await getPwAiModule({ mode: "strict" });
|
|
147
|
+
const createPageViaPlaywright = (mod as Partial<PwAiModule> | null)?.createPageViaPlaywright;
|
|
148
|
+
if (typeof createPageViaPlaywright === "function") {
|
|
149
|
+
const page = await createPageViaPlaywright({
|
|
150
|
+
cdpUrl: profile.cdpUrl,
|
|
151
|
+
url,
|
|
152
|
+
...ssrfPolicyOpts,
|
|
153
|
+
navigationChecked: true,
|
|
154
|
+
});
|
|
155
|
+
const profileState = getProfileState();
|
|
156
|
+
profileState.lastTargetId = page.targetId;
|
|
157
|
+
return {
|
|
158
|
+
targetId: page.targetId,
|
|
159
|
+
title: page.title,
|
|
160
|
+
url: page.url,
|
|
161
|
+
type: page.type,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const createdViaCdp = await createTargetViaCdp({
|
|
167
|
+
cdpUrl: profile.cdpUrl,
|
|
168
|
+
url,
|
|
169
|
+
...ssrfPolicyOpts,
|
|
170
|
+
navigationChecked: true,
|
|
171
|
+
})
|
|
172
|
+
.then((r) => r.targetId)
|
|
173
|
+
.catch(() => null);
|
|
174
|
+
|
|
175
|
+
if (createdViaCdp) {
|
|
176
|
+
const profileState = getProfileState();
|
|
177
|
+
profileState.lastTargetId = createdViaCdp;
|
|
178
|
+
const deadline = Date.now() + 2000;
|
|
179
|
+
while (Date.now() < deadline) {
|
|
180
|
+
const tabs = await listTabs().catch(() => [] as BrowserTab[]);
|
|
181
|
+
const found = tabs.find((t) => t.targetId === createdViaCdp);
|
|
182
|
+
if (found) {
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
186
|
+
}
|
|
187
|
+
return { targetId: createdViaCdp, title: "", url, type: "page" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const encoded = encodeURIComponent(url);
|
|
191
|
+
type CdpTarget = {
|
|
192
|
+
id?: string;
|
|
193
|
+
title?: string;
|
|
194
|
+
url?: string;
|
|
195
|
+
webSocketDebuggerUrl?: string;
|
|
196
|
+
type?: string;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const endpointUrl = new URL(appendCdpPath(profile.cdpUrl, "/json/new"));
|
|
200
|
+
const endpoint = endpointUrl.search
|
|
201
|
+
? (() => {
|
|
202
|
+
endpointUrl.searchParams.set("url", url);
|
|
203
|
+
return endpointUrl.toString();
|
|
204
|
+
})()
|
|
205
|
+
: `${endpointUrl.toString()}?${encoded}`;
|
|
206
|
+
const created = await fetchJson<CdpTarget>(endpoint, 1500, {
|
|
207
|
+
method: "PUT",
|
|
208
|
+
}).catch(async (err) => {
|
|
209
|
+
if (String(err).includes("HTTP 405")) {
|
|
210
|
+
return await fetchJson<CdpTarget>(endpoint, 1500);
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!created.id) {
|
|
216
|
+
throw new Error("Failed to open tab (missing id)");
|
|
217
|
+
}
|
|
218
|
+
const profileState = getProfileState();
|
|
219
|
+
profileState.lastTargetId = created.id;
|
|
220
|
+
return {
|
|
221
|
+
targetId: created.id,
|
|
222
|
+
title: created.title ?? "",
|
|
223
|
+
url: created.url ?? url,
|
|
224
|
+
wsUrl: normalizeWsUrl(created.webSocketDebuggerUrl, profile.cdpUrl),
|
|
225
|
+
type: created.type,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const resolveRemoteHttpTimeout = (timeoutMs: number | undefined) => {
|
|
230
|
+
if (profile.cdpIsLoopback) {
|
|
231
|
+
return timeoutMs ?? 300;
|
|
232
|
+
}
|
|
233
|
+
const resolved = state().resolved;
|
|
234
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
|
235
|
+
return Math.max(Math.floor(timeoutMs), resolved.remoteCdpTimeoutMs);
|
|
236
|
+
}
|
|
237
|
+
return resolved.remoteCdpTimeoutMs;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const resolveRemoteWsTimeout = (timeoutMs: number | undefined) => {
|
|
241
|
+
if (profile.cdpIsLoopback) {
|
|
242
|
+
const base = timeoutMs ?? 300;
|
|
243
|
+
return Math.max(200, Math.min(2000, base * 2));
|
|
244
|
+
}
|
|
245
|
+
const resolved = state().resolved;
|
|
246
|
+
if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs)) {
|
|
247
|
+
return Math.max(Math.floor(timeoutMs) * 2, resolved.remoteCdpHandshakeTimeoutMs);
|
|
248
|
+
}
|
|
249
|
+
return resolved.remoteCdpHandshakeTimeoutMs;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const isReachable = async (timeoutMs?: number) => {
|
|
253
|
+
const httpTimeout = resolveRemoteHttpTimeout(timeoutMs);
|
|
254
|
+
const wsTimeout = resolveRemoteWsTimeout(timeoutMs);
|
|
255
|
+
return await isChromeCdpReady(profile.cdpUrl, httpTimeout, wsTimeout);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const isHttpReachable = async (timeoutMs?: number) => {
|
|
259
|
+
const httpTimeout = resolveRemoteHttpTimeout(timeoutMs);
|
|
260
|
+
return await isChromeReachable(profile.cdpUrl, httpTimeout);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const attachRunning = (running: NonNullable<ProfileRuntimeState["running"]>) => {
|
|
264
|
+
setProfileRunning(running);
|
|
265
|
+
running.proc.on("exit", () => {
|
|
266
|
+
// Guard against server teardown (e.g., SIGUSR1 restart)
|
|
267
|
+
if (!opts.getState()) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const profileState = getProfileState();
|
|
271
|
+
if (profileState.running?.pid === running.pid) {
|
|
272
|
+
setProfileRunning(null);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const ensureBrowserAvailable = async (): Promise<void> => {
|
|
278
|
+
const current = state();
|
|
279
|
+
const remoteCdp = !profile.cdpIsLoopback;
|
|
280
|
+
const isExtension = profile.driver === "extension";
|
|
281
|
+
const profileState = getProfileState();
|
|
282
|
+
const httpReachable = await isHttpReachable();
|
|
283
|
+
|
|
284
|
+
if (isExtension && remoteCdp) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Profile "${profile.name}" uses driver=extension but cdpUrl is not loopback (${profile.cdpUrl}).`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (isExtension) {
|
|
291
|
+
if (!httpReachable) {
|
|
292
|
+
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl });
|
|
293
|
+
if (await isHttpReachable(1200)) {
|
|
294
|
+
// continue: we still need the extension to connect for CDP websocket.
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Chrome extension relay for profile "${profile.name}" is not reachable at ${profile.cdpUrl}.`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (await isReachable(600)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Relay server is up, but no attached tab yet. Prompt user to attach.
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Chrome extension relay is running, but no tab is connected. Click the OpenClaw Chrome extension icon on a tab to attach it (profile "${profile.name}").`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!httpReachable) {
|
|
312
|
+
if ((current.resolved.attachOnly || remoteCdp) && opts.onEnsureAttachTarget) {
|
|
313
|
+
await opts.onEnsureAttachTarget(profile);
|
|
314
|
+
if (await isHttpReachable(1200)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (current.resolved.attachOnly || remoteCdp) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
remoteCdp
|
|
321
|
+
? `Remote CDP for profile "${profile.name}" is not reachable at ${profile.cdpUrl}.`
|
|
322
|
+
: `Browser attachOnly is enabled and profile "${profile.name}" is not running.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const launched = await launchOpenClawChrome(current.resolved, profile);
|
|
326
|
+
attachRunning(launched);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Port is reachable - check if we own it
|
|
331
|
+
if (await isReachable()) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// HTTP responds but WebSocket fails - port in use by something else
|
|
336
|
+
if (!profileState.running) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Port ${profile.cdpPort} is in use for profile "${profile.name}" but not by openclaw. ` +
|
|
339
|
+
`Run action=reset-profile profile=${profile.name} to kill the process.`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// We own it but WebSocket failed - restart
|
|
344
|
+
if (current.resolved.attachOnly || remoteCdp) {
|
|
345
|
+
if (opts.onEnsureAttachTarget) {
|
|
346
|
+
await opts.onEnsureAttachTarget(profile);
|
|
347
|
+
if (await isReachable(1200)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw new Error(
|
|
352
|
+
remoteCdp
|
|
353
|
+
? `Remote CDP websocket for profile "${profile.name}" is not reachable.`
|
|
354
|
+
: `Browser attachOnly is enabled and CDP websocket for profile "${profile.name}" is not reachable.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await stopOpenClawChrome(profileState.running);
|
|
359
|
+
setProfileRunning(null);
|
|
360
|
+
|
|
361
|
+
const relaunched = await launchOpenClawChrome(current.resolved, profile);
|
|
362
|
+
attachRunning(relaunched);
|
|
363
|
+
|
|
364
|
+
if (!(await isReachable(600))) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Chrome CDP websocket for profile "${profile.name}" is not reachable after restart.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const ensureTabAvailable = async (targetId?: string): Promise<BrowserTab> => {
|
|
372
|
+
await ensureBrowserAvailable();
|
|
373
|
+
const profileState = getProfileState();
|
|
374
|
+
const tabs1 = await listTabs();
|
|
375
|
+
if (tabs1.length === 0) {
|
|
376
|
+
if (profile.driver === "extension") {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`tab not found (no attached Chrome tabs for profile "${profile.name}"). ` +
|
|
379
|
+
"Click the OpenClaw Browser Relay toolbar icon on the tab you want to control (badge ON).",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
await openTab("about:blank");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const tabs = await listTabs();
|
|
386
|
+
// For remote profiles using Playwright's persistent connection, we don't need wsUrl
|
|
387
|
+
// because we access pages directly through Playwright, not via individual WebSocket URLs.
|
|
388
|
+
const candidates =
|
|
389
|
+
profile.driver === "extension" || !profile.cdpIsLoopback
|
|
390
|
+
? tabs
|
|
391
|
+
: tabs.filter((t) => Boolean(t.wsUrl));
|
|
392
|
+
|
|
393
|
+
const resolveById = (raw: string) => {
|
|
394
|
+
const resolved = resolveTargetIdFromTabs(raw, candidates);
|
|
395
|
+
if (!resolved.ok) {
|
|
396
|
+
if (resolved.reason === "ambiguous") {
|
|
397
|
+
return "AMBIGUOUS" as const;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return candidates.find((t) => t.targetId === resolved.targetId) ?? null;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const pickDefault = () => {
|
|
405
|
+
const last = profileState.lastTargetId?.trim() || "";
|
|
406
|
+
const lastResolved = last ? resolveById(last) : null;
|
|
407
|
+
if (lastResolved && lastResolved !== "AMBIGUOUS") {
|
|
408
|
+
return lastResolved;
|
|
409
|
+
}
|
|
410
|
+
// Prefer a real page tab first (avoid service workers/background targets).
|
|
411
|
+
const page = candidates.find((t) => (t.type ?? "page") === "page");
|
|
412
|
+
return page ?? candidates.at(0) ?? null;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
let chosen = targetId ? resolveById(targetId) : pickDefault();
|
|
416
|
+
if (!chosen && profile.driver === "extension" && candidates.length === 1) {
|
|
417
|
+
// If an agent passes a stale/foreign targetId but we only have a single attached tab,
|
|
418
|
+
// recover by using that tab instead of failing hard.
|
|
419
|
+
chosen = candidates[0] ?? null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (chosen === "AMBIGUOUS") {
|
|
423
|
+
throw new Error("ambiguous target id prefix");
|
|
424
|
+
}
|
|
425
|
+
if (!chosen) {
|
|
426
|
+
throw new Error("tab not found");
|
|
427
|
+
}
|
|
428
|
+
profileState.lastTargetId = chosen.targetId;
|
|
429
|
+
return chosen;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const focusTab = async (targetId: string): Promise<void> => {
|
|
433
|
+
const tabs = await listTabs();
|
|
434
|
+
const resolved = resolveTargetIdFromTabs(targetId, tabs);
|
|
435
|
+
if (!resolved.ok) {
|
|
436
|
+
if (resolved.reason === "ambiguous") {
|
|
437
|
+
throw new Error("ambiguous target id prefix");
|
|
438
|
+
}
|
|
439
|
+
throw new Error("tab not found");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!profile.cdpIsLoopback) {
|
|
443
|
+
const mod = await getPwAiModule({ mode: "strict" });
|
|
444
|
+
const focusPageByTargetIdViaPlaywright = (mod as Partial<PwAiModule> | null)
|
|
445
|
+
?.focusPageByTargetIdViaPlaywright;
|
|
446
|
+
if (typeof focusPageByTargetIdViaPlaywright === "function") {
|
|
447
|
+
await focusPageByTargetIdViaPlaywright({
|
|
448
|
+
cdpUrl: profile.cdpUrl,
|
|
449
|
+
targetId: resolved.targetId,
|
|
450
|
+
});
|
|
451
|
+
const profileState = getProfileState();
|
|
452
|
+
profileState.lastTargetId = resolved.targetId;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await fetchOk(appendCdpPath(profile.cdpUrl, `/json/activate/${resolved.targetId}`));
|
|
458
|
+
const profileState = getProfileState();
|
|
459
|
+
profileState.lastTargetId = resolved.targetId;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const closeTab = async (targetId: string): Promise<void> => {
|
|
463
|
+
const tabs = await listTabs();
|
|
464
|
+
const resolved = resolveTargetIdFromTabs(targetId, tabs);
|
|
465
|
+
if (!resolved.ok) {
|
|
466
|
+
if (resolved.reason === "ambiguous") {
|
|
467
|
+
throw new Error("ambiguous target id prefix");
|
|
468
|
+
}
|
|
469
|
+
throw new Error("tab not found");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// For remote profiles, use Playwright's persistent connection to close tabs
|
|
473
|
+
if (!profile.cdpIsLoopback) {
|
|
474
|
+
const mod = await getPwAiModule({ mode: "strict" });
|
|
475
|
+
const closePageByTargetIdViaPlaywright = (mod as Partial<PwAiModule> | null)
|
|
476
|
+
?.closePageByTargetIdViaPlaywright;
|
|
477
|
+
if (typeof closePageByTargetIdViaPlaywright === "function") {
|
|
478
|
+
await closePageByTargetIdViaPlaywright({
|
|
479
|
+
cdpUrl: profile.cdpUrl,
|
|
480
|
+
targetId: resolved.targetId,
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await fetchOk(appendCdpPath(profile.cdpUrl, `/json/close/${resolved.targetId}`));
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const stopRunningBrowser = async (): Promise<{ stopped: boolean }> => {
|
|
490
|
+
if (profile.driver === "extension") {
|
|
491
|
+
const stopped = await stopChromeExtensionRelayServer({
|
|
492
|
+
cdpUrl: profile.cdpUrl,
|
|
493
|
+
});
|
|
494
|
+
return { stopped };
|
|
495
|
+
}
|
|
496
|
+
const profileState = getProfileState();
|
|
497
|
+
if (!profileState.running) {
|
|
498
|
+
return { stopped: false };
|
|
499
|
+
}
|
|
500
|
+
await stopOpenClawChrome(profileState.running);
|
|
501
|
+
setProfileRunning(null);
|
|
502
|
+
return { stopped: true };
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const resetProfile = async () => {
|
|
506
|
+
if (profile.driver === "extension") {
|
|
507
|
+
await stopChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch(() => {});
|
|
508
|
+
return { moved: false, from: profile.cdpUrl };
|
|
509
|
+
}
|
|
510
|
+
if (!profile.cdpIsLoopback) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`reset-profile is only supported for local profiles (profile "${profile.name}" is remote).`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const userDataDir = resolveOpenClawUserDataDir(profile.name);
|
|
516
|
+
const profileState = getProfileState();
|
|
517
|
+
|
|
518
|
+
const httpReachable = await isHttpReachable(300);
|
|
519
|
+
if (httpReachable && !profileState.running) {
|
|
520
|
+
// Port in use but not by us - kill it
|
|
521
|
+
try {
|
|
522
|
+
const mod = await import("./pw-ai.js");
|
|
523
|
+
await mod.closePlaywrightBrowserConnection();
|
|
524
|
+
} catch {
|
|
525
|
+
// ignore
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (profileState.running) {
|
|
530
|
+
await stopRunningBrowser();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const mod = await import("./pw-ai.js");
|
|
535
|
+
await mod.closePlaywrightBrowserConnection();
|
|
536
|
+
} catch {
|
|
537
|
+
// ignore
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!fs.existsSync(userDataDir)) {
|
|
541
|
+
return { moved: false, from: userDataDir };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const moved = await movePathToTrash(userDataDir);
|
|
545
|
+
return { moved: true, from: userDataDir, to: moved };
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
profile,
|
|
550
|
+
ensureBrowserAvailable,
|
|
551
|
+
ensureTabAvailable,
|
|
552
|
+
isHttpReachable,
|
|
553
|
+
isReachable,
|
|
554
|
+
listTabs,
|
|
555
|
+
openTab,
|
|
556
|
+
focusTab,
|
|
557
|
+
closeTab,
|
|
558
|
+
stopRunningBrowser,
|
|
559
|
+
resetProfile,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteContext {
|
|
564
|
+
const refreshConfigFromDisk = opts.refreshConfigFromDisk === true;
|
|
565
|
+
|
|
566
|
+
const state = () => {
|
|
567
|
+
const current = opts.getState();
|
|
568
|
+
if (!current) {
|
|
569
|
+
throw new Error("Browser server not started");
|
|
570
|
+
}
|
|
571
|
+
return current;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const forProfile = (profileName?: string): ProfileContext => {
|
|
575
|
+
const current = state();
|
|
576
|
+
const name = profileName ?? current.resolved.defaultProfile;
|
|
577
|
+
const profile = resolveBrowserProfileWithHotReload({
|
|
578
|
+
current,
|
|
579
|
+
refreshConfigFromDisk,
|
|
580
|
+
name,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
if (!profile) {
|
|
584
|
+
const available = Object.keys(current.resolved.profiles).join(", ");
|
|
585
|
+
throw new Error(`Profile "${name}" not found. Available profiles: ${available || "(none)"}`);
|
|
586
|
+
}
|
|
587
|
+
return createProfileContext(opts, profile);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const listProfiles = async (): Promise<ProfileStatus[]> => {
|
|
591
|
+
const current = state();
|
|
592
|
+
refreshResolvedBrowserConfigFromDisk({
|
|
593
|
+
current,
|
|
594
|
+
refreshConfigFromDisk,
|
|
595
|
+
mode: "cached",
|
|
596
|
+
});
|
|
597
|
+
const result: ProfileStatus[] = [];
|
|
598
|
+
|
|
599
|
+
for (const name of Object.keys(current.resolved.profiles)) {
|
|
600
|
+
const profileState = current.profiles.get(name);
|
|
601
|
+
const profile = resolveProfile(current.resolved, name);
|
|
602
|
+
if (!profile) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let tabCount = 0;
|
|
607
|
+
let running = false;
|
|
608
|
+
|
|
609
|
+
if (profileState?.running) {
|
|
610
|
+
running = true;
|
|
611
|
+
try {
|
|
612
|
+
const ctx = createProfileContext(opts, profile);
|
|
613
|
+
const tabs = await ctx.listTabs();
|
|
614
|
+
tabCount = tabs.filter((t) => t.type === "page").length;
|
|
615
|
+
} catch {
|
|
616
|
+
// Browser might not be responsive
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
// Check if something is listening on the port
|
|
620
|
+
try {
|
|
621
|
+
const reachable = await isChromeReachable(profile.cdpUrl, 200);
|
|
622
|
+
if (reachable) {
|
|
623
|
+
running = true;
|
|
624
|
+
const ctx = createProfileContext(opts, profile);
|
|
625
|
+
const tabs = await ctx.listTabs().catch(() => []);
|
|
626
|
+
tabCount = tabs.filter((t) => t.type === "page").length;
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
// Not reachable
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
result.push({
|
|
634
|
+
name,
|
|
635
|
+
cdpPort: profile.cdpPort,
|
|
636
|
+
cdpUrl: profile.cdpUrl,
|
|
637
|
+
color: profile.color,
|
|
638
|
+
running,
|
|
639
|
+
tabCount,
|
|
640
|
+
isDefault: name === current.resolved.defaultProfile,
|
|
641
|
+
isRemote: !profile.cdpIsLoopback,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return result;
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Create default profile context for backward compatibility
|
|
649
|
+
const getDefaultContext = () => forProfile();
|
|
650
|
+
|
|
651
|
+
const mapTabError = (err: unknown) => {
|
|
652
|
+
if (err instanceof SsrFBlockedError) {
|
|
653
|
+
return { status: 400, message: err.message };
|
|
654
|
+
}
|
|
655
|
+
if (err instanceof InvalidBrowserNavigationUrlError) {
|
|
656
|
+
return { status: 400, message: err.message };
|
|
657
|
+
}
|
|
658
|
+
const msg = String(err);
|
|
659
|
+
if (msg.includes("ambiguous target id prefix")) {
|
|
660
|
+
return { status: 409, message: "ambiguous target id prefix" };
|
|
661
|
+
}
|
|
662
|
+
if (msg.includes("tab not found")) {
|
|
663
|
+
return { status: 404, message: msg };
|
|
664
|
+
}
|
|
665
|
+
if (msg.includes("not found")) {
|
|
666
|
+
return { status: 404, message: msg };
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
state,
|
|
673
|
+
forProfile,
|
|
674
|
+
listProfiles,
|
|
675
|
+
// Legacy methods delegate to default profile
|
|
676
|
+
ensureBrowserAvailable: () => getDefaultContext().ensureBrowserAvailable(),
|
|
677
|
+
ensureTabAvailable: (targetId) => getDefaultContext().ensureTabAvailable(targetId),
|
|
678
|
+
isHttpReachable: (timeoutMs) => getDefaultContext().isHttpReachable(timeoutMs),
|
|
679
|
+
isReachable: (timeoutMs) => getDefaultContext().isReachable(timeoutMs),
|
|
680
|
+
listTabs: () => getDefaultContext().listTabs(),
|
|
681
|
+
openTab: (url) => getDefaultContext().openTab(url),
|
|
682
|
+
focusTab: (targetId) => getDefaultContext().focusTab(targetId),
|
|
683
|
+
closeTab: (targetId) => getDefaultContext().closeTab(targetId),
|
|
684
|
+
stopRunningBrowser: () => getDefaultContext().stopRunningBrowser(),
|
|
685
|
+
resetProfile: () => getDefaultContext().resetProfile(),
|
|
686
|
+
mapTabError,
|
|
687
|
+
};
|
|
688
|
+
}
|