Compare commits

...

4 Commits

Author SHA1 Message Date
Simon 7470c1987b chore(ext): update extension version 2026-01-29 22:34:15 +08:00
Simon b767f10a85 feat(ext): extending execute api 2026-01-29 22:26:31 +08:00
Simon 8a03391c95 fix(core): change emitActivity to #emitActivity 2026-01-29 21:15:24 +08:00
Simon 5cfaa292d3 feat(ext): add agent heart beat check 2026-01-29 19:21:57 +08:00
11 changed files with 238 additions and 41 deletions
+1 -1
View File
@@ -11333,7 +11333,7 @@
},
"packages/extension": {
"name": "@page-agent/ext",
"version": "0.1.0-b.2",
"version": "0.1.0-b4",
"hasInstallScript": true,
"dependencies": {
"@page-agent/core": "1.0.0",
+7 -7
View File
@@ -95,7 +95,7 @@ export class PageAgentCore extends EventTarget {
// Listen to LLM retry events
this.#llm.addEventListener('retry', (e) => {
const { attempt, maxAttempts } = (e as CustomEvent).detail
this.emitActivity({ type: 'retrying', attempt, maxAttempts })
this.#emitActivity({ type: 'retrying', attempt, maxAttempts })
// Also push to history for panel rendering
this.history.push({
type: 'retry',
@@ -109,7 +109,7 @@ export class PageAgentCore extends EventTarget {
const error = (e as CustomEvent).detail.error as Error | InvokeError
if ((error as any)?.rawError?.name === 'AbortError') return
const message = String(error)
this.emitActivity({ type: 'error', message })
this.#emitActivity({ type: 'error', message })
// Also push to history for panel rendering
this.history.push({
type: 'error',
@@ -153,7 +153,7 @@ export class PageAgentCore extends EventTarget {
* Emit activity event - for transient UI feedback
* @param activity - Current agent activity
*/
emitActivity(activity: AgentActivity): void {
#emitActivity(activity: AgentActivity): void {
this.dispatchEvent(new CustomEvent('activity', { detail: activity }))
}
@@ -224,7 +224,7 @@ export class PageAgentCore extends EventTarget {
// Thinking
console.log(chalk.blue('Thinking...'))
this.emitActivity({ type: 'thinking' })
this.#emitActivity({ type: 'thinking' })
// invoke LLM
@@ -303,7 +303,7 @@ export class PageAgentCore extends EventTarget {
} catch (error: unknown) {
console.error('Task failed', error)
const errorMessage = String(error)
this.emitActivity({ type: 'error', message: errorMessage })
this.#emitActivity({ type: 'error', message: errorMessage })
this.#onDone(false)
const result: ExecutionResult = {
success: false,
@@ -376,7 +376,7 @@ export class PageAgentCore extends EventTarget {
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
// Emit executing activity
this.emitActivity({ type: 'executing', tool: toolName, input: toolInput })
this.#emitActivity({ type: 'executing', tool: toolName, input: toolInput })
const startTime = Date.now()
@@ -387,7 +387,7 @@ export class PageAgentCore extends EventTarget {
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
// Emit executed activity
this.emitActivity({
this.#emitActivity({
type: 'executed',
tool: toolName,
input: toolInput,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "0.1.0-b3",
"version": "0.1.0-b4",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -5,12 +5,26 @@ import { TabsController } from './TabsController'
import SYSTEM_PROMPT from './system_prompt.md?raw'
import { createTabTools } from './tabTools'
/**
* MultiPageAgent
* - use with extension
* - can be used from a side panel or a content script
*/
export class MultiPageAgent extends PageAgentCore {
constructor(config: Omit<PageAgentConfig, 'pageController'>) {
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
const customTools = createTabTools(tabsController)
/**
* When the agent is in side-panel and user closed the side-panel.
* There is no chance for isAgentRunning to be set false.
* (unload event doesn't work well in side panel.)
* (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.)
* This heartbeat mechanism acts as a backup.
*/
let heartBeatInterval: null | number = null
super({
...config,
pageController: pageController as any,
@@ -20,18 +34,34 @@ export class MultiPageAgent extends PageAgentCore {
onBeforeTask: async (agent) => {
await tabsController.init(agent.task)
heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({
agentHeartbeat: Date.now(),
})
}, 1_000)
await chrome.storage.local.set({
isAgentRunning: true,
})
},
onAfterTask: async () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
await chrome.storage.local.set({
isAgentRunning: false,
})
},
onDispose: () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
heartBeatInterval = null
}
chrome.storage.local.set({
isAgentRunning: false,
})
@@ -21,17 +21,14 @@ export function initPageController() {
}
intervalID = window.setInterval(async () => {
const agentHeartbeat = (await chrome.storage.local.get('agentHeartbeat')).agentHeartbeat
const now = Date.now()
const agentInTouch = typeof agentHeartbeat === 'number' && now - agentHeartbeat < 2_000
const isAgentRunning = (await chrome.storage.local.get('isAgentRunning')).isAgentRunning
const currentTabId = (await chrome.storage.local.get('currentTabId')).currentTabId
const shouldShowMask = isAgentRunning && currentTabId === (await myTabIdPromise)
// console.log('[RemotePageController] polling:', {
// isAgentRunning,
// currentTabId,
// myTabId: await myTabIdPromise,
// shouldShowMask,
// })
const shouldShowMask = isAgentRunning && agentInTouch && currentTabId === (await myTabIdPromise)
if (shouldShowMask) {
const pc = getPC()
@@ -45,13 +42,13 @@ export function initPageController() {
}
}
if (!isAgentRunning) {
if (!isAgentRunning && agentInTouch) {
if (pageController) {
pageController.dispose()
pageController = null
}
}
}, 1_000)
}, 500)
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type !== 'PAGE_CONTROL') {
@@ -1,7 +1,7 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage } from '@/agent/TabsController.background'
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type === 'TAB_CONTROL') {
return handleTabControlMessage(message, sender, sendResponse)
} else if (message.type === 'PAGE_CONTROL') {
@@ -54,6 +54,7 @@ async function exposeAgentToPage() {
switch (action) {
case 'execute': {
// singleton check
if (multiPageAgent && multiPageAgent.status === 'running') {
window.postMessage(
{
@@ -70,8 +71,64 @@ async function exposeAgentToPage() {
try {
const { task, llmConfig } = payload
// create when used
multiPageAgent = new MultiPageAgent(llmConfig)
// events
multiPageAgent.addEventListener('statuschange', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'status_change_event',
payload: multiPageAgent.status,
},
'*'
)
})
multiPageAgent.addEventListener('activity', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'activity_event',
payload: (event as CustomEvent).detail,
},
'*'
)
})
multiPageAgent.addEventListener('historychange', (event) => {
if (!multiPageAgent) return
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'history_change_event',
payload: multiPageAgent.history,
},
'*'
)
})
multiPageAgent.addEventListener('dispose', () => {
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'dispose_event',
},
'*'
)
})
// result
const result = await multiPageAgent.execute(task)
window.postMessage(
@@ -1,5 +1,19 @@
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
import type { LLMConfig } from '@page-agent/llms'
export interface ExecuteHooks {
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
onDispose?: () => void
}
export type Execute = (
task: string,
llmConfig: LLMConfig,
hooks?: ExecuteHooks
) => Promise<ExecutionResult>
export default defineUnlistedScript(() => {
const w = window as any
@@ -9,7 +23,7 @@ export default defineUnlistedScript(() => {
return _lastId
}
w.execute = async (task: string, llmConfig: LLMConfig) => {
w.execute = async (task: string, llmConfig: LLMConfig, hooks?: ExecuteHooks) => {
if (typeof task !== 'string') throw new Error('Task must be a string')
if (task.trim().length === 0) throw new Error('Task cannot be empty')
if (!llmConfig) throw new Error('LLM config is required')
@@ -19,14 +33,39 @@ export default defineUnlistedScript(() => {
const id = getId()
const promise = new Promise((resolve, reject) => {
const promise = new Promise<ExecutionResult>((resolve, reject) => {
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
// events
if (data.action === 'status_change_event' && hooks?.onStatusChange) {
hooks.onStatusChange(data.payload)
return
}
if (data.action === 'activity_event' && hooks?.onActivity) {
hooks.onActivity(data.payload)
return
}
if (data.action === 'history_change_event' && hooks?.onHistoryUpdate) {
hooks.onHistoryUpdate(data.payload)
return
}
if (data.action === 'dispose_event' && hooks?.onDispose) {
hooks.onDispose()
return
}
// result
if (data.action !== 'execute_result') return
window.removeEventListener('message', handleMessage)
if (data.error) {
+1 -1
View File
@@ -36,7 +36,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
{ title: t('nav.custom_tools'), path: '/features/custom-tools' },
{ title: t('nav.knowledge_injection'), path: '/features/custom-instructions' },
{ title: t('nav.data_masking'), path: '/features/data-masking' },
{ title: '🧪 ' + t('nav.chrome_extension'), path: '/features/chrome-extension' },
{ title: t('nav.chrome_extension'), path: '/features/chrome-extension' },
],
},
{
@@ -385,13 +385,6 @@ const result = await agent.execute('Fill in the form with test data')`}
? '向历史流推送一个观察事件,会在下一步时被 LLM 看到'
: 'Push an observation to history stream, will be seen by LLM in next step',
},
{
name: 'emitActivity(activity: AgentActivity)',
type: 'void',
description: isZh
? '发出活动事件用于 UI 反馈'
: 'Emit activity event for UI feedback',
},
{
name: 'dispose(reason?: string)',
type: 'void',
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
@@ -23,11 +24,7 @@ export default function ChromeExtension() {
{/* Hero Section */}
<section className="p-6 bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl">
<div className="flex items-start gap-4">
<span className="text-4xl">🌐</span>
<div>
<h2 className="text-2xl font-bold mb-2">
{isZh ? '轻量级 AI 浏览器' : 'Lightweight AI Browser'}
</h2>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '解锁多页任务!借助 Chrome 扩展,Agent 可以跨标签页和页面导航,突破单页限制。'
@@ -77,7 +74,7 @@ export default function ChromeExtension() {
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
<path d={siGithub.path} />
</svg>
{isZh ? '前往 GitHub Releases 下载' : 'Download from GitHub Releases'}
</a>
@@ -126,34 +123,46 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
<section>
<h2 className="text-2xl font-bold mb-4">{isZh ? 'API 参考' : 'API Reference'}</h2>
<h3 className="text-xl font-semibold mb-3">window.execute(task, llmConfig)</h3>
<h3 className="text-xl font-semibold mb-3">window.execute(task, llmConfig, hooks?)</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '使用 LLM 配置执行任务。返回一个 Promise,在任务完成时 resolve。'
: 'Execute a task with LLM configuration. Returns a Promise that resolves when the task completes.'}
? '使用 LLM 配置执行任务。返回一个 Promise,在任务完成时 resolve。可选的 hooks 参数用于监听任务执行过程中的事件。'
: 'Execute a task with LLM configuration. Returns a Promise that resolves when the task completes. Optional hooks parameter for listening to events during task execution.'}
</p>
<CodeEditor
code={
isZh
? `// 使用 LLM 配置执行任务
? `// 使用 LLM 配置和 hooks 执行任务
const result = await window.execute(
'在 GitHub 上搜索 "page-agent" 并打开第一个结果',
{
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5-2'
},
{
onStatusChange: status => console.log('状态变化:', status),
onActivity: activity => console.log('活动:', activity),
onHistoryUpdate: history => console.log('历史更新:', history),
onDispose: () => console.log('已停止')
}
)
console.log(result) // 任务执行结果`
: `// Execute a task with LLM configuration
: `// Execute a task with LLM configuration and hooks
const result = await window.execute(
'Search for "page-agent" on GitHub and open the first result',
{
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5-2'
},
{
onStatusChange: status => console.log('Status change:', status),
onActivity: activity => console.log('Activity:', activity),
onHistoryUpdate: history => console.log('History update:', history),
onDispose: () => console.log('Disposed')
}
)
@@ -203,6 +212,49 @@ window.dispose()`
/>
</section>
{/* Execute Hooks */}
<section>
<h2 className="text-2xl font-bold mb-4">{isZh ? 'Execute Hooks' : 'Execute Hooks'}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '通过 hooks 参数,你可以监听任务执行过程中的各种事件,实现实时更新 UI、日志记录等功能。'
: 'With hooks parameter, you can listen to various events during task execution for real-time UI updates, logging, and more.'}
</p>
<CodeEditor
code={
isZh
? `interface ExecuteHooks {
// Agent 状态变化时调用(idle, running, error, completed 等)
onStatusChange?: (status: AgentStatus) => void
// Agent 执行活动时调用(如点击、输入、导航等操作)
onActivity?: (activity: AgentActivity) => void
// 历史记录更新时调用(包含完整的事件历史)
onHistoryUpdate?: (history: HistoricalEvent[]) => void
// Agent 被停止时调用
onDispose?: () => void
}`
: `interface ExecuteHooks {
// Called when agent status changes (idle, running, error, completed, etc.)
onStatusChange?: (status: AgentStatus) => void
// Called when agent performs an activity (click, input, navigation, etc.)
onActivity?: (activity: AgentActivity) => void
// Called when history is updated (contains full event history)
onHistoryUpdate?: (history: HistoricalEvent[]) => void
// Called when agent is disposed
onDispose?: () => void
}`
}
language="typescript"
/>
</section>
{/* Security Notice */}
<section className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
@@ -223,6 +275,35 @@ window.dispose()`
</li>
</ul>
</section>
{/* Integration Guide */}
<section>
<h2 className="text-2xl font-bold mb-4">
{isZh
? '将 MultiPageAgent 融入你自己的插件'
: 'Integrate MultiPageAgent into Your Extension'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '你可以将 MultiPageAgent 集成到自己的浏览器扩展中,实现跨页面的 AI 自动化能力。'
: 'You can integrate MultiPageAgent into your own browser extension for cross-page AI automation capabilities.'}
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">TODO</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh ? '参考源码实现:' : 'Reference implementation:'}
</p>
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
</svg>
packages/extension/src/entrypoints/background.ts
</a>
</section>
</div>
</div>
)