2026-01-20 17:36:33 +08:00
|
|
|
/**
|
|
|
|
|
* Content Script Entry Point
|
|
|
|
|
*
|
2026-01-26 21:03:51 +08:00
|
|
|
* Runs in web page context, hosts PageController.
|
|
|
|
|
* - Receives AGENT_TO_PAGE messages and responds via sendResponse
|
|
|
|
|
* - Polls chrome.storage to manage mask visibility (no outgoing messages)
|
2026-01-20 17:36:33 +08:00
|
|
|
*/
|
|
|
|
|
import { PageController } from '@page-agent/page-controller'
|
|
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
import type { AgentState, AgentToPageMessage } from '../agent/protocol'
|
|
|
|
|
import { isExtensionMessage } from '../agent/protocol'
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
const DEBUG_PREFIX = '[Content]'
|
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-26 21:03:51 +08:00
|
|
|
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
// Lazy-initialized controller
|
2026-01-20 19:08:52 +08:00
|
|
|
let controller: PageController | null = null
|
2026-01-21 19:17:54 +08:00
|
|
|
let initError: Error | null = null
|
2026-01-26 21:03:51 +08:00
|
|
|
let myTabId: number | null = null
|
2026-01-20 19:08:52 +08:00
|
|
|
|
|
|
|
|
function getController(): PageController {
|
2026-01-26 21:03:51 +08:00
|
|
|
if (initError) 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))
|
|
|
|
|
throw initError
|
|
|
|
|
}
|
2026-01-20 19:08:52 +08:00
|
|
|
}
|
|
|
|
|
return controller
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
// Register message handler
|
|
|
|
|
chrome.runtime.onMessage.addListener(
|
|
|
|
|
(
|
|
|
|
|
message: unknown,
|
|
|
|
|
_sender: chrome.runtime.MessageSender,
|
|
|
|
|
sendResponse: (response?: unknown) => void
|
|
|
|
|
): boolean => {
|
|
|
|
|
if (!isExtensionMessage(message)) return false
|
|
|
|
|
if (message.type !== 'AGENT_TO_PAGE') return false
|
|
|
|
|
|
|
|
|
|
const msg = message as AgentToPageMessage
|
|
|
|
|
|
|
|
|
|
// Cache our tab ID from the first message
|
|
|
|
|
if (myTabId === null) {
|
|
|
|
|
myTabId = msg.tabId
|
|
|
|
|
console.debug(`${DEBUG_PREFIX} Tab ID: ${myTabId}`)
|
|
|
|
|
}
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
handleRPC(msg.method, msg.args, getController, () => controller)
|
|
|
|
|
.then(sendResponse)
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(`${DEBUG_PREFIX} RPC ${msg.method} failed:`, error)
|
|
|
|
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
|
|
|
|
})
|
2026-01-20 17:36:33 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
return true // Async response
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Start mask polling
|
|
|
|
|
startMaskPolling(
|
|
|
|
|
() => myTabId,
|
|
|
|
|
getController,
|
|
|
|
|
() => controller
|
|
|
|
|
)
|
2026-01-21 18:46:50 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
// Cleanup on unload
|
2026-01-20 17:36:33 +08:00
|
|
|
window.addEventListener('beforeunload', () => {
|
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-26 21:03:51 +08:00
|
|
|
* Poll storage every second to manage mask visibility.
|
|
|
|
|
* Content script is autonomous - decides mask state based on:
|
|
|
|
|
* - agentState in storage (tabId, running)
|
|
|
|
|
* - document.visibilityState
|
2026-01-20 17:36:33 +08:00
|
|
|
*/
|
2026-01-26 21:03:51 +08:00
|
|
|
function startMaskPolling(
|
|
|
|
|
getTabId: () => number | null,
|
2026-01-20 19:08:52 +08:00
|
|
|
getController: () => PageController,
|
2026-01-26 21:03:51 +08:00
|
|
|
getControllerIfExists: () => PageController | null
|
2026-01-20 19:08:52 +08:00
|
|
|
): void {
|
2026-01-26 21:03:51 +08:00
|
|
|
let maskVisible = false
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
const poll = async () => {
|
|
|
|
|
const tabId = getTabId()
|
|
|
|
|
if (tabId === null) return // Don't know our tab ID yet
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
try {
|
|
|
|
|
const { agentState } = (await chrome.storage.local.get('agentState')) as {
|
|
|
|
|
agentState?: AgentState
|
|
|
|
|
}
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
const shouldShow =
|
|
|
|
|
agentState?.running === true &&
|
|
|
|
|
agentState?.tabId === tabId &&
|
|
|
|
|
document.visibilityState === 'visible'
|
|
|
|
|
|
|
|
|
|
if (shouldShow && !maskVisible) {
|
|
|
|
|
await getController().showMask()
|
|
|
|
|
maskVisible = true
|
|
|
|
|
} else if (!shouldShow && maskVisible) {
|
|
|
|
|
await getControllerIfExists()?.hideMask()
|
|
|
|
|
maskVisible = false
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Storage access failed, ignore
|
2026-01-24 19:29:27 +08:00
|
|
|
}
|
2026-01-26 21:03:51 +08:00
|
|
|
}
|
2026-01-24 19:29:27 +08:00
|
|
|
|
2026-01-26 21:03:51 +08:00
|
|
|
setInterval(poll, 1000)
|
|
|
|
|
// Also poll on visibility change for faster response
|
|
|
|
|
document.addEventListener('visibilitychange', poll)
|
2026-01-24 19:29:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-26 21:03:51 +08:00
|
|
|
* Handle RPC method call
|
2026-01-24 19:29:27 +08:00
|
|
|
*/
|
2026-01-26 21:03:51 +08:00
|
|
|
async function handleRPC(
|
2026-01-26 18:55:54 +08:00
|
|
|
method: string,
|
2026-01-24 19:29:27 +08:00
|
|
|
args: unknown[],
|
|
|
|
|
getController: () => PageController,
|
2026-01-26 21:03:51 +08:00
|
|
|
getControllerIfExists: () => PageController | null
|
2026-01-24 19:29:27 +08:00
|
|
|
): Promise<unknown> {
|
|
|
|
|
switch (method) {
|
|
|
|
|
case 'getCurrentUrl':
|
|
|
|
|
return getController().getCurrentUrl()
|
|
|
|
|
|
|
|
|
|
case 'getLastUpdateTime':
|
|
|
|
|
return getController().getLastUpdateTime()
|
|
|
|
|
|
|
|
|
|
case 'getBrowserState':
|
|
|
|
|
return getController().getBrowserState()
|
|
|
|
|
|
|
|
|
|
case 'updateTree':
|
|
|
|
|
return getController().updateTree()
|
|
|
|
|
|
|
|
|
|
case 'cleanUpHighlights':
|
|
|
|
|
await getControllerIfExists()?.cleanUpHighlights()
|
|
|
|
|
return undefined
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unknown RPC method: ${method}`)
|
|
|
|
|
}
|
2026-01-20 17:36:33 +08:00
|
|
|
}
|