2026-01-20 17:36:33 +08:00
|
|
|
/**
|
|
|
|
|
* RemotePageController - Proxy for PageController in ContentScript
|
|
|
|
|
*
|
2026-01-26 21:03:51 +08:00
|
|
|
* Forwards method calls via RPC to the real PageController in ContentScript.
|
|
|
|
|
* Mask visibility is managed by content script via storage polling.
|
2026-01-20 17:36:33 +08:00
|
|
|
*/
|
|
|
|
|
import type {
|
|
|
|
|
ActionResult,
|
|
|
|
|
BrowserState,
|
|
|
|
|
ScrollHorizontallyOptions,
|
|
|
|
|
ScrollOptions,
|
2026-01-26 19:33:57 +08:00
|
|
|
} from './protocol'
|
|
|
|
|
import { type RPCClient, createRPCClient } from './rpc'
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
/**
|
|
|
|
|
* Check if a URL can run content scripts.
|
|
|
|
|
*/
|
|
|
|
|
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
|
2026-01-21 18:46:50 +08:00
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
get currentTabId(): number | null {
|
|
|
|
|
return this._currentTabId
|
2026-01-21 18:46:50 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
get currentTabUrl(): string | undefined {
|
|
|
|
|
return this._currentTabUrl
|
2026-01-21 18:46:50 +08:00
|
|
|
}
|
2026-01-20 19:03:15 +08:00
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
get isCurrentTabAccessible(): boolean {
|
|
|
|
|
return isContentScriptAllowed(this._currentTabUrl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setTargetTab(tabId: number): Promise<void> {
|
|
|
|
|
const tab = await chrome.tabs.get(tabId)
|
|
|
|
|
|
|
|
|
|
this._currentTabId = tabId
|
2026-01-26 21:03:51 +08:00
|
|
|
this._currentTabUrl = tab.url
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
if (!isContentScriptAllowed(tab.url)) {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.rpc = null
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.rpc = createRPCClient(tabId)
|
|
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
// Verify content script is ready
|
2026-01-24 19:29:27 +08:00
|
|
|
try {
|
|
|
|
|
await this.rpc.getLastUpdateTime()
|
2026-01-26 21:03:51 +08:00
|
|
|
} catch {
|
|
|
|
|
// Don't clear rpc - subsequent calls will retry
|
2026-01-24 19:29:27 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.`,
|
|
|
|
|
}
|
2026-01-20 19:03:15 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:36:33 +08:00
|
|
|
async getCurrentUrl(): Promise<string> {
|
2026-01-24 19:29:27 +08:00
|
|
|
return this._currentTabUrl || ''
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getLastUpdateTime(): Promise<number> {
|
2026-01-24 19:29:27 +08:00
|
|
|
if (!this.rpc) return Date.now()
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.getLastUpdateTime()
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getBrowserState(): Promise<BrowserState> {
|
2026-01-24 19:29:27 +08:00
|
|
|
if (!this.rpc) {
|
|
|
|
|
return this.createRestrictedPageState()
|
|
|
|
|
}
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.getBrowserState()
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateTree(): Promise<string> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return '(empty page)'
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.updateTree()
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async cleanUpHighlights(): Promise<void> {
|
2026-01-24 19:29:27 +08:00
|
|
|
if (!this.rpc) return
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.cleanUpHighlights()
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async clickElement(index: number): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('click')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.clickElement(index)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async inputText(index: number, text: string): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('input text')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.inputText(index, text)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('select option')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.selectOption(index, optionText)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async scroll(options: ScrollOptions): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('scroll')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.scroll(options)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async scrollHorizontally(options: ScrollHorizontallyOptions): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('scroll')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.scrollHorizontally(options)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async executeJavascript(script: string): Promise<ActionResult> {
|
2026-01-24 19:29:27 +08:00
|
|
|
this.ensureInitialized()
|
|
|
|
|
if (!this.rpc) return this.createRestrictedActionResult('execute script')
|
2026-01-20 19:03:15 +08:00
|
|
|
return this.rpc.executeJavascript(script)
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
/** @note Mask visibility is managed by content script via storage polling. */
|
|
|
|
|
async showMask(): Promise<void> {}
|
|
|
|
|
/** @note Mask visibility is managed by content script via storage polling. */
|
|
|
|
|
async hideMask(): Promise<void> {}
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
/** Clear local state. Content script PageControllers clean up via storage polling. */
|
2026-01-20 17:36:33 +08:00
|
|
|
dispose(): void {
|
2026-01-24 19:29:27 +08:00
|
|
|
this._currentTabId = null
|
|
|
|
|
this.rpc = null
|
|
|
|
|
}
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|