feat(ext): handcraft the whole ext from scratch
AI coding doesn't work for MV3 extensions. Threading was an unfixable mess. Removed everything and rebuilt by hand.
This commit is contained in:
@@ -1,161 +1,132 @@
|
||||
/**
|
||||
* RemotePageController - Proxy for PageController in ContentScript
|
||||
*
|
||||
* Forwards method calls via RPC to the real PageController in ContentScript.
|
||||
* Mask visibility is managed by content script via storage polling.
|
||||
*/
|
||||
import type {
|
||||
ActionResult,
|
||||
BrowserState,
|
||||
ScrollHorizontallyOptions,
|
||||
ScrollOptions,
|
||||
} from './protocol'
|
||||
import { type RPCClient, createRPCClient } from './rpc'
|
||||
import type { BrowserState, PageController } from '@page-agent/page-controller'
|
||||
|
||||
import { isContentScriptAllowed } from '@/utils'
|
||||
|
||||
import { TabsController } from './TabsController'
|
||||
|
||||
/**
|
||||
* Check if a URL can run content scripts.
|
||||
* Agent side page controller.
|
||||
* - live in the agent env (extension page or content script)
|
||||
* - communicates with remote PageController via sw
|
||||
*/
|
||||
export function isContentScriptAllowed(url: string | undefined): boolean {
|
||||
if (!url) return false
|
||||
|
||||
const restrictedPatterns = [
|
||||
/^chrome:\/\//,
|
||||
/^chrome-extension:\/\//,
|
||||
/^about:/,
|
||||
/^edge:\/\//,
|
||||
/^brave:\/\//,
|
||||
/^opera:\/\//,
|
||||
/^vivaldi:\/\//,
|
||||
/^file:\/\//,
|
||||
/^view-source:/,
|
||||
/^devtools:\/\//,
|
||||
]
|
||||
|
||||
return !restrictedPatterns.some((pattern) => pattern.test(url))
|
||||
}
|
||||
|
||||
export class RemotePageController {
|
||||
private rpc: RPCClient | null = null
|
||||
private _currentTabId: number | null = null
|
||||
private _currentTabUrl: string | undefined = undefined
|
||||
tabsController!: TabsController
|
||||
|
||||
get currentTabId(): number | null {
|
||||
return this._currentTabId
|
||||
}
|
||||
|
||||
get currentTabUrl(): string | undefined {
|
||||
return this._currentTabUrl
|
||||
}
|
||||
|
||||
get isCurrentTabAccessible(): boolean {
|
||||
return isContentScriptAllowed(this._currentTabUrl)
|
||||
}
|
||||
|
||||
async setTargetTab(tabId: number): Promise<void> {
|
||||
const tab = await chrome.tabs.get(tabId)
|
||||
|
||||
this._currentTabId = tabId
|
||||
this._currentTabUrl = tab.url
|
||||
|
||||
if (!isContentScriptAllowed(tab.url)) {
|
||||
this.rpc = null
|
||||
return
|
||||
}
|
||||
|
||||
this.rpc = createRPCClient(tabId)
|
||||
|
||||
// Verify content script is ready
|
||||
try {
|
||||
await this.rpc.getLastUpdateTime()
|
||||
} catch {
|
||||
// Don't clear rpc - subsequent calls will retry
|
||||
}
|
||||
}
|
||||
|
||||
private ensureInitialized(): void {
|
||||
if (!this._currentTabId) {
|
||||
throw new Error('RemotePageController not initialized. Call setTargetTab() first.')
|
||||
}
|
||||
}
|
||||
|
||||
private createRestrictedPageState(): BrowserState {
|
||||
return {
|
||||
url: this._currentTabUrl || '',
|
||||
title: '',
|
||||
header: '',
|
||||
content: '(empty page)',
|
||||
footer: '',
|
||||
}
|
||||
}
|
||||
|
||||
private createRestrictedActionResult(action: string): ActionResult {
|
||||
return {
|
||||
success: false,
|
||||
message: `Cannot ${action} on this page. Use open_new_tab to navigate to a web page first.`,
|
||||
}
|
||||
return this.tabsController.currentTabId
|
||||
}
|
||||
|
||||
async getCurrentUrl(): Promise<string> {
|
||||
return this._currentTabUrl || ''
|
||||
if (!this.currentTabId) return ''
|
||||
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||
return url || ''
|
||||
}
|
||||
|
||||
get currentTabUrl(): Promise<string> {
|
||||
return this.getCurrentUrl()
|
||||
}
|
||||
|
||||
async getCurrentTitle(): Promise<string> {
|
||||
if (!this.currentTabId) return ''
|
||||
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||
return title || ''
|
||||
}
|
||||
|
||||
get currentTabTitle(): Promise<string> {
|
||||
return this.getCurrentTitle()
|
||||
}
|
||||
|
||||
async getLastUpdateTime(): Promise<number> {
|
||||
if (!this.rpc) return Date.now()
|
||||
return this.rpc.getLastUpdateTime()
|
||||
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
||||
|
||||
return await chrome.runtime.sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'get_last_update_time',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
// getBrowserState
|
||||
async getBrowserState(): Promise<BrowserState> {
|
||||
if (!this.rpc) {
|
||||
return this.createRestrictedPageState()
|
||||
let browserState = {} as BrowserState
|
||||
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
browserState = {
|
||||
url: await this.currentTabUrl,
|
||||
title: await this.currentTabTitle,
|
||||
header: '',
|
||||
content: '(empty page)',
|
||||
footer: '',
|
||||
}
|
||||
} else {
|
||||
browserState = await chrome.runtime.sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'get_browser_state',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
return this.rpc.getBrowserState()
|
||||
|
||||
const sum = await this.tabsController.summarizeTabs()
|
||||
browserState.header = sum + '\n' + (browserState.header || '')
|
||||
|
||||
return browserState
|
||||
}
|
||||
|
||||
async updateTree(): Promise<string> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return '(empty page)'
|
||||
return this.rpc.updateTree()
|
||||
// updateTree
|
||||
async updateTree(): Promise<void> {
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'update_tree',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
// cleanUpHighlights
|
||||
async cleanUpHighlights(): Promise<void> {
|
||||
if (!this.rpc) return
|
||||
return this.rpc.cleanUpHighlights()
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
return
|
||||
}
|
||||
|
||||
await chrome.runtime.sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'clean_up_highlights',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
async clickElement(index: number): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('click')
|
||||
return this.rpc.clickElement(index)
|
||||
// clickElement
|
||||
async clickElement(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('click_element', args)
|
||||
}
|
||||
|
||||
async inputText(index: number, text: string): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('input text')
|
||||
return this.rpc.inputText(index, text)
|
||||
// inputText
|
||||
async inputText(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('input_text', args)
|
||||
}
|
||||
|
||||
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('select option')
|
||||
return this.rpc.selectOption(index, optionText)
|
||||
// selectOption
|
||||
async selectOption(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('select_option', args)
|
||||
}
|
||||
|
||||
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('scroll')
|
||||
return this.rpc.scroll(options)
|
||||
// scroll
|
||||
async scroll(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('scroll', args)
|
||||
}
|
||||
|
||||
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('scroll')
|
||||
return this.rpc.scrollHorizontally(options)
|
||||
// scrollHorizontally
|
||||
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('scroll_horizontally', args)
|
||||
}
|
||||
|
||||
async executeJavascript(script: string): Promise<ActionResult> {
|
||||
this.ensureInitialized()
|
||||
if (!this.rpc) return this.createRestrictedActionResult('execute script')
|
||||
return this.rpc.executeJavascript(script)
|
||||
// executeJavascript
|
||||
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('execute_javascript', args)
|
||||
}
|
||||
|
||||
/** @note Mask visibility is managed by content script via storage polling. */
|
||||
@@ -163,9 +134,37 @@ export class RemotePageController {
|
||||
/** @note Mask visibility is managed by content script via storage polling. */
|
||||
async hideMask(): Promise<void> {}
|
||||
|
||||
/** Clear local state. Content script PageControllers clean up via storage polling. */
|
||||
dispose(): void {
|
||||
this._currentTabId = null
|
||||
this.rpc = null
|
||||
// dispose
|
||||
dispose(): void {}
|
||||
|
||||
private async preCheck() {
|
||||
if (!this.currentTabId) {
|
||||
return 'RemotePageController not initialized.'
|
||||
}
|
||||
|
||||
if (!isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
return 'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
|
||||
const preCheckError = await this.preCheck()
|
||||
if (preCheckError) {
|
||||
return { success: false, message: preCheckError }
|
||||
}
|
||||
|
||||
return await chrome.runtime.sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: action,
|
||||
targetTabId: this.currentTabId!,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface DomActionReturn {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user