UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

307 lines • 13.2 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { DevTools } from '../third_party/index.js'; import { McpHostBindingAdapter } from './McpHostBindingAdapter.js'; /** * A mock implementation of an issues manager that only implements the methods * that are actually used by the IssuesAggregator */ export class FakeIssuesManager extends DevTools.Common.ObjectWrapper .ObjectWrapper { issues() { return []; } } export function overrideDevToolsGlobals({ loadResource, }) { DevTools.Host.InspectorFrontendHost.installInspectorFrontendHost(new McpHostBindingAdapter(loadResource)); // DevTools CDP errors can get noisy. DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true; const noopAgentCommand = () => { return Promise.resolve({ getError: () => undefined, }); }; // Stub out Network emulation commands on the DevTools Agent prototype globally. // This prevents the DevTools Frontend from ever resetting/clearing Puppeteer's // active network blocking/throttling rules during target setup or session lifetime. const networkAgentPrototype = DevTools.ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.get('Network'); if (networkAgentPrototype) { Object.defineProperty(networkAgentPrototype, 'invoke_emulateNetworkConditionsByRule', { value: () => { return Promise.resolve({ ruleIds: [], getError: () => undefined, }); }, writable: true, configurable: true, enumerable: true, }); Object.defineProperty(networkAgentPrototype, 'invoke_overrideNetworkState', { value: noopAgentCommand, writable: true, configurable: true, enumerable: true, }); Object.defineProperty(networkAgentPrototype, 'invoke_enable', { value: noopAgentCommand, writable: true, configurable: true, enumerable: true, }); Object.defineProperty(networkAgentPrototype, 'invoke_disable', { value: noopAgentCommand, writable: true, configurable: true, enumerable: true, }); Object.defineProperty(networkAgentPrototype, 'invoke_setBlockedURLs', { value: noopAgentCommand, writable: true, configurable: true, enumerable: true, }); } // Puppeteer already collects issues from its own Audits subscription. Avoid // enabling the DevTools Frontend's redundant subscription, which can replay // a large retained issue backlog and delay unrelated page work. const auditsAgentPrototype = DevTools.ProtocolClient.InspectorBackend.inspectorBackend.agentPrototypes.get('Audits'); if (auditsAgentPrototype) { Object.defineProperty(auditsAgentPrototype, 'invoke_enable', { value: noopAgentCommand, writable: true, configurable: true, enumerable: true, }); } DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({ create: true, data: { navigatorLanguage: 'en-US', settingLanguage: 'en-US', lookupClosestDevToolsLocale: l => l, }, }); DevTools.I18n.i18n.registerLocaleDataForTest('en-US', {}); DevTools.Formatter.FormatterWorkerPool.FormatterWorkerPool.instance({ forceNew: true, entrypointURL: import.meta .resolve('../third_party/devtools-formatter-worker.js'), }); } export async function createTargetUniverse(session, options) { const settingStorage = new DevTools.Common.Settings.SettingsStorage({}); const universe = new DevTools.Foundation.Universe.Universe({ settingsCreationOptions: { syncedStorage: settingStorage, globalStorage: settingStorage, localStorage: settingStorage, settingRegistrations: DevTools.Common.SettingRegistration.getRegisteredSettings(), }, overrideAutoStartModels: new Set([DevTools.DebuggerModel]), hostConfig: {}, inspectorFrontendHost: DevTools.Host.InspectorFrontendHost.InspectorFrontendHostInstance, supportsEmulation: false, }); const sourceMaps = options?.sourceMaps ?? true; const jsSourceMapsSetting = universe.settings.resolve(DevTools.SDKSettings.jsSourceMapsEnabledSettingDescriptor); jsSourceMapsSetting.set(sourceMaps); const cssSourceMapsSetting = universe.settings.resolve(DevTools.SDKSettings.cssSourceMapsEnabledSettingDescriptor); cssSourceMapsSetting.set(sourceMaps); const setting = universe.settings.resolve(DevTools.SourceMapManager.lazyLoadingSettingDescriptor); setting.set(true); const skipAllPausesSetting = universe.settings.resolve(DevTools.skipAllPausesSettingDescriptor); skipAllPausesSetting.set(true); // @ts-expect-error devtools-frontend has diffrent types. const connection = new DevTools.PuppeteerDevToolsConnection(session); const targetManager = universe.context.get(DevTools.TargetManager); targetManager.observeModels(DevTools.NetworkManager.NetworkManager, DISABLE_NETWORK); const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any /* parentTarget */ null, session.id(), undefined, connection); return { target, universe, session }; } // Not recording network requests in the DevTools universe. // // The network requests are collected through pptr and there isn't a use case for // enabling devtools SDK's network domain. const DISABLE_NETWORK = { modelAdded(model) { void model.target().networkAgent().invoke_disable(); }, modelRemoved() { // Do nothing. }, }; /** * Constructed from Runtime.ExceptionDetails of an uncaught error. * * TODO: Also construct from a RemoteObject of subtype 'error'. * * Consists of the message, a fully resolved stack trace and a fully resolved 'cause' chain. */ export class SymbolizedError { message; stackTrace; cause; constructor(message, stackTrace, cause) { this.message = message; this.stackTrace = stackTrace; this.cause = cause; } static async fromDetails(opts) { const message = SymbolizedError.#getMessage(opts.details); if (!opts.includeStackAndCause || !opts.devTools) { return new SymbolizedError(message, opts.resolvedStackTraceForTesting, opts.resolvedCauseForTesting); } let stackTrace; if (opts.resolvedStackTraceForTesting) { stackTrace = opts.resolvedStackTraceForTesting; } else if (opts.details.stackTrace) { try { stackTrace = await createStackTrace(opts.devTools, opts.details.stackTrace, opts.targetId); } catch { // ignore } } // TODO: Turn opts.details.exception into a JSHandle and retrieve the 'cause' property. // If its an Error, recursively create a SymbolizedError. let cause; if (opts.resolvedCauseForTesting) { cause = opts.resolvedCauseForTesting; } else if (opts.details.exception) { try { const causeRemoteObj = await SymbolizedError.#lookupCause(opts.devTools, opts.details.exception, opts.targetId); if (causeRemoteObj) { cause = await SymbolizedError.fromError({ devTools: opts.devTools, error: causeRemoteObj, targetId: opts.targetId, }); } } catch { // Ignore } } return new SymbolizedError(message, stackTrace, cause); } static async fromError(opts) { const details = await SymbolizedError.#getExceptionDetails(opts.devTools, opts.error, opts.targetId); if (details) { return SymbolizedError.fromDetails({ details, devTools: opts.devTools, targetId: opts.targetId, includeStackAndCause: true, }); } return new SymbolizedError(SymbolizedError.#getMessageFromException(opts.error)); } static #getMessage(details) { // For Runtime.exceptionThrown with a present exception object, `details.text` will be "Uncaught" and // we have to manually parse out the error text from the exception description. // In the case of Runtime.getExceptionDetails, `details.text` has the Error.message. if (details.text === 'Uncaught' && details.exception) { return ('Uncaught ' + SymbolizedError.#getMessageFromException(details.exception)); } return details.text; } static #getMessageFromException(error) { const messageWithRest = error.description?.split('\n at ', 2) ?? []; return messageWithRest[0] ?? ''; } static async #getExceptionDetails(devTools, error, targetId) { if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) { return null; } const targetManager = devTools.universe.context.get(DevTools.TargetManager); const target = targetId ? targetManager.targetById(targetId) || devTools.target : devTools.target; const model = target.model(DevTools.RuntimeModel); return ((await model.getExceptionDetails(error.objectId)) ?? null); } static async #lookupCause(devTools, error, targetId) { if (!devTools || (error.type !== 'object' && error.subtype !== 'error')) { return null; } const targetManager = devTools.universe.context.get(DevTools.TargetManager); const target = targetId ? targetManager.targetById(targetId) || devTools.target : devTools.target; const properties = await target.runtimeAgent().invoke_getProperties({ objectId: error.objectId, }); if (properties.getError()) { return null; } return properties.result.find(prop => prop.name === 'cause')?.value ?? null; } static createForTesting(message, stackTrace, cause) { return new SymbolizedError(message, stackTrace, cause); } } export async function createStackTraceForConsoleMessage(devTools, consoleMessage) { const message = consoleMessage; const rawStackTrace = message._rawStackTrace(); if (rawStackTrace) { return createStackTrace(devTools, rawStackTrace, message._targetId()); } return undefined; } export async function createStackTrace(devTools, rawStackTrace, targetId) { const targetManager = devTools.universe.context.get(DevTools.TargetManager); const target = targetId ? targetManager.targetById(targetId) || devTools.target : devTools.target; const model = target.model(DevTools.DebuggerModel); // DevTools doesn't wait for source maps to attach before building a stack trace, rather it'll send // an update event once a source map was attached and the stack trace retranslated. This doesn't // work in the MCP case, so we'll collect all script IDs upfront and wait for any pending source map // loads before creating the stack trace. We might also have to wait for Debugger.ScriptParsed events if // the stack trace is created particularly early. const scriptIds = new Set(); for (const frame of rawStackTrace.callFrames) { scriptIds.add(frame.scriptId); } for (let asyncStack = rawStackTrace.parent; asyncStack; asyncStack = asyncStack.parent) { for (const frame of asyncStack.callFrames) { scriptIds.add(frame.scriptId); } } const signal = AbortSignal.timeout(1_000); await Promise.all([...scriptIds].map(id => waitForScript(model, id, signal) .then(script => model.sourceMapManager().sourceMapForClientPromise(script)) .catch())); const binding = devTools.universe.context.get(DevTools.DebuggerWorkspaceBinding); // DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe. return binding.createStackTraceFromProtocolRuntime(rawStackTrace, target); } // Waits indefinitely for the script so pair it with Promise.race. async function waitForScript(model, scriptId, signal) { while (true) { if (signal.aborted) { throw signal.reason; } const script = model.scriptForId(scriptId); if (script) { return script; } await new Promise((resolve, reject) => { signal.addEventListener('abort', () => reject(signal.reason), { once: true, }); void model .once('ParsedScriptSource') .then(resolve); }); } } //# sourceMappingURL=DevtoolsUtils.js.map