mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(browser): default act timeout budget
Co-authored-by: Andy Lin <andyylin@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/nodes: add disabled-by-default `gateway.nodes.pairing.autoApproveCidrs` for first-time node pairing from explicit trusted CIDRs, while keeping operator/browser pairing and all upgrade flows manual. Fixes #60800. Thanks @sahilsatralkar.
|
||||
- Browser: add viewport coordinate clicks for managed and existing-session automation, plus `openclaw browser click-coords` for CLI use. (#54452) Thanks @dluttz.
|
||||
- Browser: add `browser.actionTimeoutMs` and use a 60s default action budget so healthy long browser waits do not fail at the client transport boundary. (#62589) Thanks @andyylin.
|
||||
- Browser/config: support per-profile `browser.profiles.<name>.headless` overrides for locally launched browser profiles, so one profile can run headless without forcing all browser profiles headless. Thanks @nakamotoliu.
|
||||
- Plugins/PDF: move local PDF extraction into a bundled `document-extract` plugin so core no longer owns `pdfjs-dist` or PDF image-rendering dependencies. Thanks @vincentkoc.
|
||||
- Dependencies/memory: stop installing `node-llama-cpp` by default; local embeddings now load it only when operators install the optional runtime package. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
13b68287fec00108ca66032120909a0eac797ed541e026357e175e3fce5bacdd config-baseline.json
|
||||
77ee66fb3b2cde94b393712bc03a132b096cf601c193bde1fe42902eecb0b66b config-baseline.core.json
|
||||
f1fd4557473391980caf6d6b32f78e4de25f8504b29dfe083f7f9e325d0b204c config-baseline.json
|
||||
68e0784ca0f9279d49b40ce4493e1cb2c416e1fb70a137a853a10a8c078c97ca config-baseline.core.json
|
||||
d72032762ab46b99480b57deb81130a0ab5b1401189cfbaf4f7fef4a063a7f6c config-baseline.channel.json
|
||||
0d5ba81f0030bd39b7ae285096276cc18b150836c2252fd2217329fc6154e80e config-baseline.plugin.json
|
||||
0504c4f38d4c753fffeb465c93540d829df6b0fcef921eb0e2226ac16bdbbe07 config-baseline.plugin.json
|
||||
|
||||
@@ -242,6 +242,8 @@ This path is host-only. For Docker, headless servers, Browserless, or other remo
|
||||
Current existing-session limits:
|
||||
|
||||
- snapshot-driven actions use refs, not CSS selectors
|
||||
- `browser.actionTimeoutMs` defaults supported `act` requests to 60000 ms when
|
||||
callers omit `timeoutMs`; per-call `timeoutMs` still wins.
|
||||
- `click` is left-click only
|
||||
- `type` does not support `slowly=true`
|
||||
- `press` does not support `delayMs`
|
||||
|
||||
@@ -129,6 +129,7 @@ Browser settings live in `~/.openclaw/openclaw.json`.
|
||||
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
|
||||
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
|
||||
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
|
||||
actionTimeoutMs: 60000, // default browser act timeout (ms)
|
||||
tabCleanup: {
|
||||
enabled: true, // default: true
|
||||
idleMinutes: 120, // set 0 to disable idle cleanup
|
||||
@@ -173,6 +174,7 @@ Browser settings live in `~/.openclaw/openclaw.json`.
|
||||
- Control service binds to loopback on a port derived from `gateway.port` (default `18791` = gateway + 2). Overriding `gateway.port` or `OPENCLAW_GATEWAY_PORT` shifts the derived ports in the same family.
|
||||
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; set those only for remote CDP. `cdpUrl` defaults to the managed local CDP port when unset.
|
||||
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP HTTP reachability checks; `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket handshakes.
|
||||
- `actionTimeoutMs` is the default budget for browser `act` requests when the caller does not pass `timeoutMs`. The client transport adds a small slack window so long waits can finish instead of timing out at the HTTP boundary.
|
||||
- `tabCleanup` is best-effort cleanup for tabs opened by primary-agent browser sessions. Subagent, cron, and ACP lifecycle cleanup still closes their explicit tracked tabs at session end; primary sessions keep active tabs reusable, then close idle or excess tracked tabs in the background.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveProfile,
|
||||
wrapExternalContent,
|
||||
} from "./browser-tool.runtime.js";
|
||||
import { DEFAULT_BROWSER_ACTION_TIMEOUT_MS } from "./browser/constants.js";
|
||||
|
||||
const browserToolActionDeps = {
|
||||
browserAct,
|
||||
@@ -25,6 +26,94 @@ const browserToolActionDeps = {
|
||||
loadConfig,
|
||||
};
|
||||
|
||||
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000;
|
||||
|
||||
type BrowserActRequest = Parameters<typeof browserAct>[1];
|
||||
type BrowserActRequestWithTimeout = BrowserActRequest & { timeoutMs?: number };
|
||||
|
||||
function normalizePositiveTimeoutMs(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? Math.floor(value)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function supportsBrowserActTimeout(request: BrowserActRequest): boolean {
|
||||
switch (request.kind) {
|
||||
case "click":
|
||||
case "type":
|
||||
case "hover":
|
||||
case "scrollIntoView":
|
||||
case "drag":
|
||||
case "select":
|
||||
case "fill":
|
||||
case "evaluate":
|
||||
case "wait":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function existingSessionRejectsActTimeout(request: BrowserActRequest): boolean {
|
||||
switch (request.kind) {
|
||||
case "type":
|
||||
case "hover":
|
||||
case "scrollIntoView":
|
||||
case "drag":
|
||||
case "select":
|
||||
case "fill":
|
||||
case "evaluate":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function usesExistingSessionProfile(profileName: string | undefined): boolean {
|
||||
const cfg = browserToolActionDeps.loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
||||
const profile = resolveProfile(resolved, profileName ?? resolved.defaultProfile);
|
||||
return profile ? getBrowserProfileCapabilities(profile).usesChromeMcp : false;
|
||||
}
|
||||
|
||||
function withConfiguredActTimeout(
|
||||
request: BrowserActRequest,
|
||||
profileName: string | undefined,
|
||||
): BrowserActRequest {
|
||||
const typedRequest = request as BrowserActRequestWithTimeout;
|
||||
if (normalizePositiveTimeoutMs(typedRequest.timeoutMs) !== undefined) {
|
||||
return request;
|
||||
}
|
||||
if (!supportsBrowserActTimeout(request)) {
|
||||
return request;
|
||||
}
|
||||
if (existingSessionRejectsActTimeout(request) && usesExistingSessionProfile(profileName)) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const cfg = browserToolActionDeps.loadConfig();
|
||||
const configuredTimeout =
|
||||
normalizePositiveTimeoutMs(cfg.browser?.actionTimeoutMs) ?? DEFAULT_BROWSER_ACTION_TIMEOUT_MS;
|
||||
return { ...typedRequest, timeoutMs: configuredTimeout } as BrowserActRequest;
|
||||
}
|
||||
|
||||
function resolveActProxyTimeoutMs(request: BrowserActRequest): number | undefined {
|
||||
const candidateTimeouts: number[] = [];
|
||||
const explicitTimeout = normalizePositiveTimeoutMs(
|
||||
(request as BrowserActRequestWithTimeout).timeoutMs,
|
||||
);
|
||||
if (explicitTimeout !== undefined) {
|
||||
candidateTimeouts.push(explicitTimeout + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
|
||||
}
|
||||
if (request.kind === "wait") {
|
||||
const waitDuration = normalizePositiveTimeoutMs(request.timeMs);
|
||||
if (waitDuration !== undefined) {
|
||||
candidateTimeouts.push(waitDuration + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
|
||||
}
|
||||
}
|
||||
return candidateTimeouts.length ? Math.max(...candidateTimeouts) : undefined;
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
setDepsForTest(
|
||||
overrides: Partial<{
|
||||
@@ -408,32 +497,34 @@ export async function executeConsoleAction(params: {
|
||||
}
|
||||
|
||||
export async function executeActAction(params: {
|
||||
request: Parameters<typeof browserAct>[1];
|
||||
request: BrowserActRequest;
|
||||
baseUrl?: string;
|
||||
profile?: string;
|
||||
proxyRequest: BrowserProxyRequest | null;
|
||||
onTabActivity?: (targetId: string | undefined) => void;
|
||||
}): Promise<AgentToolResult<unknown>> {
|
||||
const { request, baseUrl, profile, proxyRequest } = params;
|
||||
const effectiveRequest = withConfiguredActTimeout(request, profile);
|
||||
try {
|
||||
const result = proxyRequest
|
||||
? await proxyRequest({
|
||||
method: "POST",
|
||||
path: "/act",
|
||||
profile,
|
||||
body: request,
|
||||
body: effectiveRequest,
|
||||
timeoutMs: resolveActProxyTimeoutMs(effectiveRequest),
|
||||
})
|
||||
: await browserToolActionDeps.browserAct(baseUrl, request, {
|
||||
: await browserToolActionDeps.browserAct(baseUrl, effectiveRequest, {
|
||||
profile,
|
||||
});
|
||||
params.onTabActivity?.(
|
||||
readStringValue((result as { targetId?: unknown }).targetId) ??
|
||||
readStringValue(request.targetId),
|
||||
readStringValue(effectiveRequest.targetId),
|
||||
);
|
||||
return jsonResult(result);
|
||||
} catch (err) {
|
||||
if (isChromeStaleTargetError(profile, err)) {
|
||||
const retryRequest = stripTargetIdFromActRequest(request);
|
||||
const retryRequest = stripTargetIdFromActRequest(effectiveRequest);
|
||||
const tabs = proxyRequest
|
||||
? ((
|
||||
(await proxyRequest({
|
||||
@@ -445,7 +536,7 @@ export async function executeActAction(params: {
|
||||
: await browserToolActionDeps.browserTabs(baseUrl, { profile }).catch(() => []);
|
||||
// Some user-browser targetIds can go stale between snapshots and actions.
|
||||
// Only retry safe read-only actions, and only when exactly one tab remains attached.
|
||||
if (retryRequest && canRetryChromeActWithoutTargetId(request) && tabs.length === 1) {
|
||||
if (retryRequest && canRetryChromeActWithoutTargetId(effectiveRequest) && tabs.length === 1) {
|
||||
try {
|
||||
const retryResult = proxyRequest
|
||||
? await proxyRequest({
|
||||
@@ -453,6 +544,7 @@ export async function executeActAction(params: {
|
||||
path: "/act",
|
||||
profile,
|
||||
body: retryRequest,
|
||||
timeoutMs: resolveActProxyTimeoutMs(retryRequest),
|
||||
})
|
||||
: await browserToolActionDeps.browserAct(baseUrl, retryRequest, {
|
||||
profile,
|
||||
|
||||
@@ -69,6 +69,7 @@ const browserConfigMocks = vi.hoisted(() => ({
|
||||
controlPort: 18791,
|
||||
profiles: {},
|
||||
defaultProfile: "openclaw",
|
||||
actionTimeoutMs: 60_000,
|
||||
})),
|
||||
resolveProfile: vi.fn((resolved: Record<string, unknown>, name: string) => {
|
||||
const profile = (resolved.profiles as Record<string, Record<string, unknown>> | undefined)?.[
|
||||
@@ -249,6 +250,7 @@ function resetBrowserToolMocks() {
|
||||
controlPort: 18791,
|
||||
profiles: {},
|
||||
defaultProfile: "openclaw",
|
||||
actionTimeoutMs: 60_000,
|
||||
});
|
||||
nodesUtilsMocks.listNodes.mockResolvedValue([]);
|
||||
browserToolTesting.setDepsForTest({
|
||||
@@ -292,6 +294,7 @@ function setResolvedBrowserProfiles(
|
||||
controlPort: 18791,
|
||||
profiles,
|
||||
defaultProfile,
|
||||
actionTimeoutMs: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1078,6 +1081,87 @@ describe("browser tool act compatibility", () => {
|
||||
expect.objectContaining({ profile: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies configured browser action timeout when act timeout is omitted", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } });
|
||||
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "act",
|
||||
request: {
|
||||
kind: "wait",
|
||||
timeMs: 20_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(browserActionsMocks.browserAct).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
{
|
||||
kind: "wait",
|
||||
timeMs: 20_000,
|
||||
timeoutMs: 45_000,
|
||||
},
|
||||
expect.objectContaining({ profile: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not inject unsupported action timeout for existing-session type actions", async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
configMocks.loadConfig.mockReturnValue({ browser: { actionTimeoutMs: 45_000 } });
|
||||
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "act",
|
||||
profile: "user",
|
||||
target: "host",
|
||||
request: {
|
||||
kind: "type",
|
||||
ref: "f1e3",
|
||||
text: "Test Title",
|
||||
},
|
||||
});
|
||||
|
||||
expect(browserActionsMocks.browserAct).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
{
|
||||
kind: "type",
|
||||
ref: "f1e3",
|
||||
text: "Test Title",
|
||||
},
|
||||
expect.objectContaining({ profile: "user" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes configured act timeout through node proxy with transport slack", async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {
|
||||
actionTimeoutMs: 45_000,
|
||||
},
|
||||
gateway: { nodes: { browser: { node: "node-1" } } },
|
||||
});
|
||||
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", {
|
||||
action: "act",
|
||||
target: "node",
|
||||
request: { kind: "wait", timeMs: 20_000 },
|
||||
});
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 55_000 },
|
||||
expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
path: "/act",
|
||||
body: { kind: "wait", timeMs: 20_000, timeoutMs: 45_000 },
|
||||
timeoutMs: 45_000 + 5_000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser tool snapshot labels", () => {
|
||||
|
||||
@@ -681,6 +681,7 @@ describe("browser chrome launch args", () => {
|
||||
evaluateEnabled: false,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
actionTimeoutMs: 60_000,
|
||||
extraArgs: [],
|
||||
color: "#FF4500",
|
||||
headless: false,
|
||||
|
||||
@@ -6,7 +6,10 @@ import type {
|
||||
import { buildProfileQuery, withBaseUrl } from "./client-actions-url.js";
|
||||
import type { BrowserActRequest, BrowserFormField } from "./client-actions.types.js";
|
||||
import { fetchBrowserJson } from "./client-fetch.js";
|
||||
import { DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS } from "./constants.js";
|
||||
import {
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS,
|
||||
} from "./constants.js";
|
||||
|
||||
export type { BrowserActRequest, BrowserFormField } from "./client-actions.types.js";
|
||||
|
||||
@@ -26,6 +29,29 @@ export type BrowserDownloadPayload = {
|
||||
|
||||
type BrowserDownloadResult = { ok: true; targetId: string; download: BrowserDownloadPayload };
|
||||
|
||||
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000;
|
||||
|
||||
function normalizePositiveTimeoutMs(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? Math.floor(value)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveBrowserActRequestTimeoutMs(req: BrowserActRequest): number {
|
||||
const explicitTimeout = normalizePositiveTimeoutMs((req as { timeoutMs?: unknown }).timeoutMs);
|
||||
const candidateTimeouts =
|
||||
explicitTimeout === undefined
|
||||
? [DEFAULT_BROWSER_ACTION_TIMEOUT_MS]
|
||||
: [explicitTimeout + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS];
|
||||
if (req.kind === "wait") {
|
||||
const waitDuration = normalizePositiveTimeoutMs(req.timeMs);
|
||||
if (waitDuration !== undefined) {
|
||||
candidateTimeouts.push(waitDuration + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
|
||||
}
|
||||
}
|
||||
return Math.max(...candidateTimeouts);
|
||||
}
|
||||
|
||||
async function postDownloadRequest(
|
||||
baseUrl: string | undefined,
|
||||
route: "/wait/download" | "/download",
|
||||
@@ -167,7 +193,7 @@ export async function browserAct(
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
? Math.max(1, Math.floor(opts.timeoutMs))
|
||||
: 20000,
|
||||
: resolveBrowserActRequestTimeoutMs(req),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -334,4 +334,30 @@ describe("browser client", () => {
|
||||
timeoutMs: 20_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("gives browser act requests enough client timeout for long waits", async () => {
|
||||
const calls: Array<{ url: string; init?: RequestInit & { timeoutMs?: number } }> = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string, init?: RequestInit & { timeoutMs?: number }) => {
|
||||
calls.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ ok: true, targetId: "t1" }),
|
||||
} as unknown as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
await browserAct("http://127.0.0.1:18791", { kind: "click", ref: "1" });
|
||||
await browserAct("http://127.0.0.1:18791", {
|
||||
kind: "wait",
|
||||
timeMs: 70_000,
|
||||
});
|
||||
await browserAct("http://127.0.0.1:18791", {
|
||||
kind: "wait",
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
|
||||
expect(calls.map((call) => call.init?.timeoutMs)).toEqual([60_000, 75_000, 50_000]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("browser config", () => {
|
||||
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
|
||||
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
|
||||
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
|
||||
expect(resolved.actionTimeoutMs).toBe(60_000);
|
||||
expect(resolved.tabCleanup).toEqual({
|
||||
enabled: true,
|
||||
idleMinutes: 120,
|
||||
@@ -119,9 +120,11 @@ describe("browser config", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
remoteCdpTimeoutMs: 2200,
|
||||
remoteCdpHandshakeTimeoutMs: 5000,
|
||||
actionTimeoutMs: 45_000,
|
||||
});
|
||||
expect(resolved.remoteCdpTimeoutMs).toBe(2200);
|
||||
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(5000);
|
||||
expect(resolved.actionTimeoutMs).toBe(45_000);
|
||||
});
|
||||
|
||||
it("supports custom browser tab cleanup policy", () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { resolveUserPath } from "../utils.js";
|
||||
import { parseBrowserHttpUrl, redactCdpUrl, isLoopbackHost } from "./cdp.helpers.js";
|
||||
import {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES,
|
||||
@@ -66,6 +67,7 @@ export type ResolvedBrowserConfig = {
|
||||
cdpIsLoopback: boolean;
|
||||
remoteCdpTimeoutMs: number;
|
||||
remoteCdpHandshakeTimeoutMs: number;
|
||||
actionTimeoutMs: number;
|
||||
color: string;
|
||||
executablePath?: string;
|
||||
headless: boolean;
|
||||
@@ -263,6 +265,10 @@ export function resolveBrowserConfig(
|
||||
cfg?.remoteCdpHandshakeTimeoutMs,
|
||||
Math.max(2000, remoteCdpTimeoutMs * 2),
|
||||
);
|
||||
const actionTimeoutMs = normalizeTimeoutMs(
|
||||
cfg?.actionTimeoutMs,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
|
||||
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
|
||||
@@ -343,6 +349,7 @@ export function resolveBrowserConfig(
|
||||
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
|
||||
remoteCdpTimeoutMs,
|
||||
remoteCdpHandshakeTimeoutMs,
|
||||
actionTimeoutMs,
|
||||
color: defaultColor,
|
||||
executablePath,
|
||||
headless,
|
||||
|
||||
@@ -3,6 +3,7 @@ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
||||
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
||||
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
|
||||
export const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 20_000;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_IDLE_MINUTES = 120;
|
||||
export const DEFAULT_BROWSER_TAB_CLEANUP_MAX_TABS_PER_SESSION = 8;
|
||||
|
||||
@@ -44,6 +44,7 @@ function makeState(): BrowserServerState {
|
||||
cdpIsLoopback: true,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
actionTimeoutMs: 60_000,
|
||||
color: "#FF4500",
|
||||
headless: false,
|
||||
noSandbox: false,
|
||||
|
||||
@@ -24,6 +24,7 @@ export function makeState(
|
||||
cdpIsLoopback: profile !== "remote",
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
actionTimeoutMs: 60_000,
|
||||
evaluateEnabled: false,
|
||||
extraArgs: [],
|
||||
color: "#FF4500",
|
||||
|
||||
@@ -37,6 +37,7 @@ export function makeBrowserServerState(params?: {
|
||||
evaluateEnabled: false,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
actionTimeoutMs: 60_000,
|
||||
extraArgs: [],
|
||||
color: profile.color,
|
||||
headless: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
stopBrowserBridgeServer,
|
||||
} from "../../plugin-sdk/browser-bridge.js";
|
||||
import {
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
@@ -96,6 +97,7 @@ function buildSandboxBrowserResolvedConfig(params: {
|
||||
cdpPortRangeEnd: cdpPortRange.end,
|
||||
remoteCdpTimeoutMs: 1500,
|
||||
remoteCdpHandshakeTimeoutMs: 3000,
|
||||
actionTimeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
color: DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
executablePath: undefined,
|
||||
headless: params.headless,
|
||||
|
||||
@@ -603,6 +603,14 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
description:
|
||||
"Timeout in milliseconds for post-connect CDP handshake readiness checks against remote browser targets. Raise this for slow-start remote browsers and lower to fail fast in automation loops.",
|
||||
},
|
||||
actionTimeoutMs: {
|
||||
type: "integer",
|
||||
exclusiveMinimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
title: "Browser Action Timeout (ms)",
|
||||
description:
|
||||
"Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
title: "Browser Accent Color",
|
||||
@@ -23933,6 +23941,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
help: "Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
"browser.actionTimeoutMs": {
|
||||
label: "Browser Action Timeout (ms)",
|
||||
help: "Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
|
||||
tags: ["performance"],
|
||||
},
|
||||
"browser.color": {
|
||||
label: "Browser Accent Color",
|
||||
help: "Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",
|
||||
|
||||
@@ -258,6 +258,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Enables browser capability wiring in the gateway so browser tools and CDP-driven workflows can run. Disable when browser automation is not needed to reduce surface area and startup work.",
|
||||
"browser.cdpUrl":
|
||||
"Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
|
||||
"browser.actionTimeoutMs":
|
||||
"Default timeout in milliseconds for browser act requests before the client gives up waiting. Raise this when healthy waits or UI interactions exceed the default request budget.",
|
||||
"browser.color":
|
||||
"Default accent color used for browser profile/UI cues where colored identity hints are displayed. Use consistent colors to help operators identify active browser profile context quickly.",
|
||||
"browser.executablePath":
|
||||
|
||||
@@ -140,6 +140,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
browser: "Browser",
|
||||
"browser.enabled": "Browser Enabled",
|
||||
"browser.cdpUrl": "Browser CDP URL",
|
||||
"browser.actionTimeoutMs": "Browser Action Timeout (ms)",
|
||||
"browser.color": "Browser Accent Color",
|
||||
"browser.executablePath": "Browser Executable Path",
|
||||
"browser.headless": "Browser Headless Mode",
|
||||
|
||||
@@ -54,6 +54,8 @@ export type BrowserConfig = {
|
||||
remoteCdpTimeoutMs?: number;
|
||||
/** Remote CDP WebSocket handshake timeout (ms). Default: max(remoteCdpTimeoutMs * 2, 2000). */
|
||||
remoteCdpHandshakeTimeoutMs?: number;
|
||||
/** Default browser act timeout (ms). Default: 60000. */
|
||||
actionTimeoutMs?: number;
|
||||
/** Accent color for the openclaw browser profile (hex). Default: #FF4500 */
|
||||
color?: string;
|
||||
/** Override the browser executable path (all platforms). */
|
||||
|
||||
@@ -381,6 +381,7 @@ export const OpenClawSchema = z
|
||||
cdpUrl: z.string().optional(),
|
||||
remoteCdpTimeoutMs: z.number().int().nonnegative().optional(),
|
||||
remoteCdpHandshakeTimeoutMs: z.number().int().nonnegative().optional(),
|
||||
actionTimeoutMs: z.number().int().positive().optional(),
|
||||
color: z.string().optional(),
|
||||
executablePath: z.string().optional(),
|
||||
headless: z.boolean().optional(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
DEFAULT_AI_SNAPSHOT_MAX_CHARS,
|
||||
DEFAULT_BROWSER_ACTION_TIMEOUT_MS,
|
||||
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
|
||||
@@ -9,6 +9,7 @@ export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
||||
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
||||
export const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw";
|
||||
export const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 60_000;
|
||||
export const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 80_000;
|
||||
export const DEFAULT_UPLOAD_DIR = path.join(resolvePreferredOpenClawTmpDir(), "uploads");
|
||||
|
||||
@@ -30,6 +31,7 @@ export type ResolvedBrowserConfig = {
|
||||
cdpIsLoopback: boolean;
|
||||
remoteCdpTimeoutMs: number;
|
||||
remoteCdpHandshakeTimeoutMs: number;
|
||||
actionTimeoutMs: number;
|
||||
color: string;
|
||||
executablePath?: string;
|
||||
headless: boolean;
|
||||
|
||||
Reference in New Issue
Block a user