feat(ext): expose extension to main-world and auth with token

This commit is contained in:
Simon
2026-01-28 19:47:15 +08:00
parent ff75aebc20
commit ef2d115742
7 changed files with 192 additions and 9 deletions

View File

@@ -1,5 +1,13 @@
import type { LLMConfig } from '@page-agent/llms'
// Demo LLM for testing // Demo LLM for testing
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM' export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEMO_BASE_URL = export const DEMO_BASE_URL =
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy' 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM' export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEMO_CONFIG: LLMConfig = {
apiKey: DEMO_API_KEY,
baseURL: DEMO_BASE_URL,
model: DEMO_MODEL,
}

View File

@@ -5,9 +5,8 @@ import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/co
import type { LLMConfig } from '@page-agent/llms' import type { LLMConfig } from '@page-agent/llms'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
import { MultiPageAgent } from './MultiPageAgent' import { MultiPageAgent } from './MultiPageAgent'
import { DEMO_CONFIG } from './constants'
export interface UseAgentResult { export interface UseAgentResult {
status: AgentStatus status: AgentStatus
@@ -20,12 +19,6 @@ export interface UseAgentResult {
configure: (config: LLMConfig) => Promise<void> configure: (config: LLMConfig) => Promise<void>
} }
const DEMO_CONFIG: LLMConfig = {
apiKey: DEMO_API_KEY,
baseURL: DEMO_BASE_URL,
model: DEMO_MODEL,
}
export function useAgent(): UseAgentResult { export function useAgent(): UseAgentResult {
const agentRef = useRef<MultiPageAgent | null>(null) const agentRef = useRef<MultiPageAgent | null>(null)
const [status, setStatus] = useState<AgentStatus>('idle') const [status, setStatus] = useState<AgentStatus>('idle')

View File

@@ -19,5 +19,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
export default defineBackground(() => { export default defineBackground(() => {
console.log('[Background] Service Worker started') console.log('[Background] Service Worker started')
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
if (result.PageAgentExtUserAuthToken) return
const userAuthToken = crypto.randomUUID()
chrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })
})
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}) })

View File

@@ -1,4 +1,5 @@
import { initPageController } from '@/agent/RemotePageController.content' import { initPageController } from '@/agent/RemotePageController.content'
import { DEMO_CONFIG } from '@/agent/constants'
const DEBUG_PREFIX = '[Content]' const DEBUG_PREFIX = '[Content]'
@@ -9,5 +10,62 @@ export default defineContentScript({
main() { main() {
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`) console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
initPageController() initPageController()
// if auth token matches, expose agent to page
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
if (!result.PageAgentExtUserAuthToken) return
if (!localStorage.getItem('PageAgentExtUserAuthToken')) return
if (localStorage.getItem('PageAgentExtUserAuthToken') !== result.PageAgentExtUserAuthToken)
return
exposeAgentToPage()
injectScript('/main-world.js')
})
}, },
}) })
async function exposeAgentToPage() {
const { MultiPageAgent } = await import('@/agent/MultiPageAgent')
console.log('MultiPageAgent loaded', MultiPageAgent)
/**
* singleton MultiPageAgent to handle requests from the page
*/
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
window.addEventListener('message', async (e) => {
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
const { action, payload, id } = data
switch (action) {
case 'execute': {
if (!multiPageAgent) multiPageAgent = new MultiPageAgent(DEMO_CONFIG)
const result = await multiPageAgent.execute(payload)
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'execute_result',
payload: result,
},
'*'
)
break
}
case 'dispose': {
// @note stop ongoing processes but can still be re-used later
multiPageAgent?.dispose()
break
}
default:
console.warn(`${DEBUG_PREFIX} Unknown action from page:`, action)
break
}
})
}

View File

@@ -0,0 +1,53 @@
export default defineUnlistedScript(() => {
const w = window as any
let _lastId = 0
function getId() {
_lastId += 1
return _lastId
}
w.execute = async (task: string) => {
const id = getId()
const promise = new Promise((resolve) => {
function handleMessage(e: MessageEvent) {
const data = e.data
if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
if (data.action !== 'execute_result') return
if (data.id !== id) return
window.removeEventListener('message', handleMessage)
resolve(data.payload)
}
window.addEventListener('message', handleMessage)
})
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'execute',
payload: task,
},
'*'
)
return promise
}
w.dispose = () => {
const id = getId()
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'dispose',
},
'*'
)
}
})

View File

@@ -1,5 +1,5 @@
import type { LLMConfig } from '@page-agent/llms' import type { LLMConfig } from '@page-agent/llms'
import { Loader2 } from 'lucide-react' import { Copy, Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants' import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
@@ -17,6 +17,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL) const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL) const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
const [copied, setCopied] = useState(false)
// Update local state when config prop changes // Update local state when config prop changes
useEffect(() => { useEffect(() => {
@@ -25,6 +27,38 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
setModel(config?.model || DEMO_MODEL) setModel(config?.model || DEMO_MODEL)
}, [config]) }, [config])
// Poll for user auth token every second until found
useEffect(() => {
let interval: NodeJS.Timeout | null = null
const fetchToken = async () => {
const result = await chrome.storage.local.get('PageAgentExtUserAuthID')
const token = result.PageAgentExtUserAuthID
if (typeof token === 'string' && token) {
setUserAuthToken(token)
if (interval) {
clearInterval(interval)
interval = null
}
}
}
fetchToken()
interval = setInterval(fetchToken, 1000)
return () => {
if (interval) clearInterval(interval)
}
}, [])
const handleCopyToken = async () => {
if (userAuthToken) {
await navigator.clipboard.writeText(userAuthToken)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const handleSave = async () => { const handleSave = async () => {
setSaving(true) setSaving(true)
try { try {
@@ -38,6 +72,30 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex flex-col gap-4 p-4"> <div className="flex flex-col gap-4 p-4">
<h2 className="text-base font-semibold">Settings</h2> <h2 className="text-base font-semibold">Settings</h2>
{/* User Auth Token Section */}
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
<p className="text-[10px] text-muted-foreground mb-1">
Add this token to a website's localStorage to give it authorization to call this extension
</p>
<div className="flex gap-2 items-center">
<Input
readOnly
value={userAuthToken || 'Loading...'}
className="text-xs h-8 font-mono bg-background"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={handleCopyToken}
disabled={!userAuthToken}
>
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
</Button>
</div>
</div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label> <label className="text-xs text-muted-foreground">Base URL</label>
<Input <Input

View File

@@ -44,6 +44,12 @@ export default defineConfig({
action: { action: {
default_title: 'Open Page Agent', default_title: 'Open Page Agent',
}, },
web_accessible_resources: [
{
resources: ['main-world.js'],
matches: ['*://*/*'],
},
],
side_panel: { side_panel: {
default_path: 'sidepanel/index.html', default_path: 'sidepanel/index.html',
}, },