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:
Simon
2026-01-27 17:21:32 +08:00
parent 8efa8e18c1
commit fdc3cf4e6d
18 changed files with 797 additions and 1749 deletions

View File

@@ -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
}