mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 06:31:11 +00:00
perf(browser): precompute browser help
This commit is contained in:
parent
3064ea78ab
commit
c977643460
8 changed files with 212 additions and 17 deletions
|
|
@ -55,6 +55,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Subagents: fall back to direct completion delivery when the parent announce turn finishes without a visible payload, so child results still reach channel-backed requester sessions.
|
||||
- Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.
|
||||
- Browser/CLI: lazy-load browser command groups and plugin runtime services so `openclaw browser --help` can render without loading the full browser automation stack. Fixes #65400. (#65460, #66640) Thanks @pandego and @Tianworld.
|
||||
- Browser/CLI: serve precomputed `openclaw browser --help` text from CLI startup metadata, avoiding the full plugin/config startup path for the common help invocation.
|
||||
- Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser.
|
||||
- Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger.
|
||||
- Browser/client: stop telling agents to permanently avoid the browser after transient timeout or cancellation failures; keep the no-retry hint for persistent unavailable/rate-limit cases. (#46505) Thanks @jriff.
|
||||
|
|
|
|||
31
openclaw.mjs
31
openclaw.mjs
|
|
@ -128,13 +128,18 @@ const buildMissingEntryErrorMessage = async () => {
|
|||
const isBareRootHelpInvocation = (argv) =>
|
||||
argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h");
|
||||
|
||||
const loadPrecomputedRootHelpText = () => {
|
||||
const isBrowserHelpInvocation = (argv) =>
|
||||
argv.length === 4 && argv[2] === "browser" && (argv[3] === "--help" || argv[3] === "-h");
|
||||
|
||||
const isHelpFastPathDisabled = () =>
|
||||
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1";
|
||||
|
||||
const loadPrecomputedHelpText = (key) => {
|
||||
try {
|
||||
const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return typeof parsed?.rootHelpText === "string" && parsed.rootHelpText.length > 0
|
||||
? parsed.rootHelpText
|
||||
: null;
|
||||
const value = parsed?.[key];
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -144,7 +149,7 @@ const tryOutputBareRootHelp = async () => {
|
|||
if (!isBareRootHelpInvocation(process.argv)) {
|
||||
return false;
|
||||
}
|
||||
const precomputed = loadPrecomputedRootHelpText();
|
||||
const precomputed = loadPrecomputedHelpText("rootHelpText");
|
||||
if (precomputed) {
|
||||
process.stdout.write(precomputed);
|
||||
return true;
|
||||
|
|
@ -166,7 +171,21 @@ const tryOutputBareRootHelp = async () => {
|
|||
return false;
|
||||
};
|
||||
|
||||
if (await tryOutputBareRootHelp()) {
|
||||
const tryOutputBrowserHelp = () => {
|
||||
if (!isBrowserHelpInvocation(process.argv)) {
|
||||
return false;
|
||||
}
|
||||
const precomputed = loadPrecomputedHelpText("browserHelpText");
|
||||
if (!precomputed) {
|
||||
return false;
|
||||
}
|
||||
process.stdout.write(precomputed);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!isHelpFastPathDisabled() && (await tryOutputBareRootHelp())) {
|
||||
// OK
|
||||
} else if (!isHelpFastPathDisabled() && tryOutputBrowserHelp()) {
|
||||
// OK
|
||||
} else {
|
||||
await installProcessWarningFilter();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const distDir = path.join(rootDir, "dist");
|
|||
const outputPath = path.join(distDir, "cli-startup-metadata.json");
|
||||
const extensionsDir = path.join(rootDir, "extensions");
|
||||
const ROOT_HELP_RENDER_TIMEOUT_MS = 120_000;
|
||||
const BROWSER_HELP_RENDER_TIMEOUT_MS = 120_000;
|
||||
const CORE_CHANNEL_ORDER = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
|
|
@ -70,6 +71,29 @@ function resolveRootHelpBundleIdentity(
|
|||
};
|
||||
}
|
||||
|
||||
function updateHashFromFiles(hash: ReturnType<typeof createHash>, files: string[]) {
|
||||
for (const file of files.toSorted()) {
|
||||
hash.update(`${path.relative(rootDir, file)}\0`);
|
||||
hash.update(readFileSync(file));
|
||||
hash.update("\0");
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBrowserHelpSourceSignature(): string {
|
||||
const hash = createHash("sha1");
|
||||
const browserCliDir = path.join(rootDir, "extensions/browser/src/cli");
|
||||
const browserCliFiles = readdirSync(browserCliDir)
|
||||
.filter((entry) => entry.endsWith(".ts"))
|
||||
.map((entry) => path.join(browserCliDir, entry));
|
||||
updateHashFromFiles(hash, browserCliFiles);
|
||||
updateHashFromFiles(hash, [
|
||||
path.join(rootDir, "src/cli/program/help.ts"),
|
||||
path.join(rootDir, "src/cli/program/context.ts"),
|
||||
path.join(rootDir, "src/cli/banner.ts"),
|
||||
]);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export function readBundledChannelCatalog(
|
||||
extensionsDirOverride: string = extensionsDir,
|
||||
): BundledChannelCatalog {
|
||||
|
|
@ -245,6 +269,53 @@ function renderSourceRootHelpText(
|
|||
return result.stdout ?? "";
|
||||
}
|
||||
|
||||
function renderSourceBrowserHelpText(
|
||||
renderContext: RootHelpRenderContext = createIsolatedRootHelpRenderContext(),
|
||||
): string {
|
||||
const browserCliUrl = pathToFileURL(
|
||||
path.join(rootDir, "extensions/browser/src/cli/browser-cli.ts"),
|
||||
).href;
|
||||
const helpUrl = pathToFileURL(path.join(rootDir, "src/cli/program/help.ts")).href;
|
||||
const contextUrl = pathToFileURL(path.join(rootDir, "src/cli/program/context.ts")).href;
|
||||
const inlineModule = [
|
||||
`const { Command } = await import("commander");`,
|
||||
`const { registerBrowserCli } = await import(${JSON.stringify(browserCliUrl)});`,
|
||||
`const { configureProgramHelp } = await import(${JSON.stringify(helpUrl)});`,
|
||||
`const { createProgramContext } = await import(${JSON.stringify(contextUrl)});`,
|
||||
`const program = new Command();`,
|
||||
`configureProgramHelp(program, createProgramContext());`,
|
||||
`registerBrowserCli(program, ["node", "openclaw", "browser", "--help"]);`,
|
||||
`const browser = program.commands.find((cmd) => cmd.name() === "browser");`,
|
||||
`if (!browser) throw new Error("Browser command was not registered.");`,
|
||||
`browser.outputHelp();`,
|
||||
"process.exit(0);",
|
||||
].join("\n");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "--input-type=module", "--eval", inlineModule],
|
||||
{
|
||||
cwd: rootDir,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...renderContext.env,
|
||||
OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH: "1",
|
||||
},
|
||||
timeout: BROWSER_HELP_RENDER_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
const stderr = result.stderr?.trim();
|
||||
throw new Error(
|
||||
"Failed to render source browser help" +
|
||||
(stderr ? `: ${stderr}` : result.signal ? `: terminated by ${result.signal}` : ""),
|
||||
);
|
||||
}
|
||||
return result.stdout ?? "";
|
||||
}
|
||||
|
||||
export async function writeCliStartupMetadata(options?: {
|
||||
distDir?: string;
|
||||
outputPath?: string;
|
||||
|
|
@ -255,6 +326,7 @@ export async function writeCliStartupMetadata(options?: {
|
|||
const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir;
|
||||
const channelCatalog = readBundledChannelCatalog(resolvedExtensionsDir);
|
||||
const bundleIdentity = resolveRootHelpBundleIdentity(resolvedDistDir);
|
||||
const browserHelpSourceSignature = resolveBrowserHelpSourceSignature();
|
||||
const bundledPluginsDir = path.join(resolvedDistDir, "extensions");
|
||||
const renderContext = createIsolatedRootHelpRenderContext(
|
||||
existsSync(bundledPluginsDir) ? bundledPluginsDir : resolvedExtensionsDir,
|
||||
|
|
@ -264,12 +336,17 @@ export async function writeCliStartupMetadata(options?: {
|
|||
try {
|
||||
const existing = JSON.parse(readFileSync(resolvedOutputPath, "utf8")) as {
|
||||
rootHelpBundleSignature?: unknown;
|
||||
browserHelpSourceSignature?: unknown;
|
||||
channelCatalogSignature?: unknown;
|
||||
browserHelpText?: unknown;
|
||||
};
|
||||
if (
|
||||
bundleIdentity &&
|
||||
existing.rootHelpBundleSignature === bundleIdentity.signature &&
|
||||
existing.channelCatalogSignature === channelCatalog.signature
|
||||
existing.browserHelpSourceSignature === browserHelpSourceSignature &&
|
||||
existing.channelCatalogSignature === channelCatalog.signature &&
|
||||
typeof existing.browserHelpText === "string" &&
|
||||
existing.browserHelpText.length > 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -283,6 +360,7 @@ export async function writeCliStartupMetadata(options?: {
|
|||
} catch {
|
||||
rootHelpText = renderSourceRootHelpText(renderContext);
|
||||
}
|
||||
const browserHelpText = renderSourceBrowserHelpText(renderContext);
|
||||
|
||||
mkdirSync(resolvedDistDir, { recursive: true });
|
||||
writeFileSync(
|
||||
|
|
@ -293,6 +371,8 @@ export async function writeCliStartupMetadata(options?: {
|
|||
channelOptions,
|
||||
channelCatalogSignature: channelCatalog.signature,
|
||||
rootHelpBundleSignature: bundleIdentity?.signature ?? null,
|
||||
browserHelpSourceSignature,
|
||||
browserHelpText,
|
||||
rootHelpText,
|
||||
},
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import path from "node:path";
|
|||
import { fileURLToPath } from "node:url";
|
||||
|
||||
let precomputedRootHelpText: string | null | undefined;
|
||||
let precomputedBrowserHelpText: string | null | undefined;
|
||||
|
||||
export function loadPrecomputedRootHelpText(): string | null {
|
||||
if (precomputedRootHelpText !== undefined) {
|
||||
return precomputedRootHelpText;
|
||||
function loadPrecomputedHelpText(
|
||||
key: "rootHelpText" | "browserHelpText",
|
||||
cache: string | null | undefined,
|
||||
setCache: (value: string | null) => void,
|
||||
): string | null {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
}
|
||||
try {
|
||||
const metadataPath = path.resolve(
|
||||
|
|
@ -15,18 +20,31 @@ export function loadPrecomputedRootHelpText(): string | null {
|
|||
"cli-startup-metadata.json",
|
||||
);
|
||||
const raw = fs.readFileSync(metadataPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as { rootHelpText?: unknown };
|
||||
if (typeof parsed.rootHelpText === "string" && parsed.rootHelpText.length > 0) {
|
||||
precomputedRootHelpText = parsed.rootHelpText;
|
||||
return precomputedRootHelpText;
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const value = parsed[key];
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
setCache(value);
|
||||
return value;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to live root-help rendering.
|
||||
// Fall back to live help rendering.
|
||||
}
|
||||
precomputedRootHelpText = null;
|
||||
setCache(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function loadPrecomputedRootHelpText(): string | null {
|
||||
return loadPrecomputedHelpText("rootHelpText", precomputedRootHelpText, (value) => {
|
||||
precomputedRootHelpText = value;
|
||||
});
|
||||
}
|
||||
|
||||
export function loadPrecomputedBrowserHelpText(): string | null {
|
||||
return loadPrecomputedHelpText("browserHelpText", precomputedBrowserHelpText, (value) => {
|
||||
precomputedBrowserHelpText = value;
|
||||
});
|
||||
}
|
||||
|
||||
export function outputPrecomputedRootHelpText(): boolean {
|
||||
const rootHelpText = loadPrecomputedRootHelpText();
|
||||
if (!rootHelpText) {
|
||||
|
|
@ -36,8 +54,18 @@ export function outputPrecomputedRootHelpText(): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function outputPrecomputedBrowserHelpText(): boolean {
|
||||
const browserHelpText = loadPrecomputedBrowserHelpText();
|
||||
if (!browserHelpText) {
|
||||
return false;
|
||||
}
|
||||
process.stdout.write(browserHelpText);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resetPrecomputedRootHelpTextForTests(): void {
|
||||
precomputedRootHelpText = undefined;
|
||||
precomputedBrowserHelpText = undefined;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const ensureTaskRegistryReadyMock = vi.hoisted(() => vi.fn());
|
|||
const startTaskRegistryMaintenanceMock = vi.hoisted(() => vi.fn());
|
||||
const outputRootHelpMock = vi.hoisted(() => vi.fn());
|
||||
const outputPrecomputedRootHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const outputPrecomputedBrowserHelpTextMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const buildProgramMock = vi.hoisted(() => vi.fn());
|
||||
const getProgramContextMock = vi.hoisted(() => vi.fn(() => null));
|
||||
const registerCoreCliByNameMock = vi.hoisted(() => vi.fn());
|
||||
|
|
@ -71,6 +72,7 @@ vi.mock("./program/root-help.js", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("./root-help-metadata.js", () => ({
|
||||
outputPrecomputedBrowserHelpText: outputPrecomputedBrowserHelpTextMock,
|
||||
outputPrecomputedRootHelpText: outputPrecomputedRootHelpTextMock,
|
||||
}));
|
||||
|
||||
|
|
@ -98,8 +100,10 @@ describe("runCli exit behavior", () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
hasMemoryRuntimeMock.mockReturnValue(false);
|
||||
outputPrecomputedBrowserHelpTextMock.mockReturnValue(false);
|
||||
outputPrecomputedRootHelpTextMock.mockReturnValue(false);
|
||||
getProgramContextMock.mockReturnValue(null);
|
||||
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
|
||||
});
|
||||
|
||||
it("does not force process.exit after successful routed command", async () => {
|
||||
|
|
@ -119,6 +123,29 @@ describe("runCli exit behavior", () => {
|
|||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("renders browser help from startup metadata without building the full program", async () => {
|
||||
outputPrecomputedBrowserHelpTextMock.mockReturnValueOnce(true);
|
||||
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
||||
throw new Error(`unexpected process.exit(${String(code)})`);
|
||||
}) as typeof process.exit);
|
||||
|
||||
await runCli(["node", "openclaw", "browser", "--help"]);
|
||||
|
||||
expect(maybeRunCliInContainerMock).toHaveBeenCalledWith([
|
||||
"node",
|
||||
"openclaw",
|
||||
"browser",
|
||||
"--help",
|
||||
]);
|
||||
expect(tryRouteCliMock).not.toHaveBeenCalled();
|
||||
expect(outputPrecomputedBrowserHelpTextMock).toHaveBeenCalledTimes(1);
|
||||
expect(outputRootHelpMock).not.toHaveBeenCalled();
|
||||
expect(buildProgramMock).not.toHaveBeenCalled();
|
||||
expect(closeActiveMemorySearchManagersMock).not.toHaveBeenCalled();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("renders root help without building the full program", async () => {
|
||||
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
||||
throw new Error(`unexpected process.exit(${String(code)})`);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
shouldEnsureCliPath,
|
||||
shouldStartCrestodianForBareRoot,
|
||||
shouldStartCrestodianForModernOnboard,
|
||||
shouldUseBrowserHelpFastPath,
|
||||
shouldUseRootHelpFastPath,
|
||||
} from "./run-main.js";
|
||||
|
||||
|
|
@ -131,6 +132,20 @@ describe("shouldUseRootHelpFastPath", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("shouldUseBrowserHelpFastPath", () => {
|
||||
it("uses the fast path for browser command help only", () => {
|
||||
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "--help"])).toBe(true);
|
||||
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "-h"])).toBe(true);
|
||||
expect(
|
||||
shouldUseBrowserHelpFastPath(["node", "openclaw", "--profile", "work", "browser", "-h"]),
|
||||
).toBe(true);
|
||||
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "browser", "status", "--help"])).toBe(
|
||||
false,
|
||||
);
|
||||
expect(shouldUseBrowserHelpFastPath(["node", "openclaw", "status", "--help"])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMissingPluginCommandMessage", () => {
|
||||
it("explains plugins.allow misses for a bundled plugin command", () => {
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,22 @@ export function shouldEnsureCliPath(argv: string[]): boolean {
|
|||
}
|
||||
|
||||
export function shouldUseRootHelpFastPath(argv: string[]): boolean {
|
||||
return resolveCliArgvInvocation(argv).isRootHelpInvocation;
|
||||
return (
|
||||
process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH !== "1" &&
|
||||
resolveCliArgvInvocation(argv).isRootHelpInvocation
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldUseBrowserHelpFastPath(argv: string[]): boolean {
|
||||
if (process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1") {
|
||||
return false;
|
||||
}
|
||||
const invocation = resolveCliArgvInvocation(argv);
|
||||
return (
|
||||
invocation.commandPath.length === 1 &&
|
||||
invocation.commandPath[0] === "browser" &&
|
||||
invocation.hasHelpOrVersion
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldStartCrestodianForBareRoot(argv: string[]): boolean {
|
||||
|
|
@ -217,6 +232,13 @@ export async function runCli(argv: string[] = process.argv) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (shouldUseBrowserHelpFastPath(normalizedArgv)) {
|
||||
const { outputPrecomputedBrowserHelpText } = await import("./root-help-metadata.js");
|
||||
if (outputPrecomputedBrowserHelpText()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStartCrestodianForBareRoot(normalizedArgv)) {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
console.error(
|
||||
|
|
|
|||
|
|
@ -32,10 +32,13 @@ describe("write-cli-startup-metadata", () => {
|
|||
await writeCliStartupMetadata({ distDir, outputPath, extensionsDir });
|
||||
|
||||
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
|
||||
browserHelpText: string;
|
||||
channelOptions: string[];
|
||||
rootHelpText: string;
|
||||
};
|
||||
expect(written.channelOptions).toContain("matrix");
|
||||
expect(written.browserHelpText).toContain("Usage:");
|
||||
expect(written.browserHelpText).toContain("openclaw browser");
|
||||
expect(written.rootHelpText).toContain("Usage:");
|
||||
expect(written.rootHelpText).toContain("openclaw");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue