2026-01-20 17:36:33 +08:00
|
|
|
/**
|
|
|
|
|
* Content Script Entry Point
|
|
|
|
|
*
|
|
|
|
|
* This script runs in the context of web pages and hosts the real PageController.
|
2026-01-24 19:29:27 +08:00
|
|
|
* It listens for RPC messages relayed through the Background Script and
|
|
|
|
|
* dispatches them to PageController.
|
2026-01-20 19:08:52 +08:00
|
|
|
*
|
2026-01-24 19:29:27 +08:00
|
|
|
* Message flow:
|
|
|
|
|
* - RPC: SidePanel → SW → ContentScript (this file) → response → SW → SidePanel
|
|
|
|
|
* - Query: ContentScript → SW → SidePanel → SW → ContentScript (for shouldShowMask)
|
2026-01-20 17:36:33 +08:00
|
|
|
*/
|
|
|
|
|
import { PageController } from '@page-agent/page-controller'
|
|
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
import type {
|
|
|
|
|
CSQueryMessage,
|
|
|
|
|
CSRPCMessage,
|
|
|
|
|
QueryResponseMessage,
|
|
|
|
|
RPCMethod,
|
|
|
|
|
} from '../messaging/protocol'
|
|
|
|
|
import { generateMessageId, isExtensionMessage } from '../messaging/protocol'
|
|
|
|
|
|
|
|
|
|
const DEBUG_PREFIX = '[ContentScript]'
|
2026-01-20 17:36:33 +08:00
|
|
|
|
|
|
|
|
export default defineContentScript({
|
|
|
|
|
matches: ['<all_urls>'],
|
|
|
|
|
runAt: 'document_idle',
|
|
|
|
|
|
2026-01-21 18:46:50 +08:00
|
|
|
async main() {
|
2026-01-24 19:29:27 +08:00
|
|
|
const pageUrl = window.location.href
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} Content script loaded on ${pageUrl}`)
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-20 19:08:52 +08:00
|
|
|
// Lazy-initialized controller - created on demand, disposed between tasks
|
|
|
|
|
let controller: PageController | null = null
|
2026-01-21 19:17:54 +08:00
|
|
|
let initError: Error | null = null
|
2026-01-20 19:08:52 +08:00
|
|
|
|
|
|
|
|
function getController(): PageController {
|
2026-01-21 19:17:54 +08:00
|
|
|
if (initError) {
|
2026-01-24 19:29:27 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} getController: re-throwing init error`)
|
2026-01-21 19:17:54 +08:00
|
|
|
throw initError
|
|
|
|
|
}
|
2026-01-20 19:08:52 +08:00
|
|
|
if (!controller) {
|
2026-01-21 19:17:54 +08:00
|
|
|
try {
|
|
|
|
|
controller = new PageController({ enableMask: true })
|
2026-01-24 19:29:27 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} PageController created`)
|
2026-01-21 19:17:54 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
initError = error instanceof Error ? error : new Error(String(error))
|
2026-01-24 19:29:27 +08:00
|
|
|
console.error(`${DEBUG_PREFIX} Failed to create PageController:`, initError)
|
2026-01-21 19:17:54 +08:00
|
|
|
throw initError
|
|
|
|
|
}
|
2026-01-20 19:08:52 +08:00
|
|
|
}
|
|
|
|
|
return controller
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
function disposeController(): void {
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} Disposing controller...`)
|
|
|
|
|
controller?.dispose()
|
|
|
|
|
controller = null
|
|
|
|
|
initError = null
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} PageController disposed`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register RPC message handler
|
|
|
|
|
registerRPCHandler(getController, () => controller, disposeController)
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-21 18:46:50 +08:00
|
|
|
// Check if there's an active task that needs mask to be shown
|
2026-01-24 19:29:27 +08:00
|
|
|
setTimeout(() => queryShouldShowMask(getController), 100)
|
2026-01-21 18:46:50 +08:00
|
|
|
|
2026-01-20 17:36:33 +08:00
|
|
|
// Cleanup on page unload
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
2026-01-24 19:29:27 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} Page unloading, disposing controller`)
|
2026-01-20 19:08:52 +08:00
|
|
|
controller?.dispose()
|
|
|
|
|
controller = null
|
2026-01-20 17:36:33 +08:00
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-21 19:17:54 +08:00
|
|
|
/**
|
2026-01-24 19:29:27 +08:00
|
|
|
* Query the sidepanel (via SW) whether mask should be shown
|
2026-01-21 19:17:54 +08:00
|
|
|
*/
|
2026-01-24 19:29:27 +08:00
|
|
|
async function queryShouldShowMask(getController: () => PageController): Promise<void> {
|
|
|
|
|
const tabId = await getCurrentTabId()
|
|
|
|
|
if (!tabId) {
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} Cannot query shouldShowMask: no tab ID`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const queryId = generateMessageId()
|
|
|
|
|
const queryMessage: CSQueryMessage = {
|
|
|
|
|
type: 'cs:query',
|
|
|
|
|
id: queryId,
|
|
|
|
|
queryType: 'shouldShowMask',
|
|
|
|
|
tabId,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 16:16:04 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} shouldShowMask query:`, {
|
|
|
|
|
tabId,
|
|
|
|
|
url: window.location.href,
|
|
|
|
|
queryId,
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-24 19:29:27 +08:00
|
|
|
try {
|
|
|
|
|
// Set up response listener
|
|
|
|
|
const responsePromise = new Promise<boolean>((resolve) => {
|
|
|
|
|
const timeout = setTimeout(() => {
|
2026-01-26 16:16:04 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} shouldShowMask query timeout (3s)`)
|
2026-01-24 19:29:27 +08:00
|
|
|
chrome.runtime.onMessage.removeListener(listener)
|
|
|
|
|
resolve(false)
|
|
|
|
|
}, 3000)
|
|
|
|
|
|
|
|
|
|
const listener = (message: unknown) => {
|
|
|
|
|
if (!isExtensionMessage(message)) return
|
|
|
|
|
if (message.type !== 'query:response') return
|
|
|
|
|
if ((message as QueryResponseMessage).id !== queryId) return
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeout)
|
|
|
|
|
chrome.runtime.onMessage.removeListener(listener)
|
|
|
|
|
resolve((message as QueryResponseMessage).result as boolean)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chrome.runtime.onMessage.addListener(listener)
|
2026-01-21 19:17:54 +08:00
|
|
|
})
|
2026-01-24 19:29:27 +08:00
|
|
|
|
|
|
|
|
// Send query
|
|
|
|
|
await chrome.runtime.sendMessage(queryMessage)
|
|
|
|
|
|
|
|
|
|
// Wait for response
|
|
|
|
|
const shouldShowMask = await responsePromise
|
2026-01-26 16:16:04 +08:00
|
|
|
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} shouldShowMask response:`, {
|
|
|
|
|
tabId,
|
|
|
|
|
shouldShowMask,
|
|
|
|
|
action: shouldShowMask ? 'showMask' : 'noAction',
|
|
|
|
|
})
|
2026-01-24 19:29:27 +08:00
|
|
|
|
|
|
|
|
if (shouldShowMask) {
|
|
|
|
|
await getController().showMask()
|
2026-01-26 16:16:04 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} Mask shown after page load`)
|
2026-01-24 19:29:27 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} shouldShowMask query failed:`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get current tab ID
|
|
|
|
|
*/
|
|
|
|
|
async function getCurrentTabId(): Promise<number | null> {
|
|
|
|
|
try {
|
|
|
|
|
const response = await chrome.runtime.sendMessage({ type: 'getTabId' })
|
|
|
|
|
return response?.tabId ?? null
|
|
|
|
|
} catch {
|
|
|
|
|
// Fallback: we're in content script, tab ID comes from sender in SW
|
|
|
|
|
return null
|
|
|
|
|
}
|
2026-01-21 19:17:54 +08:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:36:33 +08:00
|
|
|
/**
|
2026-01-24 19:29:27 +08:00
|
|
|
* Register RPC message handler
|
2026-01-20 17:36:33 +08:00
|
|
|
*/
|
2026-01-24 19:29:27 +08:00
|
|
|
function registerRPCHandler(
|
2026-01-20 19:08:52 +08:00
|
|
|
getController: () => PageController,
|
|
|
|
|
getControllerIfExists: () => PageController | null,
|
|
|
|
|
disposeController: () => void
|
|
|
|
|
): void {
|
2026-01-24 19:29:27 +08:00
|
|
|
chrome.runtime.onMessage.addListener(
|
|
|
|
|
(
|
|
|
|
|
message: unknown,
|
|
|
|
|
_sender: chrome.runtime.MessageSender,
|
|
|
|
|
sendResponse: (response?: unknown) => void
|
|
|
|
|
): boolean => {
|
|
|
|
|
if (!isExtensionMessage(message)) return false
|
|
|
|
|
if (message.type !== 'cs:rpc') return false
|
|
|
|
|
|
|
|
|
|
const rpcMessage = message as CSRPCMessage
|
|
|
|
|
const { method, args } = rpcMessage
|
|
|
|
|
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} RPC: ${method}`, args)
|
|
|
|
|
|
|
|
|
|
// Handle the RPC call
|
|
|
|
|
handleRPCCall(method, args, getController, getControllerIfExists, disposeController)
|
|
|
|
|
.then((result) => {
|
|
|
|
|
sendResponse(result)
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(`${DEBUG_PREFIX} RPC ${method} failed:`, error)
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Return true to indicate async response
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} RPC handler registered`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle an RPC call
|
|
|
|
|
*/
|
|
|
|
|
async function handleRPCCall(
|
|
|
|
|
method: RPCMethod,
|
|
|
|
|
args: unknown[],
|
|
|
|
|
getController: () => PageController,
|
|
|
|
|
getControllerIfExists: () => PageController | null,
|
|
|
|
|
disposeController: () => void
|
|
|
|
|
): Promise<unknown> {
|
|
|
|
|
switch (method) {
|
|
|
|
|
// State queries
|
|
|
|
|
case 'getCurrentUrl':
|
|
|
|
|
return getController().getCurrentUrl()
|
|
|
|
|
|
|
|
|
|
case 'getLastUpdateTime':
|
|
|
|
|
return getController().getLastUpdateTime()
|
|
|
|
|
|
|
|
|
|
case 'getBrowserState':
|
|
|
|
|
return getController().getBrowserState()
|
|
|
|
|
|
|
|
|
|
// DOM operations
|
|
|
|
|
case 'updateTree':
|
|
|
|
|
return getController().updateTree()
|
|
|
|
|
|
|
|
|
|
case 'cleanUpHighlights':
|
|
|
|
|
await getControllerIfExists()?.cleanUpHighlights()
|
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
|
|
// Element actions
|
|
|
|
|
case 'clickElement':
|
|
|
|
|
return getController().clickElement(args[0] as number)
|
|
|
|
|
|
|
|
|
|
case 'inputText':
|
|
|
|
|
return getController().inputText(args[0] as number, args[1] as string)
|
|
|
|
|
|
|
|
|
|
case 'selectOption':
|
|
|
|
|
return getController().selectOption(args[0] as number, args[1] as string)
|
|
|
|
|
|
|
|
|
|
case 'scroll':
|
|
|
|
|
return getController().scroll(args[0] as Parameters<PageController['scroll']>[0])
|
|
|
|
|
|
|
|
|
|
case 'scrollHorizontally':
|
|
|
|
|
return getController().scrollHorizontally(
|
|
|
|
|
args[0] as Parameters<PageController['scrollHorizontally']>[0]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case 'executeJavascript':
|
|
|
|
|
return getController().executeJavascript(args[0] as string)
|
|
|
|
|
|
|
|
|
|
// Mask operations
|
|
|
|
|
case 'showMask':
|
|
|
|
|
await getController().showMask()
|
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
|
|
case 'hideMask':
|
|
|
|
|
await getControllerIfExists()?.hideMask()
|
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
|
|
// Lifecycle
|
|
|
|
|
case 'dispose':
|
|
|
|
|
disposeController()
|
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unknown RPC method: ${method}`)
|
|
|
|
|
}
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|