feat: custom PageController; remove beforeunload; node-safe PageController

This commit is contained in:
Simon
2026-01-18 00:07:23 +08:00
parent 8bba3a63f5
commit ef2777b521
3 changed files with 23 additions and 24 deletions

View File

@@ -68,7 +68,6 @@ export class PageAgent extends EventTarget {
#llm: LLM #llm: LLM
#abortController = new AbortController() #abortController = new AbortController()
#beforeUnloadListener: ((e: Event) => void) | null = null
/** PageController for DOM operations */ /** PageController for DOM operations */
pageController: PageController pageController: PageController
@@ -92,10 +91,12 @@ export class PageAgent extends EventTarget {
this.tools = new Map(tools) this.tools = new Map(tools)
// Initialize PageController with config (mask enabled by default) // Initialize PageController with config (mask enabled by default)
this.pageController = new PageController({ this.pageController =
...this.config, this.config.pageController ??
enableMask: this.config.enableMask ?? true, new PageController({
}) ...this.config,
enableMask: this.config.enableMask ?? true,
})
// Listen to LLM retry events // Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => { this.#llm.addEventListener('retry', (e) => {
@@ -137,11 +138,6 @@ export class PageAgent extends EventTarget {
if (!this.config.experimentalScriptExecutionTool) { if (!this.config.experimentalScriptExecutionTool) {
this.tools.delete('execute_javascript') this.tools.delete('execute_javascript')
} }
this.#beforeUnloadListener = (e) => {
if (!this.disposed) this.dispose('PAGE_UNLOADING')
}
window.addEventListener('beforeunload', this.#beforeUnloadListener)
} }
/** Get current agent status */ /** Get current agent status */
@@ -591,12 +587,6 @@ export class PageAgent extends EventTarget {
this.history = [] this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed') this.#abortController.abort(reason ?? 'PageAgent disposed')
// Clean up window event listeners
if (this.#beforeUnloadListener) {
window.removeEventListener('beforeunload', this.#beforeUnloadListener)
this.#beforeUnloadListener = null
}
// Emit dispose event for UI cleanup // Emit dispose event for UI cleanup
this.dispatchEvent(new Event('dispose')) this.dispatchEvent(new Event('dispose'))

View File

@@ -1,5 +1,5 @@
import type { LLMConfig } from '@page-agent/llms' import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller' import type { PageController, PageControllerConfig } from '@page-agent/page-controller'
import type { PageAgent } from '../PageAgent' import type { PageAgent } from '../PageAgent'
import type { PageAgentTool } from '../tools' import type { PageAgentTool } from '../tools'
@@ -74,7 +74,6 @@ export interface AgentConfig {
/** /**
* @note this hook can block the disposal process * @note this hook can block the disposal process
* @note when dispose caused by page unload, reason will be 'PAGE_UNLOADING'. this method CANNOT block unloading. async operations may be cut.
* @todo remove `this` binding, pass agent as explicit parameter instead * @todo remove `this` binding, pass agent as explicit parameter instead
*/ */
onDispose?: (this: PageAgent, reason?: string) => void onDispose?: (this: PageAgent, reason?: string) => void
@@ -105,6 +104,13 @@ export interface AgentConfig {
*/ */
transformPageContent?: (content: string) => Promise<string> | string transformPageContent?: (content: string) => Promise<string> | string
/**
* @experimental
* Custom PageController instance to control page navigation and actions
* @note If not provided, a default PageController will be created
*/
pageController?: PageController
/** /**
* TODO: @unimplemented * TODO: @unimplemented
* hook when action causes a new page to be opened * hook when action causes a new page to be opened

View File

@@ -18,11 +18,8 @@ import { VIEWPORT_EXPANSION } from './constants'
import * as dom from './dom' import * as dom from './dom'
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type' import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
import { getPageInfo } from './dom/getPageInfo' import { getPageInfo } from './dom/getPageInfo'
import { SimulatorMask } from './mask/SimulatorMask'
import { patchReact } from './patches/react' import { patchReact } from './patches/react'
export { SimulatorMask }
/** /**
* Configuration for PageController * Configuration for PageController
*/ */
@@ -84,7 +81,7 @@ export class PageController extends EventTarget {
private lastTimeUpdate = 0 private lastTimeUpdate = 0
/** Visual mask overlay for blocking user interaction during automation */ /** Visual mask overlay for blocking user interaction during automation */
private mask: SimulatorMask | null = null private mask: InstanceType<typeof import('./mask/SimulatorMask').SimulatorMask> | null = null
constructor(config: PageControllerConfig = {}) { constructor(config: PageControllerConfig = {}) {
super() super()
@@ -94,10 +91,16 @@ export class PageController extends EventTarget {
patchReact(this) patchReact(this)
if (config.enableMask) { if (config.enableMask) {
this.mask = new SimulatorMask() this.initMask()
} }
} }
/**
* Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
*/
private async initMask(): Promise<void> {
const { SimulatorMask } = await import('./mask/SimulatorMask')
this.mask = new SimulatorMask()
}
// ======= State Queries ======= // ======= State Queries =======
/** /**