feat(ext): expose extension to main-world and auth with token
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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(() => {})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
53
packages/extension/src/entrypoints/main-world.ts
Normal file
53
packages/extension/src/entrypoints/main-world.ts
Normal 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',
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user