UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

203 lines (202 loc) 8.17 kB
import { n as collectErrorGraphCandidates } from "./error-coercion-D_-xJ90S.js"; import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { S as isAcpSessionKey, T as isSubagentSessionKey, w as isCronSessionKey } from "./session-key-BnWWjqNc.js"; import { c as registerUnhandledRejectionHandler } from "./unhandled-rejections-iZOzdBz8.js"; import "./error-runtime-Bz9Tw57Z.js"; import "./runtime-env-0Q-gCyUb.js"; import "./routing-adlWg0R3.js"; import { r as resolveBrowserConfig } from "./config-BosO4Zbt.js"; import { t as getExtensionRelayModule } from "./extension-relay.runtime.js"; import { d as markBrowserRuntimeStopping, r as beginProfileTransition } from "./server-context.lifecycle-Tmt_A8dN.js"; import "./config-C0cjs7WS.js"; import { n as sweepTrackedBrowserTabs } from "./session-tab-registry-7B39oGZ2.js"; //#region extensions/browser/src/browser/server-lifecycle.ts /** Browser server lifecycle helpers for parallel profile shutdown. */ /** Invalidate every profile before awaiting any cleanup, then drain in parallel. */ async function stopKnownBrowserProfiles(params) { const drains = [...params.current.profiles.values()].map((runtime) => beginProfileTransition({ state: params.current, runtime, reason: "Browser runtime shutdown", closeSharedAdapters: params.closeSharedAdapters })); const failed = (await Promise.allSettled(drains)).find((result) => result.status === "rejected"); if (failed?.status === "rejected") { params.onWarn(`openclaw browser stop failed: ${String(failed.reason)}`); throw failed.reason; } } //#endregion //#region extensions/browser/src/browser/session-tab-cleanup.ts /** * Periodic cleanup for browser tabs tracked to primary OpenClaw sessions. */ const MIN_SWEEP_INTERVAL_MS = 6e4; function minutesToMs(minutes) { return Math.max(0, Math.floor(minutes * 6e4)); } /** Returns true for user-facing sessions whose tabs should be tracked for cleanup. */ function isPrimaryTrackedBrowserSessionKey(sessionKey) { return !isSubagentSessionKey(sessionKey) && !isCronSessionKey(sessionKey) && !isAcpSessionKey(sessionKey); } function resolveBrowserTabCleanupRuntimeConfig() { const cfg = getRuntimeConfig(); return resolveBrowserConfig(cfg.browser, cfg).tabCleanup; } /** Starts the recurring Browser tab cleanup timer and returns its disposer. */ function startTrackedBrowserTabCleanupTimer(params) { let stopped = false; let timer = null; let running = null; const schedule = () => { if (stopped) return; let sweepMinutes = 5; try { sweepMinutes = resolveBrowserTabCleanupRuntimeConfig().sweepMinutes; } catch (err) { params.onWarn(`failed to resolve browser tab cleanup config: ${String(err)}`); } timer = setTimeout(run, Math.max(MIN_SWEEP_INTERVAL_MS, minutesToMs(sweepMinutes))); timer.unref?.(); }; const run = () => { if (stopped) return; running = (async () => { const cleanup = resolveBrowserTabCleanupRuntimeConfig(); if (cleanup.enabled) await sweepTrackedBrowserTabs({ idleMs: minutesToMs(cleanup.idleMinutes), maxTabsPerSession: cleanup.maxTabsPerSession, sessionFilter: isPrimaryTrackedBrowserSessionKey, ...params }); })().catch((error) => { params.onWarn(`failed to sweep tracked browser tabs: ${String(error)}`); }).finally(() => { running = null; schedule(); }); }; schedule(); return async () => { stopped = true; if (timer) { clearTimeout(timer); timer = null; } await running?.catch(() => {}); }; } //#endregion //#region extensions/browser/src/browser/unhandled-rejections.ts /** * Browser-specific unhandled rejection filter for benign Playwright dialog * races. */ const PLAYWRIGHT_DIALOG_METHODS = /* @__PURE__ */ new Set(["Page.handleJavaScriptDialog", "Dialog.handleJavaScriptDialog"]); const NO_DIALOG_MESSAGE = "no dialog is showing"; function readMessage(err) { if (typeof err === "string") return err; if (!err || typeof err !== "object") return ""; const message = err.message; return typeof message === "string" ? message : ""; } function readPlaywrightMethod(err) { if (!err || typeof err !== "object") return; const method = err.method; return typeof method === "string" ? method : void 0; } /** Detects Playwright "no dialog is showing" races that can escape as rejections. */ function isPlaywrightDialogRaceUnhandledRejection(reason) { for (const candidate of collectErrorGraphCandidates(reason, (current) => [ current.cause, current.reason, current.original, current.error, current.data, ...Array.isArray(current.errors) ? current.errors : [] ])) { const message = readMessage(candidate); if (!message.toLowerCase().includes(NO_DIALOG_MESSAGE)) continue; const method = readPlaywrightMethod(candidate); if (method && PLAYWRIGHT_DIALOG_METHODS.has(method)) return true; for (const playwrightMethod of PLAYWRIGHT_DIALOG_METHODS) if (message.includes(playwrightMethod)) return true; } return false; } /** Installs the Browser unhandled-rejection filter and returns its disposer. */ function registerBrowserUnhandledRejectionHandler() { return registerUnhandledRejectionHandler(isPlaywrightDialogRaceUnhandledRejection); } //#endregion //#region extensions/browser/src/browser/runtime-lifecycle.ts const trackedTabCleanupDisposers = /* @__PURE__ */ new WeakMap(); /** Creates Browser server state and starts runtime-wide cleanup handlers. */ async function createBrowserRuntimeState(params) { const state = { server: params.server ?? null, port: params.port, resolved: params.resolved, profiles: /* @__PURE__ */ new Map() }; const stopTrackedTabCleanup = startTrackedBrowserTabCleanupTimer({ getResolvedBrowserConfig: () => state.resolved, onWarn: params.onWarn }); trackedTabCleanupDisposers.set(state, stopTrackedTabCleanup); state.stopTrackedTabCleanup = () => { stopTrackedTabCleanup().catch(() => {}); }; state.stopUnhandledRejectionHandler = registerBrowserUnhandledRejectionHandler(); return state; } async function stopBrowserRuntimeInternal(params, finalizeGlobalAdapters) { const current = params.current; if (!current) return; markBrowserRuntimeStopping(current); let firstError; const profileDrain = stopKnownBrowserProfiles({ current, closeSharedAdapters: finalizeGlobalAdapters, onWarn: params.onWarn }); const stopTrackedTabCleanup = trackedTabCleanupDisposers.get(current); const tabCleanup = Promise.resolve().then(async () => { if (stopTrackedTabCleanup) await stopTrackedTabCleanup(); else current.stopTrackedTabCleanup?.(); }); for (const result of await Promise.allSettled([profileDrain, tabCleanup])) if (result.status === "rejected") firstError ??= toRuntimeLifecycleError(result.reason, "Browser profile cleanup failed."); if (current.extensionRelays?.size) try { const { stopExtensionRelays } = await getExtensionRelayModule(); await stopExtensionRelays(current); } catch (err) { firstError ??= toRuntimeLifecycleError(err, "Browser relay cleanup failed."); } if (finalizeGlobalAdapters) try { const { disposeGatewayExtensionRelay } = await import("./gateway-relay-route-C92voZAt.js"); disposeGatewayExtensionRelay(); } catch (err) { firstError ??= toRuntimeLifecycleError(err, "Gateway browser relay cleanup failed."); } if (!firstError) { if (params.closeServer && current.server) await new Promise((resolve) => { current.server?.close(() => resolve()); }); params.clearState(); trackedTabCleanupDisposers.delete(current); current.stopUnhandledRejectionHandler?.(); } if (firstError) throw firstError; } function toRuntimeLifecycleError(value, message) { return value instanceof Error ? value : new Error(message, { cause: value }); } /** Stops Browser profiles, the optional HTTP server, and loaded Playwright state. */ async function stopBrowserRuntime(params) { await stopBrowserRuntimeInternal(params, true); } /** Internal bridge shutdown leaves process-global adapters owned by the main runtime intact. */ async function stopBrowserBridgeRuntime(params) { await stopBrowserRuntimeInternal(params, false); } //#endregion export { stopBrowserBridgeRuntime as n, stopBrowserRuntime as r, createBrowserRuntimeState as t };