Compare commits
33 Commits
v1.1.1
..
EXT_v0.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 6abe75f199 | |||
| e27ebda960 | |||
| 9b603d90ba | |||
| b89228693d | |||
| 66cc9e0a3c | |||
| 53144a8021 | |||
| 43b7c1b136 | |||
| 3e7851849f | |||
| dffcb53db9 | |||
| 4dc332a32c | |||
| f19b3cc2cc | |||
| 11d66f42c4 | |||
| 20e947a1c1 | |||
| 7c87c90258 | |||
| fcb9ec4e57 | |||
| 1fcf931685 | |||
| c71ffcec1c | |||
| 430be466fc | |||
| 3f284cf2e6 | |||
| 6f0655478a | |||
| 7d5449967b | |||
| fd47888d1b | |||
| 10f470ce7d | |||
| cb29db1cc4 | |||
| 8c08f639dd | |||
| 488f080e68 | |||
| 6e07af422b | |||
| e9d721f6b9 | |||
| 840162df8c | |||
| 1517bc4868 | |||
| 67f6a56a87 | |||
| 6f0ff1fd33 | |||
| 6fdb8d03f2 |
Vendored
+3
-2
@@ -18,7 +18,7 @@
|
||||
"wouter"
|
||||
],
|
||||
"files.exclude": {
|
||||
"packages/*/node_modules": true,
|
||||
"packages/*/node_modules": true
|
||||
},
|
||||
"markdownlint.config": {
|
||||
// "comment": "Relaxed rules",
|
||||
@@ -32,6 +32,7 @@
|
||||
"first-line-h1": false,
|
||||
"block-spacing": false,
|
||||
"blanks-around-lists": false,
|
||||
"ol-prefix": false
|
||||
"ol-prefix": false,
|
||||
"no-duplicate-heading": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ npm start # Start website dev server
|
||||
npm run build # Build all packages
|
||||
npm run build:libs # Build all libraries
|
||||
npm run lint # ESLint with TypeScript strict rules
|
||||
npm run zip -w @page-agent/ext # Zip the extension package
|
||||
```
|
||||
|
||||
## Architecture
|
||||
@@ -36,7 +37,7 @@ packages/
|
||||
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
||||
├── website/ # @page-agent/website (private)
|
||||
├── llms/ # @page-agent/llms
|
||||
├── extension/ # 🚧 WIP: Browser extension (WXT + React)
|
||||
├── extension/ # Browser extension (WXT + React)
|
||||
├── page-controller/ # @page-agent/page-controller
|
||||
└── ui/ # @page-agent/ui
|
||||
```
|
||||
|
||||
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.3.0] - 2026-02-13
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Lifecycle: `stop()` vs `dispose()`** - New `stop()` method to cancel the current task while keeping the agent reusable. `dispose()` is now terminal — a disposed agent cannot be reused. This affects both `PageAgentCore` and `PanelAgentAdapter`.
|
||||
|
||||
### Features
|
||||
|
||||
- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status
|
||||
- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean "Task stopped" message instead of a raw error stack
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-02-11
|
||||
|
||||
### Features
|
||||
|
||||
- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages
|
||||
- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation
|
||||
|
||||
### Improvements
|
||||
|
||||
- Pruned system prompts for lower token usage and faster responses
|
||||
- Improved error handling during agent steps with better error messages
|
||||
- Zod tree-shaking for smaller bundle size
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed indentation lost in DOM extraction caused by `trimLines`
|
||||
- Fixed `gpt-5-mini` temperature configuration
|
||||
|
||||
---
|
||||
|
||||
## [1.1.0] - 2026-02-02
|
||||
|
||||
### Features
|
||||
|
||||
- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt
|
||||
- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management
|
||||
|
||||
### Improvements
|
||||
|
||||
- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)
|
||||
- Lazy-loaded mask module for faster initialization
|
||||
- Better date formatting and error messages from LLM client
|
||||
- Added `rawRequest` to step history for easier debugging
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles
|
||||
- Fixed `AbortError` being incorrectly retried and shown to users
|
||||
- Fixed mask not working correctly when starting a new task after stopping a previous one
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] - 2026-01-19
|
||||
|
||||
### 🎉 First Stable Release
|
||||
|
||||
+12
-9
@@ -20,10 +20,11 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
|
||||
|
||||
### Project Structure
|
||||
|
||||
This is a **monorepo** with npm workspaces containing **3 main packages**:
|
||||
This is a **monorepo** with npm workspaces containing **4 main packages**:
|
||||
|
||||
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
||||
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
||||
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
|
||||
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||
|
||||
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
||||
@@ -145,6 +146,16 @@ If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add
|
||||
npm start
|
||||
```
|
||||
|
||||
### Extension Development
|
||||
|
||||
```bash
|
||||
npm run dev -w @page-agent/ext
|
||||
npm run zip -w @page-agent/ext
|
||||
```
|
||||
|
||||
- Load extension in Chrome via `chrome://extensions` -> **Load unpacked**
|
||||
- Use `packages/extension/docs/extension_api.md` (EN) or `packages/extension/docs/extension_api_zh.md` (ZH) for API integration details
|
||||
|
||||
### Testing on Other Websites
|
||||
|
||||
- Start and serve a local `iife` script
|
||||
@@ -193,14 +204,6 @@ By contributing to this project, you agree that your contributions will be licen
|
||||
|
||||
> You may need to sign a github CLA before you create a PR.
|
||||
|
||||
### Browser-Use Attribution
|
||||
|
||||
Parts of this project are derived from the [browser-use](https://github.com/browser-use/browser-use) project (MIT License). When contributing to DOM-related functionality:
|
||||
|
||||
- Maintain existing attribution comments
|
||||
- Follow similar patterns for consistency
|
||||
- Credit browser-use for derived concepts
|
||||
|
||||
## 💬 Questions?
|
||||
|
||||
- Open a GitHub issue for technical questions
|
||||
|
||||
+23
-28
@@ -1,4 +1,4 @@
|
||||
# PageAgent 🤖🪄
|
||||
# Page Agent
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||
@@ -19,18 +19,16 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🎯 轻松集成**
|
||||
- 无需 Python,无需无头浏览器,无需浏览器插件。纯页面内脚本。
|
||||
- **🔐 端侧运行**
|
||||
- **🧠 HTML 脱水**
|
||||
- **💬 自然语言接口**
|
||||
- **🎨 HITL 交互界面**
|
||||
|
||||
以及 😉
|
||||
|
||||
- **🧪 实验性的 Chrome 扩展,支持跨页面控制** - `packages/extension`
|
||||
|
||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
||||
- **🎯 轻松集成**
|
||||
- 无需 `浏览器插件` / `Python` / `无头浏览器`。
|
||||
- 纯页面内 JavaScript,一切都在你的网页中完成。
|
||||
- The best tool for your agent to control web pages.
|
||||
- **📖 基于文本的 DOM 操作**
|
||||
- 无需截图,无需 OCR 或多模态模型。
|
||||
- 无需特殊权限。
|
||||
- **🧠 用你自己的 LLM**
|
||||
- **🎨 精美 UI,支持人机协同**
|
||||
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension),支持跨页面任务。**
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
@@ -39,19 +37,15 @@
|
||||
通过我们免费的 Demo LLM 快速体验 PageAgent:
|
||||
|
||||
```html
|
||||
<script
|
||||
src="https://registry.npmmirror.com/page-agent/1.1.1/files/dist/iife/page-agent.demo.js"
|
||||
crossorigin="true"
|
||||
></script>
|
||||
<script src="{URL}" crossorigin="true"></script>
|
||||
```
|
||||
|
||||
> - **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,可能随时变更。
|
||||
> - **🌷 建议使用自己的 LLM API。**
|
||||
|
||||
| Mirrors | URL |
|
||||
| ------- | ---------------------------------------------------------------------------------- |
|
||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.1.1/dist/iife/page-agent.demo.js |
|
||||
| China | https://registry.npmmirror.com/page-agent/1.1.1/files/dist/iife/page-agent.demo.js |
|
||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js |
|
||||
| China | https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js |
|
||||
|
||||
> **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,速度较慢,可能随时变更。
|
||||
|
||||
### NPM 安装
|
||||
|
||||
@@ -72,7 +66,7 @@ const agent = new PageAgent({
|
||||
await agent.execute('点击登录按钮')
|
||||
```
|
||||
|
||||
适用于无法使用 NPM 的环境,我们也提供了 IIFE 构建的 CDN 方式。[@see CDN Usage](https://alibaba.github.io/page-agent/#/docs/integration/cdn-setup)
|
||||
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/#/docs/introduction/overview)。
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
@@ -80,12 +74,13 @@ PageAgent adopts a simplified monorepo structure:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
||||
├── core/ # ** Core agent logic (npm: @page-agent/core) **
|
||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
||||
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
|
||||
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
|
||||
└── website/ # 文档站点
|
||||
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
|
||||
├── ui/ # 面板 UI (npm: @page-agent/ui)
|
||||
├── page-agent/ # 入口类 & iife 包 (npm: page-agent)
|
||||
├── extension/ # Chrome 扩展,支持跨页面任务
|
||||
└── website/ # 网站 & 文档站点
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# PageAgent 🤖🪄
|
||||
# Page Agent
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||
@@ -19,18 +19,16 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🎯 Easy Integration**
|
||||
- No python. No headless browser. No browser extension. Just in-page scripts.
|
||||
- **🔐 Client-Side Processing**
|
||||
- **🧠 DOM Extraction**
|
||||
- **💬 Natural Language Interface**
|
||||
- **🎨 UI with Human in the loop**
|
||||
|
||||
And 😉
|
||||
|
||||
- **🧪 `cross-page` control with an experimental chrome extension** - `packages/extension`
|
||||
|
||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
||||
- **🎯 Easy integration**
|
||||
- No need for `browser extension` / `python` / `headless browser`.
|
||||
- Just in-page javascript. Everything happens in your web page.
|
||||
- The best tool for your agent to control web pages.
|
||||
- **📖 Text-based DOM manipulation**
|
||||
- No screenshots. No OCR or multi-modal LLMs needed.
|
||||
- No special permissions required.
|
||||
- **🧠 Bring your own LLMs**
|
||||
- **🎨 Pretty UI with human-in-the-loop**
|
||||
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension) for multi-page tasks.**
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -39,19 +37,15 @@ And 😉
|
||||
Fastest way to try PageAgent with our free Demo LLM:
|
||||
|
||||
```html
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/page-agent@1.1.1/dist/iife/page-agent.demo.js"
|
||||
crossorigin="true"
|
||||
></script>
|
||||
<script src="{URL}" crossorigin="true"></script>
|
||||
```
|
||||
|
||||
> - **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. May change without notice.
|
||||
> - **🌷 Bring your own LLM API.**
|
||||
|
||||
| Mirrors | URL |
|
||||
| ------- | ---------------------------------------------------------------------------------- |
|
||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.1.1/dist/iife/page-agent.demo.js |
|
||||
| China | https://registry.npmmirror.com/page-agent/1.1.1/files/dist/iife/page-agent.demo.js |
|
||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js |
|
||||
| China | https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js |
|
||||
|
||||
> **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. Slow. May change without notice.
|
||||
|
||||
### NPM Installation
|
||||
|
||||
@@ -72,18 +66,21 @@ const agent = new PageAgent({
|
||||
await agent.execute('Click the login button')
|
||||
```
|
||||
|
||||
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/#/docs/introduction/overview).
|
||||
|
||||
## 🏗️ Structure
|
||||
|
||||
PageAgent adopts a simplified monorepo structure:
|
||||
|
||||
```
|
||||
packages/
|
||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
||||
├── core/ # ** Core agent logic (npm: @page-agent/core) **
|
||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
||||
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
|
||||
├── ui/ # Panel & i18n (npm: @page-agent/ui)
|
||||
└── website/ # Demo & Documentation site
|
||||
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||
├── ui/ # Panel UI (npm: @page-agent/ui)
|
||||
├── page-agent/ # Entry class and iife builds(npm: page-agent)
|
||||
├── extension/ # Chrome extension for multi-page tasks
|
||||
└── website/ # Website & Documentation site
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Generated
+388
-376
File diff suppressed because it is too large
Load Diff
+12
-10
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/page-controller",
|
||||
@@ -21,8 +21,7 @@
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"scripts": {
|
||||
"cleanup": "rm -rf packages/*/dist",
|
||||
@@ -39,27 +38,30 @@
|
||||
"@commitlint/cli": "^20.3.1",
|
||||
"@commitlint/config-conventional": "^20.3.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@microsoft/api-extractor": "^7.55.2",
|
||||
"@microsoft/api-extractor": "^7.56.3",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/node": "^25.0.9",
|
||||
"@types/node": "^25.2.2",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"chalk": "^5.6.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"dotenv": "^17.2.4",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-dom": "^2.7.2",
|
||||
"eslint-plugin-react-dom": "^2.12.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.0",
|
||||
"eslint-plugin-react-x": "^2.7.2",
|
||||
"eslint-plugin-react-x": "^2.12.2",
|
||||
"globals": "^17.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.4",
|
||||
"prettier": "^3.8.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"typescript-eslint": "^8.55.0",
|
||||
"unplugin-dts": "^1.0.0-beta.6",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||
"vite-bundle-analyzer": "^1.3.6"
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@page-agent/core",
|
||||
"private": false,
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"main": "./dist/esm/page-agent-core.js",
|
||||
"module": "./dist/esm/page-agent-core.js",
|
||||
@@ -45,7 +45,7 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.3.5",
|
||||
"@page-agent/llms": "1.1.1",
|
||||
"@page-agent/page-controller": "1.1.1"
|
||||
"@page-agent/llms": "1.3.0",
|
||||
"@page-agent/page-controller": "1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
+166
-125
@@ -3,15 +3,15 @@
|
||||
* All rights reserved.
|
||||
*/
|
||||
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
||||
import type { PageController } from '@page-agent/page-controller'
|
||||
import type { BrowserState, PageController } from '@page-agent/page-controller'
|
||||
import chalk from 'chalk'
|
||||
import zod from 'zod'
|
||||
import * as zod from 'zod'
|
||||
|
||||
import { type PageAgentConfig } from './config'
|
||||
import { type PageAgentConfig, type SupportedLanguage } from './config'
|
||||
import { DEFAULT_MAX_STEPS } from './config/constants'
|
||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||
import { tools } from './tools'
|
||||
import {
|
||||
import type {
|
||||
AgentActivity,
|
||||
AgentReflection,
|
||||
AgentStatus,
|
||||
@@ -21,17 +21,26 @@ import {
|
||||
MacroToolInput,
|
||||
MacroToolResult,
|
||||
} from './types'
|
||||
import { normalizeResponse, trimLines, uid, waitFor } from './utils'
|
||||
import { assert } from './utils/assert'
|
||||
import { assert, normalizeResponse, uid, waitFor } from './utils'
|
||||
|
||||
export { type PageAgentConfig }
|
||||
export type { SupportedLanguage }
|
||||
export { tool, type PageAgentTool } from './tools'
|
||||
export type * from './types'
|
||||
|
||||
/**
|
||||
* AI agent for browser DOM automation.
|
||||
* AI agent for browser automation.
|
||||
*
|
||||
* @remarks
|
||||
* ## Re-act Agent Loop
|
||||
* - step
|
||||
* - observe (gather information about current environment and context)
|
||||
* - think (LLM calling)
|
||||
* - reflection (evaluate history, generate memory, short-term planning)
|
||||
* - action (give the action to approach the next goal)
|
||||
* - act (execute the action)
|
||||
* - loop
|
||||
*
|
||||
* ## Event System
|
||||
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
||||
* - `historychange` - History events updated (persistent, part of agent memory)
|
||||
@@ -50,14 +59,18 @@ export type * from './types'
|
||||
* - Types: thinking, executing, executed, retrying, error
|
||||
*/
|
||||
export class PageAgentCore extends EventTarget {
|
||||
config: PageAgentConfig & { maxSteps: number }
|
||||
id = uid()
|
||||
tools: typeof tools
|
||||
readonly id = uid()
|
||||
readonly config: PageAgentConfig & { maxSteps: number }
|
||||
readonly tools: typeof tools
|
||||
/** PageController for DOM operations */
|
||||
readonly pageController: PageController
|
||||
|
||||
task = ''
|
||||
taskId = ''
|
||||
|
||||
/** Agent execution status */
|
||||
#status: AgentStatus = 'idle'
|
||||
/** History events */
|
||||
history: HistoricalEvent[] = []
|
||||
/** Whether this agent has been disposed */
|
||||
disposed = false
|
||||
|
||||
/**
|
||||
* Callback for when agent needs user input (ask_user tool)
|
||||
@@ -66,23 +79,21 @@ export class PageAgentCore extends EventTarget {
|
||||
*/
|
||||
onAskUser?: (question: string) => Promise<string>
|
||||
|
||||
#status: AgentStatus = 'idle'
|
||||
#llm: LLM
|
||||
#abortController = new AbortController()
|
||||
#observations: string[] = []
|
||||
|
||||
/** PageController for DOM operations */
|
||||
pageController: PageController
|
||||
|
||||
/** Runtime states for tracking across steps */
|
||||
states = {
|
||||
/** Accumulated wait time in seconds, used by wait tool */
|
||||
/** internal states during a single task execution */
|
||||
#states = {
|
||||
/** Accumulated wait time in seconds */
|
||||
totalWaitTime: 0,
|
||||
/** Last known URL for detecting navigation */
|
||||
/** For detecting navigation */
|
||||
lastURL: '',
|
||||
/** Browser state */
|
||||
browserState: null as BrowserState | null,
|
||||
}
|
||||
|
||||
/** History events */
|
||||
history: HistoricalEvent[] = []
|
||||
|
||||
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
||||
super()
|
||||
|
||||
@@ -166,15 +177,24 @@ export class PageAgentCore extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a persistent observation to the history event stream.
|
||||
* This will be visible in <agent_history> and remain in memory across steps.
|
||||
* Push a observation message to the history event stream.
|
||||
* This will be visible in <agent_history> and remain persistent in memory across steps.
|
||||
* @experimental @internal
|
||||
* @note history change will be emitted before next step starts
|
||||
*/
|
||||
pushObservation(content: string): void {
|
||||
this.history.push({ type: 'observation', content })
|
||||
this.#emitHistoryChange()
|
||||
this.#observations.push(content)
|
||||
}
|
||||
|
||||
/** Stop the current task. Agent remains reusable. */
|
||||
stop() {
|
||||
this.pageController.cleanUpHighlights()
|
||||
this.pageController.hideMask()
|
||||
this.#abortController.abort()
|
||||
}
|
||||
|
||||
async execute(task: string): Promise<ExecutionResult> {
|
||||
if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')
|
||||
if (!task) throw new Error('Task is required')
|
||||
this.task = task
|
||||
this.taskId = uid()
|
||||
@@ -203,30 +223,25 @@ export class PageAgentCore extends EventTarget {
|
||||
this.#setStatus('running')
|
||||
this.#emitHistoryChange()
|
||||
|
||||
// Reset states
|
||||
this.states = {
|
||||
totalWaitTime: 0,
|
||||
lastURL: '',
|
||||
}
|
||||
// Reset internal states
|
||||
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
|
||||
|
||||
try {
|
||||
let step = 0
|
||||
let step = 0
|
||||
|
||||
while (true) {
|
||||
await this.#generateObservations(step)
|
||||
while (true) {
|
||||
try {
|
||||
console.group(`step: ${step}`)
|
||||
|
||||
await onBeforeStep?.(this, step)
|
||||
|
||||
console.group(`step: ${step}`)
|
||||
// observe
|
||||
|
||||
// abort
|
||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||
console.log(chalk.blue.bold('👀 Observing...'))
|
||||
|
||||
// Thinking
|
||||
console.log(chalk.blue('Thinking...'))
|
||||
this.#emitActivity({ type: 'thinking' })
|
||||
this.#states.browserState = await this.pageController.getBrowserState()
|
||||
await this.#handleObservations(step)
|
||||
|
||||
// invoke LLM
|
||||
// assemble prompts
|
||||
|
||||
const messages = [
|
||||
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
||||
@@ -235,12 +250,17 @@ export class PageAgentCore extends EventTarget {
|
||||
|
||||
const tools = { AgentOutput: this.#packMacroTool() }
|
||||
|
||||
// invoke LLM
|
||||
|
||||
console.log(chalk.blue.bold('🧠 Thinking...'))
|
||||
this.#emitActivity({ type: 'thinking' })
|
||||
|
||||
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
||||
toolChoiceName: 'AgentOutput',
|
||||
normalizeResponse,
|
||||
})
|
||||
|
||||
// assemble history event
|
||||
// assemble history
|
||||
|
||||
const macroResult = result.toolResult as MacroToolResult
|
||||
const input = macroResult.input
|
||||
@@ -270,22 +290,12 @@ export class PageAgentCore extends EventTarget {
|
||||
|
||||
//
|
||||
|
||||
console.log(chalk.green('Step finished:'), actionName)
|
||||
console.groupEnd()
|
||||
|
||||
await onAfterStep?.(this, this.history)
|
||||
|
||||
step++
|
||||
if (step > this.config.maxSteps) {
|
||||
this.#onDone(false)
|
||||
const result: ExecutionResult = {
|
||||
success: false,
|
||||
data: 'Step count exceeded maximum limit',
|
||||
history: this.history,
|
||||
}
|
||||
await onAfterTask?.(this, result)
|
||||
return result
|
||||
}
|
||||
console.groupEnd()
|
||||
|
||||
// finish task if done
|
||||
|
||||
if (actionName === 'done') {
|
||||
const success = action.input?.success ?? false
|
||||
const text = action.input?.text || 'no text provided'
|
||||
@@ -299,19 +309,39 @@ export class PageAgentCore extends EventTarget {
|
||||
await onAfterTask?.(this, result)
|
||||
return result
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.groupEnd() // to prevent nested groups
|
||||
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
|
||||
|
||||
console.error('Task failed', error)
|
||||
const errorMessage = isAbortError ? 'Task stopped' : String(error)
|
||||
this.#emitActivity({ type: 'error', message: errorMessage })
|
||||
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
|
||||
this.#emitHistoryChange()
|
||||
this.#onDone(false)
|
||||
const result: ExecutionResult = {
|
||||
success: false,
|
||||
data: errorMessage,
|
||||
history: this.history,
|
||||
}
|
||||
await onAfterTask?.(this, result)
|
||||
return result
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Task failed', error)
|
||||
const errorMessage = String(error)
|
||||
this.#emitActivity({ type: 'error', message: errorMessage })
|
||||
this.#onDone(false)
|
||||
const result: ExecutionResult = {
|
||||
success: false,
|
||||
data: errorMessage,
|
||||
history: this.history,
|
||||
|
||||
step++
|
||||
if (step > this.config.maxSteps) {
|
||||
const errorMessage = 'Step count exceeded maximum limit'
|
||||
this.history.push({ type: 'error', message: errorMessage })
|
||||
this.#emitHistoryChange()
|
||||
this.#onDone(false)
|
||||
const result: ExecutionResult = {
|
||||
success: false,
|
||||
data: errorMessage,
|
||||
history: this.history,
|
||||
}
|
||||
await onAfterTask?.(this, result)
|
||||
return result
|
||||
}
|
||||
await onAfterTask?.(this, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +374,13 @@ export class PageAgentCore extends EventTarget {
|
||||
})
|
||||
|
||||
return {
|
||||
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
|
||||
description: 'You MUST call this tool every step!',
|
||||
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
||||
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
||||
// abort
|
||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||
|
||||
console.log(chalk.blue.bold('MacroTool execute'), input)
|
||||
console.log(chalk.blue.bold('MacroTool input'), input)
|
||||
const action = input.action
|
||||
|
||||
const toolName = Object.keys(action)[0]
|
||||
@@ -371,7 +401,7 @@ export class PageAgentCore extends EventTarget {
|
||||
|
||||
// Find the corresponding tool
|
||||
const tool = tools.get(toolName)
|
||||
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
||||
assert(tool, `Tool ${toolName} not found`)
|
||||
|
||||
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
||||
|
||||
@@ -395,9 +425,11 @@ export class PageAgentCore extends EventTarget {
|
||||
duration,
|
||||
})
|
||||
|
||||
// Reset wait time for non-wait tools
|
||||
if (toolName !== 'wait') {
|
||||
this.states.totalWaitTime = 0
|
||||
// counting wait time
|
||||
if (toolName === 'wait') {
|
||||
this.#states.totalWaitTime += toolInput?.seconds || 0
|
||||
} else {
|
||||
this.#states.totalWaitTime = 0
|
||||
}
|
||||
|
||||
// Return structured result
|
||||
@@ -427,17 +459,17 @@ export class PageAgentCore extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instructions from config and format as XML block
|
||||
* Get instructions from config
|
||||
*/
|
||||
async #getInstructions(): Promise<string> {
|
||||
const { instructions } = this.config
|
||||
if (!instructions) return ''
|
||||
|
||||
const systemInstructions = instructions.system?.trim()
|
||||
const url = await this.pageController.getCurrentUrl()
|
||||
let pageInstructions: string | undefined
|
||||
|
||||
if (instructions.getPageInstructions) {
|
||||
const url = this.#states.browserState?.url || ''
|
||||
if (instructions.getPageInstructions && url) {
|
||||
try {
|
||||
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||
} catch (error) {
|
||||
@@ -465,23 +497,28 @@ export class PageAgentCore extends EventTarget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate observations before each step
|
||||
* - URL change detection
|
||||
* - Too many steps warning
|
||||
* Generate system observations before each step
|
||||
* @todo loop detection
|
||||
* @todo console error
|
||||
*/
|
||||
async #generateObservations(stepCount: number): Promise<void> {
|
||||
async #handleObservations(step: number): Promise<void> {
|
||||
// Accumulated wait time warning
|
||||
if (this.#states.totalWaitTime >= 3) {
|
||||
this.pushObservation(
|
||||
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
|
||||
)
|
||||
}
|
||||
|
||||
// Detect URL change
|
||||
const currentURL = await this.pageController.getCurrentUrl()
|
||||
if (currentURL !== this.states.lastURL) {
|
||||
const currentURL = this.#states.browserState?.url || ''
|
||||
if (currentURL !== this.#states.lastURL) {
|
||||
this.pushObservation(`Page navigated to → ${currentURL}`)
|
||||
this.states.lastURL = currentURL
|
||||
this.#states.lastURL = currentURL
|
||||
await waitFor(0.5) // wait for page to stabilize
|
||||
}
|
||||
|
||||
// Warn about remaining steps
|
||||
const remaining = this.config.maxSteps - stepCount
|
||||
// Remaining steps warning
|
||||
const remaining = this.config.maxSteps - step
|
||||
if (remaining === 5) {
|
||||
this.pushObservation(
|
||||
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
||||
@@ -491,12 +528,25 @@ export class PageAgentCore extends EventTarget {
|
||||
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
||||
)
|
||||
}
|
||||
|
||||
// Push observations to history and emit
|
||||
if (this.#observations.length > 0) {
|
||||
for (const content of this.#observations) {
|
||||
this.history.push({ type: 'observation', content })
|
||||
console.log(chalk.cyan('Observation:'), content)
|
||||
}
|
||||
this.#observations = []
|
||||
this.#emitHistoryChange()
|
||||
}
|
||||
}
|
||||
|
||||
async #assembleUserPrompt(): Promise<string> {
|
||||
const browserState = this.#states.browserState!
|
||||
|
||||
let prompt = ''
|
||||
|
||||
// <instructions> (optional)
|
||||
|
||||
prompt += await this.#getInstructions()
|
||||
|
||||
// <agent_state>
|
||||
@@ -506,38 +556,36 @@ export class PageAgentCore extends EventTarget {
|
||||
|
||||
const stepCount = this.history.filter((e) => e.type === 'step').length
|
||||
|
||||
prompt += `<agent_state>
|
||||
<user_request>
|
||||
${this.task}
|
||||
</user_request>
|
||||
<step_info>
|
||||
Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
|
||||
Current date and time: ${new Date().toLocaleString()}
|
||||
</step_info>
|
||||
</agent_state>
|
||||
`
|
||||
prompt += '<agent_state>\n'
|
||||
prompt += '<user_request>\n'
|
||||
prompt += `${this.task}\n`
|
||||
prompt += '</user_request>\n'
|
||||
prompt += '<step_info>\n'
|
||||
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`
|
||||
prompt += `Current time: ${new Date().toLocaleString()}\n`
|
||||
prompt += '</step_info>\n'
|
||||
prompt += '</agent_state>\n\n'
|
||||
|
||||
// <agent_history>
|
||||
// - <step_N> for steps
|
||||
// - <sys> for observations and system messages
|
||||
|
||||
prompt += '\n<agent_history>\n'
|
||||
prompt += '<agent_history>\n'
|
||||
|
||||
let stepIndex = 0
|
||||
for (const event of this.history) {
|
||||
if (event.type === 'step') {
|
||||
stepIndex++
|
||||
prompt += `<step_${stepIndex}>
|
||||
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
|
||||
Memory: ${event.reflection.memory}
|
||||
Next Goal: ${event.reflection.next_goal}
|
||||
Action Results: ${event.action.output}
|
||||
</step_${stepIndex}>
|
||||
`
|
||||
prompt += `<step_${stepIndex}>\n`
|
||||
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`
|
||||
prompt += `Memory: ${event.reflection.memory}\n`
|
||||
prompt += `Next Goal: ${event.reflection.next_goal}\n`
|
||||
prompt += `Action Results: ${event.action.output}\n`
|
||||
prompt += `</step_${stepIndex}>\n`
|
||||
} else if (event.type === 'observation') {
|
||||
prompt += `<sys>${event.content}</sys>\n`
|
||||
} else if (event.type === 'user_takeover') {
|
||||
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
|
||||
prompt += `<sys>User took over control and made changes to the page</sys>\n`
|
||||
} else if (event.type === 'error') {
|
||||
// Error events are mainly for panel rendering, not included in LLM context
|
||||
// to avoid polluting the agent's reasoning with transient errors
|
||||
@@ -548,9 +596,18 @@ export class PageAgentCore extends EventTarget {
|
||||
|
||||
// <browser_state>
|
||||
|
||||
prompt += await this.#getBrowserState()
|
||||
let pageContent = browserState.content
|
||||
if (this.config.transformPageContent) {
|
||||
pageContent = await this.config.transformPageContent(pageContent)
|
||||
}
|
||||
|
||||
return trimLines(prompt)
|
||||
prompt += '<browser_state>\n'
|
||||
prompt += browserState.header + '\n'
|
||||
prompt += pageContent + '\n'
|
||||
prompt += browserState.footer + '\n\n'
|
||||
prompt += '</browser_state>\n\n'
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
#onDone(success = true) {
|
||||
@@ -560,25 +617,9 @@ export class PageAgentCore extends EventTarget {
|
||||
this.#abortController.abort()
|
||||
}
|
||||
|
||||
async #getBrowserState(): Promise<string> {
|
||||
const state = await this.pageController.getBrowserState()
|
||||
|
||||
let content = state.content
|
||||
if (this.config.transformPageContent) {
|
||||
content = await this.config.transformPageContent(content)
|
||||
}
|
||||
|
||||
return trimLines(`<browser_state>
|
||||
${state.header}
|
||||
${content}
|
||||
${state.footer}
|
||||
|
||||
</browser_state>
|
||||
`)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
console.log('Disposing PageAgent...')
|
||||
this.disposed = true
|
||||
this.pageController.dispose()
|
||||
// this.history = []
|
||||
this.#abortController.abort()
|
||||
|
||||
@@ -128,7 +128,6 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
|
||||
Here are examples of good output patterns. Use them as reference but never copy them directly.
|
||||
|
||||
<evaluation_examples>
|
||||
- Positive Examples:
|
||||
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
|
||||
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
|
||||
</evaluation_examples>
|
||||
@@ -139,7 +138,6 @@ Here are examples of good output patterns. Use them as reference but never copy
|
||||
|
||||
<next_goal_examples>
|
||||
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
|
||||
"next_goal": "Extract details from the first item on the page."
|
||||
</next_goal_examples>
|
||||
</examples>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Internal tools for PageAgent.
|
||||
* @note Adapted from browser-use
|
||||
*/
|
||||
import zod, { type z } from 'zod'
|
||||
import * as zod from 'zod'
|
||||
|
||||
import type { PageAgentCore } from '../PageAgentCore'
|
||||
import { waitFor } from '../utils'
|
||||
@@ -13,7 +13,7 @@ import { waitFor } from '../utils'
|
||||
export interface PageAgentTool<TParams = any> {
|
||||
// name: string
|
||||
description: string
|
||||
inputSchema: z.ZodType<TParams>
|
||||
inputSchema: zod.ZodType<TParams>
|
||||
execute: (this: PageAgentCore, args: TParams) => Promise<string>
|
||||
}
|
||||
|
||||
@@ -31,14 +31,13 @@ tools.set(
|
||||
'done',
|
||||
tool({
|
||||
description:
|
||||
'Complete task - provide a summary of results for the user. Set success=True if task completed successfully, false otherwise. Text should be your response to the user summarizing results.',
|
||||
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
|
||||
inputSchema: zod.object({
|
||||
text: zod.string(),
|
||||
success: zod.boolean().default(true),
|
||||
}),
|
||||
execute: async function (this: PageAgentCore, input) {
|
||||
// @note main loop will handle this one
|
||||
// this.onDone(input.text, input.success)
|
||||
return Promise.resolve('Task completed')
|
||||
},
|
||||
})
|
||||
@@ -47,25 +46,17 @@ tools.set(
|
||||
tools.set(
|
||||
'wait',
|
||||
tool({
|
||||
description:
|
||||
'Wait for x seconds. default 1s (max 10 seconds, min 1 second). This can be used to wait until the page or data is fully loaded.',
|
||||
description: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',
|
||||
inputSchema: zod.object({
|
||||
seconds: zod.number().min(1).max(10).default(1),
|
||||
}),
|
||||
execute: async function (this: PageAgentCore, input) {
|
||||
// try to subtract LLM calling time from the actual wait time
|
||||
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
||||
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
||||
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
||||
await waitFor(actualWaitTime)
|
||||
|
||||
this.states.totalWaitTime += input.seconds
|
||||
|
||||
if (this.states.totalWaitTime >= 3) {
|
||||
this.pushObservation(
|
||||
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
|
||||
)
|
||||
}
|
||||
|
||||
return `✅ Waited for ${input.seconds} seconds.`
|
||||
},
|
||||
})
|
||||
@@ -106,7 +97,7 @@ tools.set(
|
||||
tools.set(
|
||||
'input_text',
|
||||
tool({
|
||||
description: 'Click and input text into a input interactive element',
|
||||
description: 'Click and type text into an interactive input element',
|
||||
inputSchema: zod.object({
|
||||
index: zod.int().min(0),
|
||||
text: zod.string(),
|
||||
@@ -140,8 +131,7 @@ tools.set(
|
||||
tools.set(
|
||||
'scroll',
|
||||
tool({
|
||||
description:
|
||||
'Scroll the page by specified number of pages (set down=True to scroll down, down=False to scroll up, num_pages=number of pages to scroll like 0.5 for half page, 1.0 for one page, etc.). Optional index parameter to scroll within a specific element or its scroll container (works well for dropdowns and custom UI components). Optional pixels parameter to scroll by a specific number of pixels instead of pages.',
|
||||
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
||||
inputSchema: zod.object({
|
||||
down: zod.boolean().default(true),
|
||||
num_pages: zod.number().min(0).max(10).optional().default(0.1),
|
||||
@@ -158,11 +148,14 @@ tools.set(
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
|
||||
*/
|
||||
tools.set(
|
||||
'scroll_horizontally',
|
||||
tool({
|
||||
description:
|
||||
'Scroll the page or element horizontally (set right=True to scroll right, right=False to scroll left, pixels=number of pixels to scroll). Optional index parameter to scroll within a specific element or its scroll container (works well for wide tables).',
|
||||
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
|
||||
inputSchema: zod.object({
|
||||
right: zod.boolean().default(true),
|
||||
pixels: zod.number().int().min(0),
|
||||
@@ -190,8 +183,6 @@ tools.set(
|
||||
})
|
||||
)
|
||||
|
||||
// @todo get_dropdown_options
|
||||
// @todo select_dropdown_option
|
||||
// @todo send_keys
|
||||
// @todo upload_file
|
||||
// @todo go_back
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import chalk from 'chalk'
|
||||
|
||||
/**
|
||||
* Simple assertion function that throws an error if the condition is falsy
|
||||
* @param condition - The condition to assert
|
||||
* @param message - Optional error message
|
||||
* @throws Error if condition is falsy
|
||||
*/
|
||||
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
|
||||
if (!condition) {
|
||||
const errorMessage = message ?? 'Assertion failed'
|
||||
|
||||
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,7 @@
|
||||
import chalk from 'chalk'
|
||||
|
||||
export { normalizeResponse } from './autoFixer'
|
||||
|
||||
/**
|
||||
* Wait until condition becomes true
|
||||
* @returns Returns when condition becomes true, throws otherwise
|
||||
* @param timeout Timeout in milliseconds, default 0 means no timeout, throws error on timeout
|
||||
*/
|
||||
export async function waitUntil(check: () => boolean, timeout = 60 * 60_1000): Promise<boolean> {
|
||||
if (check()) return true
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now()
|
||||
const interval = setInterval(() => {
|
||||
if (check()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (Date.now() - start > timeout) {
|
||||
clearInterval(interval)
|
||||
reject(new Error('Timeout waiting for condition to become true'))
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitFor(seconds: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
||||
}
|
||||
@@ -37,15 +17,6 @@ export function truncate(text: string, maxLength: number): string {
|
||||
|
||||
//
|
||||
|
||||
export function trimLines(text: string): string {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export function randomID(existingIDs?: string[]): string {
|
||||
let id = Math.random().toString(36).substring(2, 11)
|
||||
|
||||
@@ -85,3 +56,19 @@ export function uid() {
|
||||
ids.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple assertion function that throws an error if the condition is falsy
|
||||
* @param condition - The condition to assert
|
||||
* @param message - Optional error message
|
||||
* @throws Error if condition is falsy
|
||||
*/
|
||||
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
|
||||
if (!condition) {
|
||||
const errorMessage = message ?? 'Assertion failed'
|
||||
|
||||
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
|
||||
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Privacy Policy for Page Agent Extension
|
||||
|
||||
**Last updated:** January 2026
|
||||
**Last updated:** February 2026
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -10,24 +10,27 @@ Page Agent Extension is a browser automation tool that uses AI to help you inter
|
||||
|
||||
### Local Processing
|
||||
|
||||
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension itself.
|
||||
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
|
||||
|
||||
### Data Transmission
|
||||
|
||||
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
|
||||
|
||||
- Your task instructions (natural language commands)
|
||||
- Sanitized page structure (simplified DOM, excluding sensitive form values)
|
||||
- Simplified page structure (cleaned DOM) of all pages under the extension's control
|
||||
|
||||
are sent to the LLM API endpoint configured in **your settings**.
|
||||
|
||||
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
|
||||
|
||||
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
|
||||
|
||||
**If you use our testing endpoint**, your requests are proxied to [DeepSeek](https://deepseek.com) for AI processing. Regarding this test endpoint:
|
||||
|
||||
- This endpoint is provided for evaluation purposes only and is not recommended for production or daily use
|
||||
- The free model and their service providers may change at any time without prior notice
|
||||
- We do **not** store your task content, page content, or visited URLs
|
||||
- Minimal logging (timestamps, request metadata, IP addresses) may occur for abuse prevention and service stability
|
||||
- Minimal logging (timestamps, request metadata, IP addresses) may be collected for abuse prevention and service stability
|
||||
- DeepSeek's [Privacy Policy](https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html) applies to their processing of your requests
|
||||
|
||||
## Data Storage
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"css": "src/assets/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
@@ -16,7 +16,7 @@
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
"hooks": "@/lib/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@magicui": "https://magicui.design/r/{name}.json"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
AI-powered browser automation. Control web pages with natural language.
|
||||
|
||||
Page Agent Ext — AI-Powered Browser Automation
|
||||
|
||||
🌟 What is Page Agent Ext?
|
||||
|
||||
Page Agent Ext brings AI-powered automation to your browser. Built on the open-source Page Agent framework, it lets you control web pages across multiple tabs using natural language — no scripting required.
|
||||
|
||||
🌟 Key Features:
|
||||
|
||||
- Natural Language Control — Command your browser in plain language, no code needed
|
||||
- Cross-Tab Automation — Seamlessly operate across multiple tabs and pages
|
||||
- Smart HTML Cleaning — Intelligently extracts and simplifies page structure for accurate AI understanding
|
||||
- Bring Your Own LLM — Use OpenAI, Anthropic, or any compatible API with full data control
|
||||
- Privacy-First — Zero data collection; all data flows directly to your chosen LLM provider
|
||||
- Open Source — MIT licensed, built on the Page Agent framework with full transparency
|
||||
|
||||
🌟 How It Works & Privacy:
|
||||
|
||||
Page Agent Ext performs DOM analysis locally in your browser. When you initiate a task, sanitized page structure is sent to the LLM API you configure. Your data is never collected or stored by us.
|
||||
|
||||
- Your API Key — Configure your own LLM API (OpenAI, Anthropic, etc.). Data goes directly to your provider
|
||||
- Test API — A free test endpoint is available for evaluation; we recommend your own key for regular use
|
||||
|
||||
Privacy policy: https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
|
||||
|
||||
🌟 Open Source:
|
||||
|
||||
This project is MIT licensed. Review the code, verify privacy claims, or extend it for your needs:
|
||||
https://github.com/alibaba/page-agent
|
||||
@@ -0,0 +1,30 @@
|
||||
AI 驱动的浏览器自动化助手,用自然语言控制网页。
|
||||
|
||||
Page Agent Ext — AI 驱动的浏览器自动化助手
|
||||
|
||||
🌟 什么是 Page Agent Ext?
|
||||
|
||||
Page Agent Ext 为浏览器带来 AI 自动化能力。基于开源的 Page Agent 框架,你可以用自然语言跨标签页控制网页,无需编写任何脚本。
|
||||
|
||||
🌟 核心特性:
|
||||
|
||||
- 自然语言控制 — 用日常语言指挥浏览器,无需编程
|
||||
- 跨标签页操作 — 在多个标签页之间无缝切换和操控
|
||||
- HTML 智能清洗 — 智能提取和精简页面结构,让 AI 准确理解网页内容
|
||||
- 使用你自己的模型 — 支持 OpenAI、Anthropic 或任何兼容 API,数据完全自主可控
|
||||
- 隐私优先 — 零数据收集,所有数据直接发送到你配置的 LLM 服务商
|
||||
- 开源透明 — MIT 协议,基于 Page Agent 开源框架,代码完全公开
|
||||
|
||||
🌟 工作原理与隐私:
|
||||
|
||||
Page Agent Ext 在浏览器本地进行 DOM 分析。当你发起任务时,经清洗的页面结构会发送到你配置的 LLM API。我们不会收集或存储你的任何数据。
|
||||
|
||||
- 你的 API Key — 配置你自己的 LLM API(OpenAI、Anthropic 等),数据直接发送到你的服务商
|
||||
- 测试 API — 提供免费测试端点供体验,日常使用建议配置自己的 Key
|
||||
|
||||
隐私政策:https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
|
||||
|
||||
🌟 开源项目:
|
||||
|
||||
MIT 协议开源。查看源码、验证隐私承诺,或按需扩展:
|
||||
https://github.com/alibaba/page-agent
|
||||
@@ -1,12 +1,18 @@
|
||||
# Page Agent Extension API
|
||||
|
||||
This document describes how to integrate the Page Agent browser extension into your web application.
|
||||
Integrate the Page Agent extension into your web app and trigger multi-page browser tasks from page JavaScript.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install the browser extension
|
||||
|
||||
Install the Page Agent extension from the Chrome Web Store.
|
||||
Primary channel:
|
||||
|
||||
- Chrome Web Store: https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
|
||||
|
||||
Latest updates are often published earlier on:
|
||||
|
||||
- GitHub Releases: https://github.com/alibaba/page-agent/releases
|
||||
|
||||
### 2. Install type definitions (recommended)
|
||||
|
||||
@@ -14,11 +20,19 @@ Install the Page Agent extension from the Chrome Web Store.
|
||||
npm install @page-agent/core --save-dev
|
||||
```
|
||||
|
||||
### 3. Set up authentication
|
||||
### 3. Authorization (Token)
|
||||
|
||||
The extension only injects APIs when it detects a valid token in `localStorage`.
|
||||
The token allows your page JS to call the extension API (`window.PAGE_AGENT_EXT`) and execute multi-page tasks.
|
||||
|
||||
1. Open the extension's side panel to get your authorization token
|
||||
Why token-based access is required:
|
||||
|
||||
- The extension has broad browser permissions (page access, navigation, multi-tab control).
|
||||
- If abused, it can harm user privacy and security.
|
||||
- Users must explicitly provide the token only to applications they trust.
|
||||
|
||||
Setup:
|
||||
|
||||
1. Open the extension side panel and copy your auth token.
|
||||
2. Set the token in your page:
|
||||
|
||||
```typescript
|
||||
@@ -60,36 +74,36 @@ if (await waitForExtension()) {
|
||||
|
||||
## Global API
|
||||
|
||||
The extension injects the following APIs into the `window` object:
|
||||
After token match, the extension injects APIs into `window`.
|
||||
|
||||
### `window.PAGE_AGENT_EXT_VERSION`
|
||||
|
||||
Extension version string (e.g., `"1.0.0"`). This is exposed separately to allow version checking before accessing the main API object.
|
||||
Extension version string (for capability checks before using the main API).
|
||||
|
||||
### `window.PAGE_AGENT_EXT`
|
||||
|
||||
Main API namespace object containing:
|
||||
Main namespace object.
|
||||
|
||||
#### `PAGE_AGENT_EXT.execute(task, config)`
|
||||
|
||||
Execute an agent task.
|
||||
Execute one agent task.
|
||||
|
||||
**Parameters:**
|
||||
Parameters:
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| ---- | ---- | -------- | ----------- |
|
||||
| `task` | `string` | Yes | Task description |
|
||||
| `config` | `ExecuteConfig` | Yes | Execution configuration (LLM settings, options, and event callbacks) |
|
||||
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
||||
|
||||
**Returns:** `Promise<ExecutionResult>`
|
||||
Returns: `Promise<ExecutionResult>`
|
||||
|
||||
#### `PAGE_AGENT_EXT.dispose()`
|
||||
#### `PAGE_AGENT_EXT.stop()`
|
||||
|
||||
Stop and destroy the current running agent.
|
||||
Stop the current task.
|
||||
|
||||
## Types
|
||||
|
||||
Install `@page-agent/core` for full type definitions:
|
||||
Install `@page-agent/core` for complete types:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
@@ -104,35 +118,24 @@ export interface ExecuteConfig {
|
||||
apiKey: string
|
||||
model: string
|
||||
|
||||
/**
|
||||
* Whether to include the initial tab (that holds this main world script) in the task.
|
||||
* @default true
|
||||
*/
|
||||
// Include the initial tab where page JS starts. Default: true.
|
||||
includeInitialTab?: boolean
|
||||
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
onDispose?: () => void
|
||||
}
|
||||
|
||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
```
|
||||
|
||||
### AgentStatus
|
||||
`AgentStatus`
|
||||
|
||||
```typescript
|
||||
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
||||
```
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `idle` | Agent is idle, ready to execute |
|
||||
| `running` | Agent is executing a task |
|
||||
| `completed` | Task completed successfully |
|
||||
| `error` | Task failed with an error |
|
||||
|
||||
### AgentActivity
|
||||
`AgentActivity`
|
||||
|
||||
```typescript
|
||||
type AgentActivity =
|
||||
@@ -143,15 +146,7 @@ type AgentActivity =
|
||||
| { type: 'error'; message: string }
|
||||
```
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `thinking` | Agent is analyzing the page and planning |
|
||||
| `executing` | Agent is executing a tool action |
|
||||
| `executed` | Tool execution completed |
|
||||
| `retrying` | Retrying after a failure |
|
||||
| `error` | An error occurred |
|
||||
|
||||
### HistoricalEvent
|
||||
`HistoricalEvent`
|
||||
|
||||
```typescript
|
||||
type HistoricalEvent =
|
||||
@@ -162,7 +157,7 @@ type HistoricalEvent =
|
||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||
```
|
||||
|
||||
### ExecutionResult
|
||||
`ExecutionResult`
|
||||
|
||||
```typescript
|
||||
interface ExecutionResult {
|
||||
@@ -183,81 +178,22 @@ const result = await window.PAGE_AGENT_EXT!.execute(
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
console.log('Task completed:', result.data)
|
||||
} else {
|
||||
console.error('Task failed')
|
||||
}
|
||||
```
|
||||
|
||||
### Exclude Initial Tab
|
||||
|
||||
By default, the agent includes the initial tab (where the script runs) in the task. Set `includeInitialTab: false` to exclude it:
|
||||
|
||||
```typescript
|
||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
||||
'Open a new tab and search for page-agent on GitHub',
|
||||
{
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
includeInitialTab: false, // Agent will open new tabs only
|
||||
includeInitialTab: false, // Optional: exclude current tab
|
||||
onStatusChange: (status) => console.log(status),
|
||||
onActivity: (activity) => console.log(activity),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### With Event Callbacks
|
||||
### Stop the Current Task
|
||||
|
||||
```typescript
|
||||
await window.PAGE_AGENT_EXT!.execute('Navigate to the settings page', {
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
onStatusChange: (status) => {
|
||||
updateUI({ agentStatus: status })
|
||||
},
|
||||
onActivity: (activity) => {
|
||||
switch (activity.type) {
|
||||
case 'thinking':
|
||||
showSpinner('Agent is thinking...')
|
||||
break
|
||||
case 'executing':
|
||||
showSpinner(`Executing: ${activity.tool}`)
|
||||
break
|
||||
case 'executed':
|
||||
log(`${activity.tool} completed in ${activity.duration}ms`)
|
||||
break
|
||||
case 'error':
|
||||
showError(activity.message)
|
||||
break
|
||||
}
|
||||
},
|
||||
onHistoryUpdate: (history) => {
|
||||
renderHistory(history)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Stop Execution
|
||||
|
||||
```typescript
|
||||
// Start a task
|
||||
window.PAGE_AGENT_EXT!.execute('Scroll through all pages', {
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
})
|
||||
|
||||
// Later, stop it
|
||||
window.PAGE_AGENT_EXT!.dispose()
|
||||
window.PAGE_AGENT_EXT!.stop()
|
||||
```
|
||||
|
||||
## Window Type Declaration
|
||||
|
||||
If not using `@page-agent/core`, add this to your project:
|
||||
If you are not importing `@page-agent/core`, add:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
@@ -275,7 +211,6 @@ interface ExecuteConfig {
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
onDispose?: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -283,8 +218,8 @@ declare global {
|
||||
PAGE_AGENT_EXT_VERSION?: string
|
||||
PAGE_AGENT_EXT?: {
|
||||
version: string
|
||||
execute: (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
dispose: () => void
|
||||
execute: Execute
|
||||
stop: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
# Page Agent 浏览器插件 API
|
||||
|
||||
本文档介绍如何在网页应用中接入 Page Agent 浏览器插件。
|
||||
|
||||
## 安装
|
||||
|
||||
### 1. 安装浏览器插件
|
||||
|
||||
从 Chrome 应用商店安装 Page Agent 插件。
|
||||
|
||||
### 2. 安装类型定义(推荐)
|
||||
|
||||
```bash
|
||||
npm install @page-agent/core --save-dev
|
||||
```
|
||||
|
||||
### 3. 配置认证
|
||||
|
||||
插件在页面加载后检测 `localStorage` 中的 token,匹配时才会注入 API。
|
||||
|
||||
1. 打开插件的侧边栏面板,获取授权 token
|
||||
2. 在页面中设置 token:
|
||||
|
||||
```typescript
|
||||
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
ExecutionResult,
|
||||
HistoricalEvent,
|
||||
} from '@page-agent/core'
|
||||
|
||||
// 等待插件注入(最多 1 秒)
|
||||
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
if (window.PAGE_AGENT_EXT) return true
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 使用
|
||||
if (await waitForExtension()) {
|
||||
const result = await window.PAGE_AGENT_EXT!.execute('点击登录按钮', {
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: 'your-api-key',
|
||||
model: 'gpt-5.2',
|
||||
onStatusChange: (status) => console.log('状态:', status),
|
||||
onActivity: (activity) => console.log('活动:', activity),
|
||||
})
|
||||
console.log('结果:', result)
|
||||
}
|
||||
```
|
||||
|
||||
## 全局 API
|
||||
|
||||
插件在 `window` 对象上注入以下 API:
|
||||
|
||||
### `window.PAGE_AGENT_EXT_VERSION`
|
||||
|
||||
插件版本号字符串(例如 `"1.0.0"`)。单独暴露版本号,方便在访问主 API 对象前进行版本检查。
|
||||
|
||||
### `window.PAGE_AGENT_EXT`
|
||||
|
||||
主 API 命名空间对象,包含:
|
||||
|
||||
#### `PAGE_AGENT_EXT.execute(task, config)`
|
||||
|
||||
执行 Agent 任务。
|
||||
|
||||
**参数:**
|
||||
|
||||
| 名称 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `task` | `string` | 是 | 任务描述 |
|
||||
| `config` | `ExecuteConfig` | 是 | 执行配置(LLM 设置、选项和事件回调) |
|
||||
|
||||
**返回:** `Promise<ExecutionResult>`
|
||||
|
||||
#### `PAGE_AGENT_EXT.dispose()`
|
||||
|
||||
停止并销毁当前运行的 Agent。
|
||||
|
||||
## 类型定义
|
||||
|
||||
安装 `@page-agent/core` 获取完整类型:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
ExecutionResult,
|
||||
HistoricalEvent,
|
||||
} from '@page-agent/core'
|
||||
|
||||
export interface ExecuteConfig {
|
||||
baseURL: string
|
||||
apiKey: string
|
||||
model: string
|
||||
|
||||
/**
|
||||
* 是否将初始标签页(运行此脚本的页面)包含在任务中。
|
||||
* @default true
|
||||
*/
|
||||
includeInitialTab?: boolean
|
||||
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
onDispose?: () => void
|
||||
}
|
||||
|
||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
```
|
||||
|
||||
### AgentStatus
|
||||
|
||||
```typescript
|
||||
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
||||
```
|
||||
|
||||
| 状态 | 说明 |
|
||||
|------|------|
|
||||
| `idle` | 空闲,准备执行 |
|
||||
| `running` | 正在执行任务 |
|
||||
| `completed` | 任务成功完成 |
|
||||
| `error` | 任务执行失败 |
|
||||
|
||||
### AgentActivity
|
||||
|
||||
```typescript
|
||||
type AgentActivity =
|
||||
| { type: 'thinking' }
|
||||
| { type: 'executing'; tool: string; input: unknown }
|
||||
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
||||
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
||||
| { type: 'error'; message: string }
|
||||
```
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `thinking` | Agent 正在分析页面并规划 |
|
||||
| `executing` | 正在执行工具操作 |
|
||||
| `executed` | 工具执行完成 |
|
||||
| `retrying` | 失败后重试 |
|
||||
| `error` | 发生错误 |
|
||||
|
||||
### HistoricalEvent
|
||||
|
||||
```typescript
|
||||
type HistoricalEvent =
|
||||
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
||||
| { type: 'observation'; content: string }
|
||||
| { type: 'user_takeover' }
|
||||
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||
```
|
||||
|
||||
### ExecutionResult
|
||||
|
||||
```typescript
|
||||
interface ExecutionResult {
|
||||
success: boolean
|
||||
data: string
|
||||
history: HistoricalEvent[]
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基础执行
|
||||
|
||||
```typescript
|
||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
||||
'在邮箱输入框填入 test@example.com 然后点击提交',
|
||||
{
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
}
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
console.log('任务完成:', result.data)
|
||||
} else {
|
||||
console.error('任务失败')
|
||||
}
|
||||
```
|
||||
|
||||
### 排除初始标签页
|
||||
|
||||
默认情况下,Agent 会将初始标签页(运行脚本的页面)包含在任务中。设置 `includeInitialTab: false` 可以排除它:
|
||||
|
||||
```typescript
|
||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
||||
'打开新标签页并在 GitHub 上搜索 page-agent',
|
||||
{
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
includeInitialTab: false, // Agent 只会打开新标签页
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 使用事件回调
|
||||
|
||||
```typescript
|
||||
await window.PAGE_AGENT_EXT!.execute('导航到设置页面', {
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
onStatusChange: (status) => {
|
||||
updateUI({ agentStatus: status })
|
||||
},
|
||||
onActivity: (activity) => {
|
||||
switch (activity.type) {
|
||||
case 'thinking':
|
||||
showSpinner('Agent 正在思考...')
|
||||
break
|
||||
case 'executing':
|
||||
showSpinner(`正在执行: ${activity.tool}`)
|
||||
break
|
||||
case 'executed':
|
||||
log(`${activity.tool} 完成,耗时 ${activity.duration}ms`)
|
||||
break
|
||||
case 'error':
|
||||
showError(activity.message)
|
||||
break
|
||||
}
|
||||
},
|
||||
onHistoryUpdate: (history) => {
|
||||
renderHistory(history)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 停止执行
|
||||
|
||||
```typescript
|
||||
// 启动任务
|
||||
window.PAGE_AGENT_EXT!.execute('滚动浏览所有页面', {
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
model: 'gpt-5.2',
|
||||
})
|
||||
|
||||
// 稍后停止
|
||||
window.PAGE_AGENT_EXT!.dispose()
|
||||
```
|
||||
|
||||
## Window 类型声明
|
||||
|
||||
如果不使用 `@page-agent/core`,可以添加以下声明:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
ExecutionResult,
|
||||
HistoricalEvent,
|
||||
} from '@page-agent/core'
|
||||
|
||||
interface ExecuteConfig {
|
||||
baseURL: string
|
||||
apiKey: string
|
||||
model: string
|
||||
includeInitialTab?: boolean
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
onDispose?: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
PAGE_AGENT_EXT_VERSION?: string
|
||||
PAGE_AGENT_EXT?: {
|
||||
version: string
|
||||
execute: (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
dispose: () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@page-agent/ext",
|
||||
"private": true,
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
@@ -10,23 +10,6 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/chrome": "^0.1.34",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"wxt": "^0.20.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@page-agent/core": "1.1.1",
|
||||
"@page-agent/llms": "1.1.1",
|
||||
"@page-agent/page-controller": "1.1.1",
|
||||
"@page-agent/ui": "1.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
@@ -34,14 +17,33 @@
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@types/chrome": "^0.1.34",
|
||||
"@types/react": "^19.2.13",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"lucide-react": "^0.563.0",
|
||||
"motion": "^12.30.0",
|
||||
"motion": "^12.34.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"rough-notation": "^0.5.1",
|
||||
"simple-icons": "^16.7.0",
|
||||
"simple-icons": "^16.8.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"wxt": "^0.20.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@page-agent/core": "1.3.0",
|
||||
"@page-agent/llms": "1.3.0",
|
||||
"@page-agent/page-controller": "1.3.0",
|
||||
"@page-agent/ui": "1.3.0",
|
||||
"ai-motion": "^0.4.8",
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extName": {
|
||||
"message": "Page Agent Ext"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
||||
},
|
||||
"extActionTitle": {
|
||||
"message": "Open Page Agent"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extName": {
|
||||
"message": "Page Agent Ext"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
||||
},
|
||||
"extActionTitle": {
|
||||
"message": "打开 Page Agent"
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,11 @@ export class MultiPageAgent extends PageAgentCore {
|
||||
})
|
||||
},
|
||||
|
||||
onBeforeStep: async (agent) => {
|
||||
// make sure the current tab is loaded before the step starts
|
||||
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
|
||||
},
|
||||
|
||||
onDispose: () => {
|
||||
if (heartBeatInterval) {
|
||||
window.clearInterval(heartBeatInterval)
|
||||
@@ -84,8 +89,7 @@ export class MultiPageAgent extends PageAgentCore {
|
||||
isAgentRunning: false,
|
||||
})
|
||||
|
||||
// no need to dispose tabsController and pageController
|
||||
// as they do not keep references
|
||||
tabsController.dispose()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,13 +8,21 @@ export function handlePageControlMessage(
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response: unknown) => void
|
||||
): true | undefined {
|
||||
const PREFIX = '[RemotePageController.background]'
|
||||
|
||||
function debug(...messages: any[]) {
|
||||
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||
}
|
||||
|
||||
const { action, payload, targetTabId } = message
|
||||
|
||||
if (action === 'get_my_tab_id') {
|
||||
debug('get_my_tab_id', sender.tab?.id)
|
||||
sendResponse({ tabId: sender.tab?.id || null })
|
||||
return
|
||||
}
|
||||
|
||||
// proxy to content script
|
||||
chrome.tabs
|
||||
.sendMessage(targetTabId, {
|
||||
type: 'PAGE_CONTROL',
|
||||
@@ -25,6 +33,7 @@ export function handlePageControlMessage(
|
||||
sendResponse(result)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(PREFIX, error)
|
||||
sendResponse({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
||||
@@ -12,10 +12,14 @@ export function initPageController() {
|
||||
.then((response) => {
|
||||
return (response as { tabId: number | null }).tabId
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[RemotePageController.ContentScript]: Failed to get my tab id', error)
|
||||
return null
|
||||
})
|
||||
|
||||
function getPC(): PageController {
|
||||
if (!pageController) {
|
||||
pageController = new PageController({ enableMask: false })
|
||||
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
|
||||
}
|
||||
return pageController
|
||||
}
|
||||
|
||||
@@ -2,13 +2,31 @@ import type { BrowserState } from '@page-agent/page-controller'
|
||||
|
||||
import type { TabsController } from './TabsController'
|
||||
|
||||
const PREFIX = '[RemotePageController]'
|
||||
|
||||
function debug(...messages: any[]) {
|
||||
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||
}
|
||||
|
||||
function sendMessage(message: {
|
||||
type: 'PAGE_CONTROL'
|
||||
action: string
|
||||
targetTabId: number
|
||||
payload?: any
|
||||
}): Promise<any> {
|
||||
return chrome.runtime.sendMessage(message).catch((error) => {
|
||||
console.error(PREFIX, message.action, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent side page controller.
|
||||
* - live in the agent env (extension page or content script)
|
||||
* - communicates with remote PageController via sw
|
||||
*/
|
||||
export class RemotePageController {
|
||||
private tabsController: TabsController
|
||||
tabsController: TabsController
|
||||
|
||||
constructor(tabsController: TabsController) {
|
||||
this.tabsController = tabsController
|
||||
@@ -18,46 +36,46 @@ export class RemotePageController {
|
||||
return this.tabsController.currentTabId
|
||||
}
|
||||
|
||||
async getCurrentUrl(): Promise<string> {
|
||||
private async getCurrentUrl(): Promise<string> {
|
||||
if (!this.currentTabId) return ''
|
||||
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||
return url || ''
|
||||
}
|
||||
|
||||
async getCurrentTitle(): Promise<string> {
|
||||
private async getCurrentTitle(): Promise<string> {
|
||||
if (!this.currentTabId) return ''
|
||||
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||
return title || ''
|
||||
}
|
||||
|
||||
get currentTabTitle(): Promise<string> {
|
||||
return this.getCurrentTitle()
|
||||
}
|
||||
|
||||
async getLastUpdateTime(): Promise<number> {
|
||||
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
||||
|
||||
return await chrome.runtime.sendMessage({
|
||||
return sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'get_last_update_time',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
// getBrowserState
|
||||
async getBrowserState(): Promise<BrowserState> {
|
||||
let browserState = {} as BrowserState
|
||||
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
||||
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
let browserState = {} as BrowserState
|
||||
debug('getBrowserState', this.currentTabId)
|
||||
|
||||
const currentUrl = await this.getCurrentUrl()
|
||||
const currentTitle = await this.getCurrentTitle()
|
||||
|
||||
if (!this.currentTabId || !isContentScriptAllowed(currentUrl)) {
|
||||
browserState = {
|
||||
url: await this.currentTabUrl,
|
||||
title: await this.currentTabTitle,
|
||||
url: currentUrl,
|
||||
title: currentTitle,
|
||||
header: '',
|
||||
content: '(empty page)',
|
||||
content: '(empty page. either current page is not readable or not loaded yet.)',
|
||||
footer: '',
|
||||
}
|
||||
} else {
|
||||
browserState = await chrome.runtime.sendMessage({
|
||||
browserState = await sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'get_browser_state',
|
||||
targetTabId: this.currentTabId,
|
||||
@@ -67,61 +85,58 @@ export class RemotePageController {
|
||||
const sum = await this.tabsController.summarizeTabs()
|
||||
browserState.header = sum + '\n\n' + (browserState.header || '')
|
||||
|
||||
debug('getBrowserState: success', this.currentTabId, browserState)
|
||||
|
||||
return browserState
|
||||
}
|
||||
|
||||
// updateTree
|
||||
async updateTree(): Promise<void> {
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
|
||||
return
|
||||
}
|
||||
|
||||
await chrome.runtime.sendMessage({
|
||||
await sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'update_tree',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
// cleanUpHighlights
|
||||
async cleanUpHighlights(): Promise<void> {
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
|
||||
return
|
||||
}
|
||||
|
||||
await chrome.runtime.sendMessage({
|
||||
await sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'clean_up_highlights',
|
||||
targetTabId: this.currentTabId,
|
||||
})
|
||||
}
|
||||
|
||||
// clickElement
|
||||
async clickElement(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('click_element', args)
|
||||
const res = await this.remoteCallDomAction('click_element', args)
|
||||
// @note may cause page navigation, wait for 1 second to ensure the page loading started
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
return res
|
||||
}
|
||||
|
||||
// inputText
|
||||
async inputText(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('input_text', args)
|
||||
}
|
||||
|
||||
// selectOption
|
||||
async selectOption(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('select_option', args)
|
||||
}
|
||||
|
||||
// scroll
|
||||
async scroll(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('scroll', args)
|
||||
}
|
||||
|
||||
// scrollHorizontally
|
||||
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('scroll_horizontally', args)
|
||||
}
|
||||
|
||||
// executeJavascript
|
||||
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
|
||||
return this.remoteCallDomAction('execute_javascript', args)
|
||||
}
|
||||
@@ -133,35 +148,26 @@ export class RemotePageController {
|
||||
/** @note Managed by content script via storage polling. */
|
||||
dispose(): void {}
|
||||
|
||||
private async preCheck() {
|
||||
if (!this.currentTabId) {
|
||||
return 'RemotePageController not initialized.'
|
||||
}
|
||||
|
||||
if (!isContentScriptAllowed(await this.currentTabUrl)) {
|
||||
return 'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
|
||||
const preCheckError = await this.preCheck()
|
||||
if (preCheckError) {
|
||||
return { success: false, message: preCheckError }
|
||||
if (!this.currentTabId) {
|
||||
return { success: false, message: 'RemotePageController not initialized.' }
|
||||
}
|
||||
|
||||
return await chrome.runtime.sendMessage({
|
||||
if (!isContentScriptAllowed(await this.getCurrentUrl())) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.',
|
||||
}
|
||||
}
|
||||
|
||||
return sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: action,
|
||||
targetTabId: this.currentTabId!,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
private get currentTabUrl(): Promise<string> {
|
||||
return this.getCurrentUrl()
|
||||
}
|
||||
}
|
||||
|
||||
interface DomActionReturn {
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
*/
|
||||
import type { TabAction } from './TabsController'
|
||||
|
||||
const PREFIX = '[TabsController.background]'
|
||||
|
||||
function debug(...messages: any[]) {
|
||||
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||
}
|
||||
|
||||
export function handleTabControlMessage(
|
||||
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
|
||||
sender: chrome.runtime.MessageSender,
|
||||
@@ -12,10 +18,12 @@ export function handleTabControlMessage(
|
||||
|
||||
switch (action as TabAction) {
|
||||
case 'get_active_tab': {
|
||||
debug('get_active_tab')
|
||||
chrome.tabs
|
||||
.query({ active: true, currentWindow: true })
|
||||
.then((tabs) => {
|
||||
const tabId = tabs.length > 0 ? tabs[0].id || null : null
|
||||
debug('get_active_tab: success', tabId)
|
||||
sendResponse({ success: true, tabId })
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -25,11 +33,12 @@ export function handleTabControlMessage(
|
||||
}
|
||||
|
||||
case 'get_tab_info': {
|
||||
debug('get_tab_info', payload)
|
||||
chrome.tabs
|
||||
.get(payload.tabId)
|
||||
.then((tab) => {
|
||||
const result = { title: tab.title || '', url: tab.url || '' }
|
||||
sendResponse(result)
|
||||
debug('get_tab_info: success', tab)
|
||||
sendResponse(tab)
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
||||
@@ -38,10 +47,11 @@ export function handleTabControlMessage(
|
||||
}
|
||||
|
||||
case 'open_new_tab': {
|
||||
debug('open_new_tab', payload)
|
||||
chrome.tabs
|
||||
.create({ url: payload.url, active: false })
|
||||
.then((newTab) => {
|
||||
// @todo: wait for tab to be fully loaded
|
||||
debug('open_new_tab: success', newTab)
|
||||
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId })
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -51,20 +61,22 @@ export function handleTabControlMessage(
|
||||
}
|
||||
|
||||
case 'create_tab_group': {
|
||||
debug('create_tab_group', payload)
|
||||
chrome.tabs
|
||||
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
|
||||
.then((groupId) => {
|
||||
console.log('Created tab group', groupId)
|
||||
debug('create_tab_group: success', groupId)
|
||||
sendResponse({ success: true, groupId })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to create tab group', error)
|
||||
console.error(PREFIX, 'Failed to create tab group', error)
|
||||
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
||||
})
|
||||
return true // async response
|
||||
}
|
||||
|
||||
case 'update_tab_group': {
|
||||
debug('update_tab_group', payload)
|
||||
chrome.tabGroups
|
||||
.update(payload.groupId, payload.properties)
|
||||
.then(() => {
|
||||
@@ -77,6 +89,7 @@ export function handleTabControlMessage(
|
||||
}
|
||||
|
||||
case 'add_tab_to_group': {
|
||||
debug('add_tab_to_group', payload)
|
||||
chrome.tabs
|
||||
.group({ tabIds: payload.tabId, groupId: payload.groupId })
|
||||
.then(() => {
|
||||
@@ -89,6 +102,7 @@ export function handleTabControlMessage(
|
||||
}
|
||||
|
||||
case 'close_tab': {
|
||||
debug('close_tab', payload)
|
||||
chrome.tabs
|
||||
.remove(payload.tabId)
|
||||
.then(() => {
|
||||
@@ -105,3 +119,42 @@ export function handleTabControlMessage(
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export function setupTabChangeEvents() {
|
||||
console.log('[TabsController.background] setupTabChangeEvents')
|
||||
|
||||
chrome.tabs.onCreated.addListener((tab) => {
|
||||
debug('onCreated', tab)
|
||||
chrome.runtime
|
||||
.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
|
||||
.catch((error) => {
|
||||
debug('onCreated error:', error)
|
||||
})
|
||||
})
|
||||
|
||||
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
||||
debug('onRemoved', tabId, removeInfo)
|
||||
chrome.runtime
|
||||
.sendMessage({
|
||||
type: 'TAB_CHANGE',
|
||||
action: 'removed',
|
||||
payload: { tabId, removeInfo },
|
||||
})
|
||||
.catch((error) => {
|
||||
debug('onRemoved error:', error)
|
||||
})
|
||||
})
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
debug('onUpdated', tabId, changeInfo)
|
||||
chrome.runtime
|
||||
.sendMessage({
|
||||
type: 'TAB_CHANGE',
|
||||
action: 'updated',
|
||||
payload: { tabId, changeInfo, tab },
|
||||
})
|
||||
.catch((error) => {
|
||||
debug('onUpdated error:', error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
const PREFIX = '[TabsController]'
|
||||
|
||||
function debug(...messages: any[]) {
|
||||
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||
}
|
||||
|
||||
function sendMessage(message: {
|
||||
type: 'TAB_CONTROL'
|
||||
action: TabAction
|
||||
payload?: any
|
||||
}): Promise<any> {
|
||||
return chrome.runtime.sendMessage(message).catch((error) => {
|
||||
console.error(PREFIX, message.action, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller for managing browser tabs.
|
||||
* - live in the agent env (extension page or content script)
|
||||
@@ -13,6 +30,8 @@ export class TabsController extends EventTarget {
|
||||
private windowId: number | null = null
|
||||
|
||||
async init(task: string, includeInitialTab: boolean = true) {
|
||||
debug('init', task, includeInitialTab)
|
||||
|
||||
this.task = task
|
||||
this.tabs = []
|
||||
this.currentTabId = null
|
||||
@@ -20,7 +39,7 @@ export class TabsController extends EventTarget {
|
||||
this.initialTabId = null
|
||||
this.windowId = null
|
||||
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
const result = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'get_active_tab',
|
||||
})
|
||||
@@ -33,9 +52,20 @@ export class TabsController extends EventTarget {
|
||||
|
||||
if (includeInitialTab) {
|
||||
this.currentTabId = this.initialTabId
|
||||
|
||||
// update tab status immediately
|
||||
const info = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'get_tab_info',
|
||||
payload: { tabId: this.initialTabId },
|
||||
})
|
||||
|
||||
this.tabs.push({
|
||||
id: result.tabId,
|
||||
isInitial: true,
|
||||
url: info.url,
|
||||
title: info.title,
|
||||
status: info.status,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -70,6 +100,14 @@ export class TabsController extends EventTarget {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.action === 'updated') {
|
||||
const { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }
|
||||
const targetTab = this.tabs.find((t) => t.id === tabId)
|
||||
if (targetTab) {
|
||||
targetTab.url = tab.url
|
||||
targetTab.title = tab.title
|
||||
targetTab.status = tab.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +118,10 @@ export class TabsController extends EventTarget {
|
||||
})
|
||||
}
|
||||
|
||||
async openNewTab(url: string): Promise<{ success: boolean; tabId: number; message: string }> {
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
async openNewTab(url: string): Promise<string> {
|
||||
debug('openNewTab', url)
|
||||
|
||||
const result = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'open_new_tab',
|
||||
payload: { url },
|
||||
@@ -104,7 +144,7 @@ export class TabsController extends EventTarget {
|
||||
await this.switchToTab(tabId)
|
||||
|
||||
if (!this.tabGroupId) {
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
const result = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'create_tab_group',
|
||||
payload: { tabIds: [tabId], windowId: this.windowId },
|
||||
@@ -118,7 +158,7 @@ export class TabsController extends EventTarget {
|
||||
|
||||
this.tabGroupId = groupId
|
||||
|
||||
await chrome.runtime.sendMessage({
|
||||
await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'update_tab_group',
|
||||
payload: {
|
||||
@@ -131,57 +171,43 @@ export class TabsController extends EventTarget {
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await chrome.runtime.sendMessage({
|
||||
await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'add_tab_to_group',
|
||||
payload: { tabId: result.tabId, groupId: this.tabGroupId },
|
||||
})
|
||||
}
|
||||
|
||||
// wait for the new tab to be fully loaded
|
||||
// @todo
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
await this.waitUntilTabLoaded(tabId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tabId,
|
||||
message: `Opened new tab ID ${tabId} with URL ${url}`,
|
||||
}
|
||||
return `✅ Opened new tab ID ${tabId} with URL ${url}`
|
||||
}
|
||||
|
||||
async switchToTab(tabId: number): Promise<{ success: boolean; message: string }> {
|
||||
async switchToTab(tabId: number): Promise<string> {
|
||||
debug('switchToTab', tabId)
|
||||
|
||||
const targetTab = this.tabs.find((t) => t.id === tabId)
|
||||
if (!targetTab) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Tab ID ${tabId} not found in tab list.`,
|
||||
}
|
||||
throw new Error(`Tab ID ${tabId} not found in tab list.`)
|
||||
}
|
||||
|
||||
await this.updateCurrentTabId(tabId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Switched to tab ID ${tabId}.`,
|
||||
}
|
||||
return `✅ Switched to tab ID ${tabId}.`
|
||||
}
|
||||
|
||||
async closeTab(tabId: number): Promise<{ success: boolean; message: string }> {
|
||||
async closeTab(tabId: number): Promise<string> {
|
||||
debug('closeTab', tabId)
|
||||
|
||||
const targetTab = this.tabs.find((t) => t.id === tabId)
|
||||
if (!targetTab) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Tab ID ${tabId} not found in tab list.`,
|
||||
}
|
||||
throw new Error(`Tab ID ${tabId} not found in tab list.`)
|
||||
}
|
||||
if (targetTab.isInitial) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Cannot close the initial tab ID ${tabId}.`,
|
||||
}
|
||||
throw new Error(`Cannot close the initial tab ID ${tabId}.`)
|
||||
}
|
||||
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
const result = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'close_tab',
|
||||
payload: { tabId },
|
||||
@@ -198,29 +224,39 @@ export class TabsController extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Closed tab ID ${tabId}.`,
|
||||
}
|
||||
return `✅ Closed tab ID ${tabId}.`
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to close tab ID ${tabId}: ${result.error}`,
|
||||
}
|
||||
throw new Error(`Failed to close tab ID ${tabId}: ${result.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async updateCurrentTabId(tabId: number | null) {
|
||||
debug('updateCurrentTabId', tabId)
|
||||
|
||||
this.currentTabId = tabId
|
||||
await chrome.storage.local.set({ currentTabId: tabId })
|
||||
}
|
||||
|
||||
async getTabInfo(tabId: number): Promise<{ title: string; url: string }> {
|
||||
const result = await chrome.runtime.sendMessage({
|
||||
// use cached tab info if available
|
||||
const tabMeta = this.tabs.find((t) => t.id === tabId)
|
||||
if (tabMeta && tabMeta.url && tabMeta.title) {
|
||||
return { title: tabMeta.title, url: tabMeta.url }
|
||||
}
|
||||
|
||||
// otherwise, pull the latest tab info from the background script
|
||||
debug('getTabInfo: pulling from background script', tabId)
|
||||
const result = await sendMessage({
|
||||
type: 'TAB_CONTROL',
|
||||
action: 'get_tab_info',
|
||||
payload: { tabId },
|
||||
})
|
||||
|
||||
if (tabMeta) {
|
||||
tabMeta.url = result.url
|
||||
tabMeta.title = result.title
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -239,6 +275,17 @@ export class TabsController extends EventTarget {
|
||||
return summaries.join('\n')
|
||||
}
|
||||
|
||||
async waitUntilTabLoaded(tabId: number): Promise<void> {
|
||||
const tab = this.tabs.find((t) => t.id === tabId)
|
||||
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)
|
||||
|
||||
if (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
|
||||
if (tab.status === 'complete') return
|
||||
|
||||
debug('waitUntilTabLoaded', tabId)
|
||||
await waitUntil(() => tab.status === 'complete', 4_000)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.dispatchEvent(new Event('dispose'))
|
||||
}
|
||||
@@ -257,6 +304,9 @@ export type TabAction =
|
||||
interface TabMeta {
|
||||
id: number
|
||||
isInitial: boolean
|
||||
url?: string
|
||||
title?: string
|
||||
status?: 'loading' | 'unloaded' | 'complete'
|
||||
}
|
||||
|
||||
const TAB_GROUP_COLORS = [
|
||||
@@ -275,3 +325,33 @@ type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
|
||||
function randomColor(): TabGroupColor {
|
||||
return TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until condition becomes true
|
||||
* @returns Returns when condition becomes true, throws otherwise
|
||||
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout
|
||||
* @param error Error object to reject on timeout. If not provided, will resolve with false
|
||||
*/
|
||||
export async function waitUntil(
|
||||
check: () => boolean | Promise<boolean>,
|
||||
timeoutMS = 60_000,
|
||||
error?: string
|
||||
): Promise<boolean> {
|
||||
if (await check()) return true
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now()
|
||||
const poll = async () => {
|
||||
if (await check()) return resolve(true)
|
||||
if (Date.now() - start > timeoutMS) {
|
||||
if (error) {
|
||||
return reject(new Error(error))
|
||||
} else {
|
||||
return resolve(false)
|
||||
}
|
||||
}
|
||||
setTimeout(poll, 100)
|
||||
}
|
||||
setTimeout(poll, 100)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,7 +120,6 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
|
||||
Here are examples of good output patterns. Use them as reference but never copy them directly.
|
||||
|
||||
<evaluation_examples>
|
||||
- Positive Examples:
|
||||
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
|
||||
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
|
||||
</evaluation_examples>
|
||||
@@ -131,7 +130,6 @@ Here are examples of good output patterns. Use them as reference but never copy
|
||||
|
||||
<next_goal_examples>
|
||||
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
|
||||
"next_goal": "Extract details from the first item on the page."
|
||||
</next_goal_examples>
|
||||
</examples>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* - switch_to_tab: Switch to an existing tab
|
||||
* - close_tab: Close a tab (optionally switch to another)
|
||||
*/
|
||||
import zod from 'zod'
|
||||
import * as zod from 'zod'
|
||||
|
||||
import type { TabsController } from './TabsController'
|
||||
|
||||
@@ -31,8 +31,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
||||
}),
|
||||
execute: async (input: unknown) => {
|
||||
const { url } = input as { url: string }
|
||||
const result = await tabsController.openNewTab(url)
|
||||
return result.message
|
||||
try {
|
||||
return await tabsController.openNewTab(url)
|
||||
} catch (error) {
|
||||
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -44,7 +47,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
||||
}),
|
||||
execute: async (input: unknown) => {
|
||||
const { tab_id } = input as { tab_id: number }
|
||||
return (await tabsController.switchToTab(tab_id)).message
|
||||
try {
|
||||
return await tabsController.switchToTab(tab_id)
|
||||
} catch (error) {
|
||||
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -56,7 +63,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
||||
}),
|
||||
execute: async (input: unknown) => {
|
||||
const { tab_id } = input as { tab_id: number }
|
||||
return (await tabsController.closeTab(tab_id)).message
|
||||
try {
|
||||
return await tabsController.closeTab(tab_id)
|
||||
} catch (error) {
|
||||
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
/**
|
||||
* React hook for using AgentController
|
||||
*/
|
||||
import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/core'
|
||||
import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
HistoricalEvent,
|
||||
SupportedLanguage,
|
||||
} from '@page-agent/core'
|
||||
import type { LLMConfig } from '@page-agent/llms'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { MultiPageAgent } from './MultiPageAgent'
|
||||
import { DEMO_CONFIG } from './constants'
|
||||
|
||||
/** Language preference: undefined means follow system */
|
||||
export type LanguagePreference = SupportedLanguage | undefined
|
||||
|
||||
export interface ExtConfig extends LLMConfig {
|
||||
language?: LanguagePreference
|
||||
}
|
||||
|
||||
export interface UseAgentResult {
|
||||
status: AgentStatus
|
||||
history: HistoricalEvent[]
|
||||
activity: AgentActivity | null
|
||||
currentTask: string
|
||||
config: LLMConfig | null
|
||||
config: ExtConfig | null
|
||||
execute: (task: string) => Promise<void>
|
||||
stop: () => void
|
||||
configure: (config: LLMConfig) => Promise<void>
|
||||
configure: (config: ExtConfig) => Promise<void>
|
||||
}
|
||||
|
||||
export function useAgent(): UseAgentResult {
|
||||
@@ -25,16 +37,17 @@ export function useAgent(): UseAgentResult {
|
||||
const [history, setHistory] = useState<HistoricalEvent[]>([])
|
||||
const [activity, setActivity] = useState<AgentActivity | null>(null)
|
||||
const [currentTask, setCurrentTask] = useState('')
|
||||
const [config, setConfig] = useState<LLMConfig | null>(null)
|
||||
const [config, setConfig] = useState<ExtConfig | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
chrome.storage.local.get('llmConfig').then((result) => {
|
||||
if (result.llmConfig) {
|
||||
setConfig(result.llmConfig as LLMConfig)
|
||||
} else {
|
||||
chrome.storage.local.get(['llmConfig', 'language']).then((result) => {
|
||||
const llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
|
||||
const language = (result.language as SupportedLanguage) || undefined
|
||||
const full = { ...llmConfig, language }
|
||||
if (!result.llmConfig) {
|
||||
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
|
||||
setConfig(DEMO_CONFIG)
|
||||
}
|
||||
setConfig(full)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -75,7 +88,7 @@ export function useAgent(): UseAgentResult {
|
||||
|
||||
const execute = useCallback(async (task: string) => {
|
||||
const agent = agentRef.current
|
||||
console.log('Fire task:', task, agent)
|
||||
console.log('🚀 [useAgent] start executing task:', task)
|
||||
if (!agent) throw new Error('Agent not initialized')
|
||||
|
||||
setCurrentTask(task)
|
||||
@@ -84,12 +97,17 @@ export function useAgent(): UseAgentResult {
|
||||
}, [])
|
||||
|
||||
const stop = useCallback(() => {
|
||||
agentRef.current?.dispose()
|
||||
agentRef.current?.stop()
|
||||
}, [])
|
||||
|
||||
const configure = useCallback(async (newConfig: LLMConfig) => {
|
||||
await chrome.storage.local.set({ llmConfig: newConfig })
|
||||
setConfig(newConfig)
|
||||
const configure = useCallback(async ({ language, ...llmConfig }: ExtConfig) => {
|
||||
await chrome.storage.local.set({ llmConfig })
|
||||
if (language) {
|
||||
await chrome.storage.local.set({ language })
|
||||
} else {
|
||||
await chrome.storage.local.remove('language')
|
||||
}
|
||||
setConfig({ ...llmConfig, language })
|
||||
}, [])
|
||||
|
||||
return {
|
||||
|
||||
@@ -111,6 +111,41 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
|
||||
@keyframes blink-cursor {
|
||||
0%,
|
||||
49% {
|
||||
opacity: 1;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow-a {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.45;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow-b {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -21,8 +21,9 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||
|
||||
// Focus state.
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
||||
// Focus state — soft multi-color glow matching ai-motion palette
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
|
||||
|
||||
// Error state.
|
||||
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { MotionProps, motion, useInView } from 'motion/react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface TypingAnimationProps extends MotionProps {
|
||||
children?: string
|
||||
words?: string[]
|
||||
className?: string
|
||||
duration?: number
|
||||
typeSpeed?: number
|
||||
deleteSpeed?: number
|
||||
delay?: number
|
||||
pauseDelay?: number
|
||||
loop?: boolean
|
||||
as?: React.ElementType
|
||||
startOnView?: boolean
|
||||
showCursor?: boolean
|
||||
blinkCursor?: boolean
|
||||
cursorStyle?: 'line' | 'block' | 'underscore'
|
||||
}
|
||||
|
||||
export function TypingAnimation({
|
||||
children,
|
||||
words,
|
||||
className,
|
||||
duration = 100,
|
||||
typeSpeed,
|
||||
deleteSpeed,
|
||||
delay = 0,
|
||||
pauseDelay = 1000,
|
||||
loop = false,
|
||||
as: Component = 'span',
|
||||
startOnView = true,
|
||||
showCursor = true,
|
||||
blinkCursor = true,
|
||||
cursorStyle = 'line',
|
||||
...props
|
||||
}: TypingAnimationProps) {
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
})
|
||||
|
||||
const [displayedText, setDisplayedText] = useState<string>('')
|
||||
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
|
||||
const elementRef = useRef<HTMLElement | null>(null)
|
||||
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||
amount: 0.3,
|
||||
once: true,
|
||||
})
|
||||
|
||||
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
|
||||
const hasMultipleWords = wordsToAnimate.length > 1
|
||||
|
||||
const typingSpeed = typeSpeed || duration
|
||||
const deletingSpeed = deleteSpeed || typingSpeed / 2
|
||||
|
||||
const shouldStart = startOnView ? isInView : true
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldStart || wordsToAnimate.length === 0) return
|
||||
|
||||
const timeoutDelay =
|
||||
delay > 0 && displayedText === ''
|
||||
? delay
|
||||
: phase === 'typing'
|
||||
? typingSpeed
|
||||
: phase === 'deleting'
|
||||
? deletingSpeed
|
||||
: pauseDelay
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const currentWord = wordsToAnimate[currentWordIndex] || ''
|
||||
const graphemes = Array.from(currentWord)
|
||||
|
||||
switch (phase) {
|
||||
case 'typing':
|
||||
if (currentCharIndex < graphemes.length) {
|
||||
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
|
||||
setCurrentCharIndex(currentCharIndex + 1)
|
||||
} else {
|
||||
if (hasMultipleWords || loop) {
|
||||
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
|
||||
if (!isLastWord || loop) {
|
||||
setPhase('pause')
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'pause':
|
||||
setPhase('deleting')
|
||||
break
|
||||
|
||||
case 'deleting':
|
||||
if (currentCharIndex > 0) {
|
||||
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
|
||||
setCurrentCharIndex(currentCharIndex - 1)
|
||||
} else {
|
||||
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||
setCurrentWordIndex(nextIndex)
|
||||
setPhase('typing')
|
||||
}
|
||||
break
|
||||
}
|
||||
}, timeoutDelay)
|
||||
|
||||
return () => clearTimeout(timeout)
|
||||
}, [
|
||||
shouldStart,
|
||||
phase,
|
||||
currentCharIndex,
|
||||
currentWordIndex,
|
||||
displayedText,
|
||||
wordsToAnimate,
|
||||
hasMultipleWords,
|
||||
loop,
|
||||
typingSpeed,
|
||||
deletingSpeed,
|
||||
pauseDelay,
|
||||
delay,
|
||||
])
|
||||
|
||||
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
|
||||
const isComplete =
|
||||
!loop &&
|
||||
currentWordIndex === wordsToAnimate.length - 1 &&
|
||||
currentCharIndex >= currentWordGraphemes.length &&
|
||||
phase !== 'deleting'
|
||||
|
||||
const shouldShowCursor =
|
||||
showCursor &&
|
||||
!isComplete &&
|
||||
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
|
||||
|
||||
const getCursorChar = () => {
|
||||
switch (cursorStyle) {
|
||||
case 'block':
|
||||
return '▌'
|
||||
case 'underscore':
|
||||
return '_'
|
||||
case 'line':
|
||||
default:
|
||||
return '|'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
ref={elementRef}
|
||||
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
|
||||
{...props}
|
||||
>
|
||||
{displayedText}
|
||||
{shouldShowCursor && (
|
||||
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
|
||||
{getCursorChar()}
|
||||
</span>
|
||||
)}
|
||||
</MotionComponent>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
|
||||
import { handleTabControlMessage } from '@/agent/TabsController.background'
|
||||
|
||||
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') {
|
||||
return handlePageControlMessage(message, sender, sendResponse)
|
||||
} else {
|
||||
sendResponse({ error: 'Unknown message type' })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Extension Setup
|
||||
// ============================================================================
|
||||
import { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log('[Background] Service Worker started')
|
||||
|
||||
// tab change events
|
||||
|
||||
setupTabChangeEvents()
|
||||
|
||||
// generate user auth token
|
||||
|
||||
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
|
||||
@@ -28,21 +17,20 @@ export default defineBackground(() => {
|
||||
chrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })
|
||||
})
|
||||
|
||||
// message proxy
|
||||
|
||||
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') {
|
||||
return handlePageControlMessage(message, sender, sendResponse)
|
||||
} else {
|
||||
sendResponse({ error: 'Unknown message type' })
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// setup
|
||||
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||
|
||||
// Tab change events
|
||||
|
||||
chrome.tabs.onCreated.addListener((tab) => {
|
||||
chrome.runtime.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
|
||||
})
|
||||
|
||||
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'TAB_CHANGE',
|
||||
action: 'removed',
|
||||
payload: { tabId, removeInfo },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ const DEBUG_PREFIX = '[Content]'
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
runAt: 'document_idle',
|
||||
runAt: 'document_end',
|
||||
|
||||
main() {
|
||||
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
|
||||
@@ -71,7 +71,8 @@ async function exposeAgentToPage() {
|
||||
try {
|
||||
const { task, config } = payload
|
||||
|
||||
// create when used
|
||||
// Dispose old instance before creating new one
|
||||
multiPageAgent?.dispose()
|
||||
|
||||
multiPageAgent = new MultiPageAgent(config)
|
||||
|
||||
@@ -116,17 +117,6 @@ async function exposeAgentToPage() {
|
||||
)
|
||||
})
|
||||
|
||||
multiPageAgent.addEventListener('dispose', () => {
|
||||
window.postMessage(
|
||||
{
|
||||
channel: 'PAGE_AGENT_EXT_RESPONSE',
|
||||
id,
|
||||
action: 'dispose_event',
|
||||
},
|
||||
'*'
|
||||
)
|
||||
})
|
||||
|
||||
// result
|
||||
|
||||
const result = await multiPageAgent.execute(task)
|
||||
@@ -155,9 +145,8 @@ async function exposeAgentToPage() {
|
||||
break
|
||||
}
|
||||
|
||||
case 'dispose': {
|
||||
// @note stop ongoing processes but can still be re-used later
|
||||
multiPageAgent?.dispose()
|
||||
case 'stop': {
|
||||
multiPageAgent?.stop()
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface ExecuteConfig {
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
onDispose?: () => void
|
||||
}
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
@@ -60,12 +59,6 @@ export default defineUnlistedScript(() => {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.action === 'dispose_event' && config.onDispose) {
|
||||
config.onDispose()
|
||||
window.removeEventListener('message', handleMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.action !== 'execute_result') return
|
||||
|
||||
// execute_result
|
||||
@@ -104,14 +97,14 @@ export default defineUnlistedScript(() => {
|
||||
return promise
|
||||
}
|
||||
|
||||
const dispose = () => {
|
||||
const stop = () => {
|
||||
const id = getId()
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
channel: 'PAGE_AGENT_EXT_REQUEST',
|
||||
id,
|
||||
action: 'dispose',
|
||||
action: 'stop',
|
||||
},
|
||||
'*'
|
||||
)
|
||||
@@ -121,6 +114,6 @@ export default defineUnlistedScript(() => {
|
||||
;(window as any).PAGE_AGENT_EXT = {
|
||||
version: __EXT_VERSION__,
|
||||
execute,
|
||||
dispose,
|
||||
stop,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Send, Settings, Square } from 'lucide-react'
|
||||
import { History, Send, Settings, Square } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -8,20 +8,47 @@ import {
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from '@/components/ui/input-group'
|
||||
import { saveSession } from '@/lib/db'
|
||||
|
||||
import { useAgent } from '../../agent/useAgent'
|
||||
import { ConfigPanel } from './components/ConfigPanel'
|
||||
import { HistoryDetail } from './components/HistoryDetail'
|
||||
import { HistoryList } from './components/HistoryList'
|
||||
import { ActivityCard, EventCard } from './components/cards'
|
||||
import { EmptyState, Logo, StatusDot } from './components/misc'
|
||||
import { EmptyState, Logo, MotionOverlay, StatusDot } from './components/misc'
|
||||
|
||||
type View =
|
||||
| { name: 'chat' }
|
||||
| { name: 'config' }
|
||||
| { name: 'history' }
|
||||
| { name: 'history-detail'; sessionId: string }
|
||||
|
||||
export default function App() {
|
||||
const [showConfig, setShowConfig] = useState(false)
|
||||
const [view, setView] = useState<View>({ name: 'chat' })
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const historyRef = useRef<HTMLDivElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
|
||||
|
||||
// Persist session when task finishes
|
||||
const prevStatusRef = useRef(status)
|
||||
useEffect(() => {
|
||||
const prev = prevStatusRef.current
|
||||
prevStatusRef.current = status
|
||||
|
||||
if (
|
||||
prev === 'running' &&
|
||||
(status === 'completed' || status === 'error') &&
|
||||
history.length > 0 &&
|
||||
currentTask
|
||||
) {
|
||||
saveSession({ task: currentTask, history, status }).catch((err) =>
|
||||
console.error('[SidePanel] Failed to save session:', err)
|
||||
)
|
||||
}
|
||||
}, [status, history, currentTask])
|
||||
|
||||
// Auto-scroll to bottom on new events
|
||||
useEffect(() => {
|
||||
if (historyRef.current) {
|
||||
@@ -56,36 +83,62 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
if (showConfig) {
|
||||
// --- View routing ---
|
||||
|
||||
if (view.name === 'config') {
|
||||
return (
|
||||
<ConfigPanel
|
||||
config={config}
|
||||
onSave={async (newConfig) => {
|
||||
await configure(newConfig)
|
||||
setShowConfig(false)
|
||||
setView({ name: 'chat' })
|
||||
}}
|
||||
onClose={() => setShowConfig(false)}
|
||||
onClose={() => setView({ name: 'chat' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (view.name === 'history') {
|
||||
return (
|
||||
<HistoryList
|
||||
onSelect={(id) => setView({ name: 'history-detail', sessionId: id })}
|
||||
onBack={() => setView({ name: 'chat' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (view.name === 'history-detail') {
|
||||
return <HistoryDetail sessionId={view.sessionId} onBack={() => setView({ name: 'history' })} />
|
||||
}
|
||||
|
||||
// --- Chat view ---
|
||||
|
||||
const isRunning = status === 'running'
|
||||
const showEmptyState = !currentTask && history.length === 0 && !isRunning
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background">
|
||||
<div className="relative flex flex-col h-screen bg-background">
|
||||
<MotionOverlay active={isRunning} />
|
||||
{/* Header */}
|
||||
<header className="flex items-center justify-between border-b px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Logo className="size-5" />
|
||||
<span className="text-sm font-medium">Page Agent Ext</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<StatusDot status={status} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setShowConfig(true)}
|
||||
onClick={() => setView({ name: 'history' })}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<History className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setView({ name: 'config' })}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Settings className="size-3.5" />
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { LLMConfig } from '@page-agent/llms'
|
||||
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { siGithub } from 'simple-icons'
|
||||
|
||||
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
|
||||
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
interface ConfigPanelProps {
|
||||
config: LLMConfig | null
|
||||
onSave: (config: LLMConfig) => Promise<void>
|
||||
config: ExtConfig | null
|
||||
onSave: (config: ExtConfig) => Promise<void>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
||||
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
|
||||
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
||||
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
||||
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [userAuthToken, setUserAuthToken] = useState<string>('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
@@ -28,6 +29,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
||||
setApiKey(config?.apiKey || DEMO_API_KEY)
|
||||
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
||||
setModel(config?.model || DEMO_MODEL)
|
||||
setLanguage(config?.language)
|
||||
}, [config])
|
||||
|
||||
// Poll for user auth token every second until found
|
||||
@@ -65,7 +67,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await onSave({ apiKey, baseURL, model })
|
||||
await onSave({ apiKey, baseURL, model, language })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -165,6 +167,19 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-muted-foreground">Language</label>
|
||||
<select
|
||||
value={language ?? ''}
|
||||
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
|
||||
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
|
||||
>
|
||||
<option value="">System</option>
|
||||
<option value="en-US">English</option>
|
||||
<option value="zh-CN">中文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
||||
Cancel
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { type SessionRecord, getSession } from '@/lib/db'
|
||||
|
||||
import { EventCard } from './cards'
|
||||
|
||||
export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack: () => void }) {
|
||||
const [session, setSession] = useState<SessionRecord | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
getSession(sessionId).then((s) => setSession(s ?? null))
|
||||
}, [sessionId])
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen text-xs text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
||||
<ArrowLeft className="size-3.5" />
|
||||
</Button>
|
||||
<span className="text-sm font-medium truncate">History</span>
|
||||
</header>
|
||||
|
||||
{/* Task */}
|
||||
<div className="border-b px-3 py-2 bg-muted/30">
|
||||
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">Task</div>
|
||||
<div className="text-xs font-medium" title={session.task}>
|
||||
{session.task}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Events (read-only) */}
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||
{session.history.map((event, index) => (
|
||||
// eslint-disable-next-line react-x/no-array-index-key
|
||||
<EventCard key={index} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
|
||||
|
||||
function timeAgo(ts: number): string {
|
||||
const seconds = Math.floor((Date.now() - ts) / 1000)
|
||||
if (seconds < 60) return 'just now'
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
|
||||
export function HistoryList({
|
||||
onSelect,
|
||||
onBack,
|
||||
}: {
|
||||
onSelect: (id: string) => void
|
||||
onBack: () => void
|
||||
}) {
|
||||
const [sessions, setSessions] = useState<SessionRecord[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setSessions(await listSessions())
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation()
|
||||
await deleteSession(id)
|
||||
setSessions((prev) => prev.filter((s) => s.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
||||
<ArrowLeft className="size-3.5" />
|
||||
</Button>
|
||||
<span className="text-sm font-medium flex-1">History</span>
|
||||
{sessions.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await clearSessions()
|
||||
setSessions([])
|
||||
}}
|
||||
className="text-[10px] text-muted-foreground hover:text-destructive cursor-pointer h-6 px-2"
|
||||
>
|
||||
<Trash2 className="size-3 mr-1" />
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* List */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && sessions.length === 0 && (
|
||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
||||
No history yet
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onSelect(session.id)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onSelect(session.id)}
|
||||
className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group"
|
||||
>
|
||||
{/* Status icon */}
|
||||
{session.status === 'completed' ? (
|
||||
<CheckCircle className="size-3.5 text-green-500 shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="size-3.5 text-destructive shrink-0 mt-0.5" />
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium truncate">{session.task}</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
{timeAgo(session.createdAt)} · {session.history.length} steps
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Delete */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleDelete(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer shrink-0"
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { AgentStatus } from '@page-agent/core'
|
||||
import { Motion } from 'ai-motion'
|
||||
import { BookOpen, Globe } from 'lucide-react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { siGithub } from 'simple-icons'
|
||||
|
||||
import { TypingAnimation } from '@/components/ui/typing-animation'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// Status dot indicator
|
||||
@@ -19,7 +24,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
|
||||
}[status]
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 mr-2">
|
||||
<span
|
||||
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
||||
/>
|
||||
@@ -32,14 +37,111 @@ export function Logo({ className }: { className?: string }) {
|
||||
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
|
||||
}
|
||||
|
||||
// Empty state with logo
|
||||
// Full-screen ai-motion glow overlay, shown only while running
|
||||
export function MotionOverlay({ active }: { active: boolean }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const motionRef = useRef<Motion | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||
const motion = new Motion({
|
||||
mode,
|
||||
borderWidth: 4,
|
||||
borderRadius: 14,
|
||||
glowWidth: mode === 'dark' ? 120 : 60,
|
||||
styles: { position: 'absolute', inset: '0' },
|
||||
})
|
||||
motionRef.current = motion
|
||||
containerRef.current!.appendChild(motion.element)
|
||||
motion.autoResize(containerRef.current!)
|
||||
|
||||
return () => {
|
||||
motion.dispose()
|
||||
motionRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const motion = motionRef.current
|
||||
if (!motion) return
|
||||
|
||||
let disposed = false
|
||||
if (active) {
|
||||
motion.start()
|
||||
motion.fadeIn()
|
||||
} else {
|
||||
motion.fadeOut().then(() => !disposed && motion.pause())
|
||||
}
|
||||
return () => {
|
||||
disposed = true
|
||||
}
|
||||
}, [active])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="pointer-events-none absolute inset-0 z-10 opacity-60"
|
||||
style={{ display: active ? undefined : 'none' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Empty state with logo and breathing glow
|
||||
export function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-6">
|
||||
<Logo className="size-20 opacity-80" />
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
|
||||
<div className="relative select-none pointer-events-none">
|
||||
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
|
||||
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
|
||||
<Logo className="relative size-20 opacity-80" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-medium text-foreground">Page Agent Ext</h2>
|
||||
<p className="text-xs text-muted-foreground mt-1">Enter a task to automate this page</p>
|
||||
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
|
||||
<TypingAnimation
|
||||
className="text-sm text-muted-foreground"
|
||||
words={[
|
||||
'Enter a task to automate this page',
|
||||
'Execute multi-page tasks',
|
||||
'Call this extension from your web page',
|
||||
'Use this extension in your own agents',
|
||||
]}
|
||||
cursorStyle="underscore"
|
||||
loop
|
||||
typeSpeed={20}
|
||||
deleteSpeed={10}
|
||||
pauseDelay={3000}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://alibaba.github.io/page-agent/#/docs/features/chrome-extension"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
title="Documentation"
|
||||
>
|
||||
<BookOpen className="size-4" />
|
||||
</a>
|
||||
<a
|
||||
href="https://alibaba.github.io/page-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground transition-colors"
|
||||
title="Website"
|
||||
>
|
||||
<Globe className="size-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { HistoricalEvent } from '@page-agent/core'
|
||||
import { type DBSchema, type IDBPDatabase, openDB } from 'idb'
|
||||
|
||||
const DB_NAME = 'page-agent-ext'
|
||||
const DB_VERSION = 1
|
||||
|
||||
export interface SessionRecord {
|
||||
id: string
|
||||
task: string
|
||||
history: HistoricalEvent[]
|
||||
status: 'completed' | 'error'
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface PageAgentDB extends DBSchema {
|
||||
sessions: {
|
||||
key: string
|
||||
value: SessionRecord
|
||||
indexes: { 'by-created': number }
|
||||
}
|
||||
}
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<PageAgentDB>> | null = null
|
||||
|
||||
function getDB() {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<PageAgentDB>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore('sessions', { keyPath: 'id' })
|
||||
store.createIndex('by-created', 'createdAt')
|
||||
},
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
|
||||
export async function saveSession(
|
||||
session: Omit<SessionRecord, 'id' | 'createdAt'>
|
||||
): Promise<SessionRecord> {
|
||||
const db = await getDB()
|
||||
const record: SessionRecord = {
|
||||
...session,
|
||||
id: crypto.randomUUID(),
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
await db.put('sessions', record)
|
||||
return record
|
||||
}
|
||||
|
||||
/** List sessions, newest first */
|
||||
export async function listSessions(): Promise<SessionRecord[]> {
|
||||
const db = await getDB()
|
||||
const all = await db.getAllFromIndex('sessions', 'by-created')
|
||||
return all.reverse()
|
||||
}
|
||||
|
||||
export async function getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const db = await getDB()
|
||||
return db.get('sessions', id)
|
||||
}
|
||||
|
||||
export async function deleteSession(id: string): Promise<void> {
|
||||
const db = await getDB()
|
||||
await db.delete('sessions', id)
|
||||
}
|
||||
|
||||
export async function clearSessions(): Promise<void> {
|
||||
const db = await getDB()
|
||||
await db.clear('sessions')
|
||||
}
|
||||
@@ -38,9 +38,9 @@ export default defineConfig({
|
||||
},
|
||||
}),
|
||||
manifest: {
|
||||
name: 'Page Agent Ext',
|
||||
description:
|
||||
'AI-powered browser automation assistant. Control web pages with natural language.',
|
||||
default_locale: 'en',
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
homepage_url: 'https://alibaba.github.io/page-agent/',
|
||||
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
||||
host_permissions: ['<all_urls>'],
|
||||
@@ -48,7 +48,7 @@ export default defineConfig({
|
||||
64: 'assets/page-agent-64.png',
|
||||
},
|
||||
action: {
|
||||
default_title: 'Open Page Agent',
|
||||
default_title: '__MSG_extActionTitle__',
|
||||
},
|
||||
web_accessible_resources: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@page-agent/llms",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent-llms.js",
|
||||
"module": "./dist/lib/page-agent-llms.js",
|
||||
|
||||
@@ -54,8 +54,10 @@ export class OpenAIClient implements LLMClient {
|
||||
signal: abortSignal,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
|
||||
const isAbortError = (error as any)?.name === 'AbortError'
|
||||
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
|
||||
if (!isAbortError) console.error(error)
|
||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
|
||||
}
|
||||
|
||||
// 3. Handle HTTP errors
|
||||
|
||||
@@ -34,12 +34,15 @@ export class InvokeError extends Error {
|
||||
super(message)
|
||||
this.name = 'InvokeError'
|
||||
this.type = type
|
||||
this.retryable = this.isRetryable(type)
|
||||
this.retryable = this.isRetryable(type, rawError)
|
||||
this.rawError = rawError
|
||||
this.rawResponse = rawResponse
|
||||
}
|
||||
|
||||
private isRetryable(type: InvokeErrorType): boolean {
|
||||
private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {
|
||||
const isAbortError = (rawError as any)?.name === 'AbortError'
|
||||
if (isAbortError) return false
|
||||
|
||||
const retryableTypes: InvokeErrorType[] = [
|
||||
InvokeErrorType.NETWORK_ERROR,
|
||||
InvokeErrorType.RATE_LIMIT,
|
||||
|
||||
@@ -49,6 +49,9 @@ export class LLM extends EventTarget {
|
||||
): Promise<InvokeResult> {
|
||||
return await withRetry(
|
||||
async () => {
|
||||
// in case user aborted before invoking
|
||||
if (abortSignal.aborted) throw new Error('AbortError')
|
||||
|
||||
const result = await this.client.invoke(messages, tools, abortSignal, options)
|
||||
|
||||
return result
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Utility functions for LLM integration
|
||||
*/
|
||||
import chalk from 'chalk'
|
||||
import { z } from 'zod'
|
||||
import * as z from 'zod'
|
||||
|
||||
import type { Tool } from './types'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "page-agent",
|
||||
"private": false,
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"main": "./dist/esm/page-agent.js",
|
||||
"module": "./dist/esm/page-agent.js",
|
||||
@@ -44,11 +44,11 @@
|
||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@page-agent/core": "1.3.0",
|
||||
"@page-agent/llms": "1.3.0",
|
||||
"@page-agent/page-controller": "1.3.0",
|
||||
"@page-agent/ui": "1.3.0",
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.3.5",
|
||||
"@page-agent/llms": "1.1.1",
|
||||
"@page-agent/page-controller": "1.1.1",
|
||||
"@page-agent/core": "1.1.1",
|
||||
"@page-agent/ui": "1.1.1"
|
||||
"zod": "^4.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { config as dotenvConfig } from 'dotenv'
|
||||
import { dirname, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { defineConfig } from 'vite'
|
||||
// import { analyzer } from 'vite-bundle-analyzer'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
@@ -14,8 +15,11 @@ dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
||||
// - alias all local packages so that they can be build in
|
||||
// - no external
|
||||
// - no d.ts. dts does not work with monorepo aliasing
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||
export default defineConfig(() => ({
|
||||
plugins: [
|
||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||
// analyzer()
|
||||
],
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@page-agent/page-controller",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-controller.js",
|
||||
"module": "./dist/lib/page-controller.js",
|
||||
|
||||
@@ -248,7 +248,7 @@ export class PageController extends EventTarget {
|
||||
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opens in a new tab. You are not capable of reading new tabs.`,
|
||||
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@page-agent/ui",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent-ui.js",
|
||||
"module": "./dist/lib/page-agent-ui.js",
|
||||
|
||||
@@ -12,6 +12,7 @@ const enUS = {
|
||||
question: 'Question: {{question}}',
|
||||
waitingPlaceholder: 'Waiting for task to start...',
|
||||
stop: 'Stop',
|
||||
close: 'Close',
|
||||
expand: 'Expand history',
|
||||
collapse: 'Collapse history',
|
||||
step: 'Step {{number}} · {{time}}{{duration}}',
|
||||
@@ -59,6 +60,7 @@ const zhCN = {
|
||||
question: '询问: {{question}}',
|
||||
waitingPlaceholder: '等待任务开始...',
|
||||
stop: '终止',
|
||||
close: '关闭',
|
||||
expand: '展开历史',
|
||||
collapse: '收起历史',
|
||||
step: '步骤 {{number}} · {{time}}{{duration}}',
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { Panel, type PanelConfig } from './panel/Panel'
|
||||
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
|
||||
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
|
||||
|
||||
@@ -33,7 +33,7 @@ export class Panel {
|
||||
#statusText: HTMLElement
|
||||
#historySection: HTMLElement
|
||||
#expandButton: HTMLElement
|
||||
#stopButton: HTMLElement
|
||||
#actionButton: HTMLElement
|
||||
#inputSection: HTMLElement
|
||||
#taskInput: HTMLInputElement
|
||||
|
||||
@@ -76,7 +76,7 @@ export class Panel {
|
||||
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
||||
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
||||
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
|
||||
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
|
||||
this.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
|
||||
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
|
||||
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
||||
|
||||
@@ -105,6 +105,15 @@ export class Panel {
|
||||
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
|
||||
this.#updateStatusIndicator(indicatorType)
|
||||
|
||||
// Morph action button: running = stop (■), not running = close (X)
|
||||
if (status === 'running') {
|
||||
this.#actionButton.textContent = '■'
|
||||
this.#actionButton.title = this.#i18n.t('ui.panel.stop')
|
||||
} else {
|
||||
this.#actionButton.textContent = 'X'
|
||||
this.#actionButton.title = this.#i18n.t('ui.panel.close')
|
||||
}
|
||||
|
||||
// Show/hide based on status
|
||||
if (status === 'running') {
|
||||
this.show()
|
||||
@@ -266,10 +275,14 @@ export class Panel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Agent
|
||||
* Action button handler: stop when running, close (dispose) when idle
|
||||
*/
|
||||
#stopAgent(): void {
|
||||
this.#agent.dispose()
|
||||
#handleActionButton(): void {
|
||||
if (this.#agent.status === 'running') {
|
||||
this.#agent.stop()
|
||||
} else {
|
||||
this.#agent.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,7 +396,7 @@ export class Panel {
|
||||
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
||||
▼
|
||||
</button>
|
||||
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
|
||||
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.close')}">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
@@ -420,10 +433,10 @@ export class Panel {
|
||||
this.#toggle()
|
||||
})
|
||||
|
||||
// Stop button
|
||||
this.#stopButton.addEventListener('click', (e) => {
|
||||
// Action button (stop / close)
|
||||
this.#actionButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
this.#stopAgent()
|
||||
this.#handleActionButton()
|
||||
})
|
||||
|
||||
// Submit on Enter key in input field
|
||||
|
||||
@@ -68,6 +68,9 @@ export interface PanelAgentAdapter extends EventTarget {
|
||||
/** Execute a task */
|
||||
execute(task: string): Promise<unknown>
|
||||
|
||||
/** Dispose the agent */
|
||||
/** Stop the current task (agent remains reusable) */
|
||||
stop(): void
|
||||
|
||||
/** Dispose the agent (terminal, cannot be reused) */
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@page-agent/website",
|
||||
"private": true,
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
@@ -10,30 +10,26 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"wouter": "^3.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@types/react": "^19.2.13",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"motion": "^12.30.0",
|
||||
"motion": "^12.34.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"rough-notation": "^0.5.1",
|
||||
"simple-icons": "^16.7.0",
|
||||
"simple-icons": "^16.8.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0"
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"wouter": "^3.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Demo build (auto-init with demo LLM, for quick testing)
|
||||
export const CDN_DEMO_URL =
|
||||
'https://cdn.jsdelivr.net/npm/page-agent@1.1.1/dist/iife/page-agent.demo.js'
|
||||
'https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js'
|
||||
export const CDN_DEMO_CN_URL =
|
||||
'https://registry.npmmirror.com/page-agent/1.1.1/files/dist/iife/page-agent.demo.js'
|
||||
'https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js'
|
||||
|
||||
// Demo LLM for website testing
|
||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||
|
||||
@@ -30,6 +30,10 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
||||
{ title: isZh ? '概览' : 'Overview', path: '/introduction/overview' },
|
||||
{ title: isZh ? '快速开始' : 'Quick Start', path: '/introduction/quick-start' },
|
||||
{ title: isZh ? '使用限制' : 'Limitations', path: '/introduction/limitations' },
|
||||
{
|
||||
title: isZh ? '故障排查' : 'Troubleshooting',
|
||||
path: '/introduction/troubleshooting',
|
||||
},
|
||||
{
|
||||
title: '🚧 ' + (isZh ? '最佳实践' : 'Best Practices'),
|
||||
path: '/integration/best-practices',
|
||||
|
||||
@@ -376,13 +376,6 @@ const result = await agent.execute('Fill in the form with test data')`}
|
||||
? '执行任务并返回结果。包含 success、data 和 history 字段。'
|
||||
: 'Execute a task and return result. Contains success, data, and history fields.',
|
||||
},
|
||||
{
|
||||
name: 'pushObservation(content: string)',
|
||||
type: 'void',
|
||||
description: isZh
|
||||
? '向历史流推送一个观察事件,会在下一步时被 LLM 看到'
|
||||
: 'Push an observation to history stream, will be seen by LLM in next step',
|
||||
},
|
||||
{
|
||||
name: 'dispose(reason?: string)',
|
||||
type: 'void',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { siGithub } from 'simple-icons'
|
||||
import { siChromewebstore, siGithub } from 'simple-icons'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { useLanguage } from '@/i18n/context'
|
||||
|
||||
export default function ChromeExtension() {
|
||||
const { isZh } = useLanguage()
|
||||
const chromeWebStoreUrl =
|
||||
'https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj'
|
||||
const githubReleasesUrl = 'https://github.com/alibaba/page-agent/releases'
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -13,70 +15,92 @@ export default function ChromeExtension() {
|
||||
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||
{isZh
|
||||
? '可选的 Chrome 扩展,解锁多页任务和第三方 API 集成。'
|
||||
: 'Optional Chrome extension that unlocks multi-page tasks and third-party API integration.'}
|
||||
? '可选的 Chrome 扩展。PageAgent.js 继续负责页面内自动化;扩展 API 额外提供多页面任务、浏览器级控制,以及从浏览器外部发起任务的能力。'
|
||||
: 'An optional Chrome extension. PageAgent.js keeps handling in-page automation, while the extension API adds multi-page tasks, browser-level control, and tasks initiated from outside the browser.'}
|
||||
</p>
|
||||
|
||||
<BetaNotice />
|
||||
|
||||
<div className="space-y-8 mt-8">
|
||||
{/* 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">
|
||||
<div>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{isZh
|
||||
? '解锁多页任务!借助 Chrome 扩展,Agent 可以跨标签页和页面导航,突破单页限制。'
|
||||
: 'Unlock multi-page tasks! With the Chrome extension, your agent can navigate across tabs and pages, breaking the single-page limitation.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '核心特性' : 'Key Features'}</h2>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">🔓 {isZh ? '多页任务' : 'Multi-Page Tasks'}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{isZh
|
||||
? '跨多个页面和标签页执行任务,不再局限于单页操作。'
|
||||
: 'Execute tasks across multiple pages and tabs. No longer limited to single-page operations.'}
|
||||
? '跨多个页面和标签页连续执行任务,不再受限于单页上下文。'
|
||||
: 'Run tasks across multiple pages and tabs without being limited to a single page context.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">
|
||||
🔌 {isZh ? '开放第三方接口' : 'Third-Party API'}
|
||||
🧭 {isZh ? '浏览器级控制' : 'Browser-Level Control'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{isZh
|
||||
? '用户授权后,你的网页、本地 Agent 或云端 Agent 都能通过扩展操作用户浏览器!'
|
||||
: 'After user authorization, your webpage, local agent, or cloud agent can control the browser through the extension.'}
|
||||
? '支持跨标签导航、页面切换和更完整的浏览器自动化能力。'
|
||||
: 'Enable richer browser automation, including cross-tab navigation and page switching.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">
|
||||
🔌 {isZh ? '开放集成接口' : 'Open Integration API'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{isZh
|
||||
? '用户主动授权后,页面 JS、本地 Agent 或云端 Agent 可通过扩展发起多页面任务。'
|
||||
: 'With explicit user authorization, page JS, local agents, or cloud agents can trigger multi-page tasks through the extension.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Download */}
|
||||
{/* Install */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '下载测试版' : 'Download Beta'}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? '扩展目前处于 Beta 阶段,请从 GitHub Releases 下载最新版本。'
|
||||
: 'The extension is currently in beta. Download the latest version from GitHub Releases.'}
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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={siGithub.path} />
|
||||
</svg>
|
||||
{isZh ? '前往 GitHub Releases 下载' : 'Download from GitHub Releases'}
|
||||
</a>
|
||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '获取扩展' : 'Get the Extension'}</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href={chromeWebStoreUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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={siChromewebstore.path} />
|
||||
</svg>
|
||||
{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}
|
||||
</a>
|
||||
<a
|
||||
href={githubReleasesUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d={siGithub.path} />
|
||||
</svg>
|
||||
{isZh ? 'GitHub Releases(更新版本)' : 'GitHub Releases (faster updates)'}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Relationship with PageAgent.js */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isZh ? '与 PageAgent.js 的关系' : 'How It Relates to PageAgent.js'}
|
||||
</h2>
|
||||
<div className="p-5 bg-gray-50 dark:bg-gray-800 rounded-lg space-y-3 text-gray-600 dark:text-gray-300">
|
||||
<p>
|
||||
{isZh
|
||||
? 'PageAgent.js 本身即可在页面内完成自动化。Chrome 扩展是可选的能力扩展。'
|
||||
: 'PageAgent.js already works for in-page automation. The Chrome extension is optional, not a dependency.'}
|
||||
</p>
|
||||
<p>
|
||||
{isZh
|
||||
? '通过扩展,你可以执行多页面任务、控制浏览器,以及从浏览器外部(本地服务或云端服务)发起任务。'
|
||||
: 'With the extension, you can perform multi-page tasks, browser-level control, and tasks triggered outside the browser (local or cloud services).'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Third-party Integration */}
|
||||
@@ -86,32 +110,33 @@ export default function ChromeExtension() {
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? '用户授权后,外部应用可以调用扩展 API 来控制浏览器。'
|
||||
: 'After user authorization, external applications can call the extension API to control the browser.'}
|
||||
? '通过页面 JavaScript 调用 `window.PAGE_AGENT_EXT`,你的应用可以发起跨页面任务并控制浏览器行为。'
|
||||
: 'By calling `window.PAGE_AGENT_EXT` from page JavaScript, your app can trigger multi-page tasks and control browser behavior.'}
|
||||
</p>
|
||||
|
||||
{/* Auth Flow */}
|
||||
<h3 className="text-xl font-semibold mb-3">{isZh ? '授权流程' : 'Authorization Flow'}</h3>
|
||||
<h3 className="text-xl font-semibold mb-3">
|
||||
{isZh ? '授权与安全' : 'Authorization and Security'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? '扩展使用基于 Token 的授权机制,扩展端和页面端必须持有匹配的 Token。'
|
||||
: 'The extension uses a token-based authorization mechanism. Both extension and page must have matching tokens.'}
|
||||
? '扩展权限范围较广(例如页面访问、导航、多标签控制)。若被滥用,可能危害用户隐私。为此,调用能力由 Token 保护,用户必须主动将 Token 提供给其信任的应用。'
|
||||
: 'The extension has broad permissions (such as page access, navigation, and multi-tab control). If abused, it can harm user privacy. That is why access is protected by a token, and users must actively share the token only with applications they trust.'}
|
||||
</p>
|
||||
|
||||
<CodeEditor
|
||||
code={
|
||||
isZh
|
||||
? `// 1. 用户安装扩展并在扩展设置中配置 auth token
|
||||
// 2. 你的页面读取相同的 token 并存入 localStorage
|
||||
// 3. Token 匹配后,扩展会暴露 window.PAGE_AGENT_EXT 对象
|
||||
? `// 1) 用户在扩展侧边栏获取 auth token
|
||||
// 2) 仅在可信应用中设置该 token
|
||||
// 3) token 匹配后,扩展会暴露 window.PAGE_AGENT_EXT
|
||||
|
||||
// ⚠️ 请在扩展弹窗中查看你的 auth token,然后填入下方
|
||||
// ⚠️ 不要把 token 提供给不可信页面或脚本
|
||||
localStorage.setItem('PageAgentExtUserAuthToken', '<从扩展中获取的-token>')`
|
||||
: `// 1. User installs extension and sets an auth token in extension settings
|
||||
// 2. Your page reads the same token and stores it in localStorage
|
||||
// 3. After token match, extension exposes window.PAGE_AGENT_EXT object
|
||||
: `// 1) Get auth token from the extension side panel
|
||||
// 2) Set it only in trusted applications
|
||||
// 3) After token match, extension exposes window.PAGE_AGENT_EXT
|
||||
|
||||
// ⚠️ Check your extension popup for the auth token
|
||||
// ⚠️ Never provide the token to untrusted pages or scripts
|
||||
localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')`
|
||||
}
|
||||
language="javascript"
|
||||
@@ -139,26 +164,90 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
|
||||
rel="noopener noreferrer"
|
||||
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
|
||||
>
|
||||
📄 {isZh ? '英文版 API 文档' : 'API Documentation (English)'}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/docs/extension_api_zh.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
|
||||
>
|
||||
📄 {isZh ? '中文版 API 文档' : 'API Documentation (Chinese)'}
|
||||
📄 {isZh ? 'API 文档' : 'API Documentation'}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3 className="text-xl font-semibold my-3">PAGE_AGENT_EXT.execute(task, config)</h3>
|
||||
{/* TypeScript Declaration */}
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isZh ? 'TypeScript 类型声明' : 'TypeScript Declaration'}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? '使用配置执行任务。返回一个 Promise,在任务完成时 resolve。config 参数包含 LLM 设置、选项和事件回调。'
|
||||
: 'Execute a task with configuration. Returns a Promise that resolves when the task completes. Config includes LLM settings, options, and event callbacks.'}
|
||||
? '推荐把 `execute` 的类型声明加入你的项目,获得完整类型提示。'
|
||||
: 'Add this `execute` declaration to your project for full type support.'}
|
||||
</p>
|
||||
|
||||
<CodeEditor
|
||||
code={
|
||||
isZh
|
||||
? `import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
ExecutionResult,
|
||||
HistoricalEvent
|
||||
} from '@page-agent/core'
|
||||
|
||||
interface ExecuteConfig {
|
||||
baseURL: string // LLM API 端点
|
||||
apiKey: string // API 密钥
|
||||
model: string // 模型名称
|
||||
|
||||
includeInitialTab?: boolean
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
}
|
||||
|
||||
type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
PAGE_AGENT_EXT_VERSION?: string
|
||||
PAGE_AGENT_EXT?: {
|
||||
version: string
|
||||
execute: Execute
|
||||
stop: () => void
|
||||
}
|
||||
}
|
||||
}`
|
||||
: `import type {
|
||||
AgentActivity,
|
||||
AgentStatus,
|
||||
ExecutionResult,
|
||||
HistoricalEvent
|
||||
} from '@page-agent/core'
|
||||
|
||||
interface ExecuteConfig {
|
||||
baseURL: string // LLM API endpoint
|
||||
apiKey: string // API key
|
||||
model: string // Model name
|
||||
|
||||
includeInitialTab?: boolean
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
}
|
||||
|
||||
type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
PAGE_AGENT_EXT_VERSION?: string
|
||||
PAGE_AGENT_EXT?: {
|
||||
version: string
|
||||
execute: Execute
|
||||
stop: () => void
|
||||
}
|
||||
}
|
||||
}`
|
||||
}
|
||||
language="typescript"
|
||||
/>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3">PAGE_AGENT_EXT.execute(task, config)</h3>
|
||||
|
||||
<CodeEditor
|
||||
code={
|
||||
isZh
|
||||
@@ -168,12 +257,11 @@ const result = await window.PAGE_AGENT_EXT.execute(
|
||||
{
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: 'your-api-key',
|
||||
model: 'gpt-5-2',
|
||||
model: 'gpt-5.2',
|
||||
// includeInitialTab: false, // 设为 false 排除初始标签页
|
||||
onStatusChange: status => console.log('状态变化:', status),
|
||||
onActivity: activity => console.log('活动:', activity),
|
||||
onHistoryUpdate: history => console.log('历史更新:', history),
|
||||
onDispose: () => console.log('已停止')
|
||||
onHistoryUpdate: history => console.log('历史更新:', history)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -184,12 +272,11 @@ const result = await window.PAGE_AGENT_EXT.execute(
|
||||
{
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
apiKey: 'your-api-key',
|
||||
model: 'gpt-5-2',
|
||||
model: 'gpt-5.2',
|
||||
// includeInitialTab: false, // Set to false to exclude initial tab
|
||||
onStatusChange: status => console.log('Status change:', status),
|
||||
onActivity: activity => console.log('Activity:', activity),
|
||||
onHistoryUpdate: history => console.log('History update:', history),
|
||||
onDispose: () => console.log('Disposed')
|
||||
onHistoryUpdate: history => console.log('History update:', history)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -198,130 +285,47 @@ console.log(result) // Task execution result`
|
||||
language="javascript"
|
||||
/>
|
||||
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3">PAGE_AGENT_EXT.dispose()</h3>
|
||||
<h3 className="text-xl font-semibold mt-6 mb-3">PAGE_AGENT_EXT.stop()</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? '停止当前正在运行的任务。停止后 Agent 可以重新使用。'
|
||||
: 'Stop the current running task. The agent can be reused after disposal.'}
|
||||
{isZh ? '停止当前正在运行的任务。' : 'Stop the current running task.'}
|
||||
</p>
|
||||
|
||||
<CodeEditor
|
||||
code={
|
||||
isZh
|
||||
? `// 停止当前任务
|
||||
window.PAGE_AGENT_EXT.dispose()`
|
||||
window.PAGE_AGENT_EXT.stop()`
|
||||
: `// Stop current task execution
|
||||
window.PAGE_AGENT_EXT.dispose()`
|
||||
window.PAGE_AGENT_EXT.stop()`
|
||||
}
|
||||
language="javascript"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ExecuteConfig */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '执行配置' : 'Execute Configuration'}</h2>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? 'config 参数包含 LLM 设置、选项和事件回调,用于控制任务执行行为。'
|
||||
: 'The config parameter includes LLM settings, options, and event callbacks to control task execution behavior.'}
|
||||
</p>
|
||||
|
||||
<CodeEditor
|
||||
code={
|
||||
isZh
|
||||
? `interface ExecuteConfig {
|
||||
baseURL: string // LLM API 端点
|
||||
apiKey: string // API 密钥
|
||||
model: string // 模型名称
|
||||
|
||||
// 是否将初始标签页包含在任务中,默认 true
|
||||
includeInitialTab?: boolean
|
||||
|
||||
// Agent 状态变化时调用(idle, running, error, completed 等)
|
||||
onStatusChange?: (status: AgentStatus) => void
|
||||
|
||||
// Agent 执行活动时调用(如点击、输入、导航等操作)
|
||||
onActivity?: (activity: AgentActivity) => void
|
||||
|
||||
// 历史记录更新时调用(包含完整的事件历史)
|
||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||
|
||||
// Agent 被停止时调用
|
||||
onDispose?: () => void
|
||||
}`
|
||||
: `interface ExecuteConfig {
|
||||
baseURL: string // LLM API endpoint
|
||||
apiKey: string // API key
|
||||
model: string // Model name
|
||||
|
||||
// Whether to include the initial tab in the task, default true
|
||||
includeInitialTab?: boolean
|
||||
|
||||
// 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">
|
||||
⚠️ {isZh ? '安全须知' : 'Security Notes'}
|
||||
</h3>
|
||||
<ul className="text-gray-600 dark:text-gray-300 space-y-1 text-sm">
|
||||
<li>
|
||||
•{' '}
|
||||
{isZh
|
||||
? '用户必须在扩展设置中显式授权每个域名'
|
||||
: 'Users must explicitly authorize each domain in extension settings'}
|
||||
</li>
|
||||
<li>
|
||||
•{' '}
|
||||
{isZh
|
||||
? '生产环境建议使用后端代理 LLM API Key'
|
||||
: 'Consider using backend proxy for LLM API keys in production'}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Integration Guide */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{isZh
|
||||
? '将 MultiPageAgent 融入你自己的插件'
|
||||
? '将 MultiPageAgent 集成你自己的插件'
|
||||
: 'Integrate MultiPageAgent into Your Extension'}
|
||||
</h2>
|
||||
<p>@TODO</p>
|
||||
<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.'}
|
||||
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
|
||||
: 'Start with the extension API docs, then use the background entry implementation as a reference.'}
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function CustomTools() {
|
||||
</p>
|
||||
|
||||
<CodeEditor
|
||||
code={`import zod from 'zod'
|
||||
code={`import * as zod from 'zod'
|
||||
import { PageAgent, tool } from 'page-agent'
|
||||
|
||||
// override internal tool
|
||||
|
||||
@@ -20,6 +20,7 @@ import Limitations from './introduction/limitations/page'
|
||||
// Introduction
|
||||
import Overview from './introduction/overview/page'
|
||||
import QuickStart from './introduction/quick-start/page'
|
||||
import Troubleshooting from './introduction/troubleshooting/page'
|
||||
|
||||
function DocsPage({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -51,6 +52,11 @@ export default function DocsRouter() {
|
||||
<Limitations />
|
||||
</DocsPage>
|
||||
</Route>
|
||||
<Route path="/introduction/troubleshooting">
|
||||
<DocsPage>
|
||||
<Troubleshooting />
|
||||
</DocsPage>
|
||||
</Route>
|
||||
|
||||
{/* Features */}
|
||||
<Route path="/features/custom-tools">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Link } from 'wouter'
|
||||
|
||||
import { useLanguage } from '@/i18n/context'
|
||||
|
||||
export default function LimitationsPage() {
|
||||
@@ -11,184 +13,144 @@ export default function LimitationsPage() {
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300">
|
||||
{isZh
|
||||
? '了解 page-agent 当前的功能边界和技术限制'
|
||||
: "Understand page-agent's current capabilities and technical constraints"}
|
||||
? 'Page Agent 基于 DOM 理解网页并执行操作。这决定了它的能力边界。'
|
||||
: 'Page Agent understands web pages via DOM and performs actions accordingly. This defines its capability boundary.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
||||
{/* PageAgent.js vs PageAgentExt */}
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{isZh ? '页面支持限制' : 'Page Support Limitations'}
|
||||
{isZh ? 'PageAgent.js vs PageAgentExt' : 'PageAgent.js vs PageAgentExt'}
|
||||
</h2>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
|
||||
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
||||
{isZh ? '单页应用限制' : 'Single Page Application Limits'}
|
||||
</h3>
|
||||
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
|
||||
<li>
|
||||
{isZh
|
||||
? '• 仅支持单页应用(SPA):目前只能在单个页面内进行操作'
|
||||
: '• SPA only: Currently operates within a single page'}
|
||||
</li>
|
||||
<li>
|
||||
{isZh
|
||||
? '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务'
|
||||
: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet'}
|
||||
</li>
|
||||
<li>
|
||||
{isZh
|
||||
? '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent'
|
||||
: '• Requires integration: Cannot operate on sites without page-agent'}
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{isZh
|
||||
? 'PageAgent.js 是核心库,运行在页面内。PageAgentExt 是可选的浏览器扩展,提供额外的浏览器级控制能力。'
|
||||
: 'PageAgent.js is the core library running inside a page. PageAgentExt is an optional browser extension that adds browser-level control.'}
|
||||
</p>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="text-left py-3 pr-4"></th>
|
||||
<th className="text-left py-3 px-4 font-semibold">PageAgent.js</th>
|
||||
<th className="text-left py-3 pl-4 font-semibold">
|
||||
PageAgentExt{' '}
|
||||
<Link
|
||||
href="/docs/features/chrome-extension"
|
||||
className="text-xs font-normal text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{isZh ? '了解更多' : 'learn more'}
|
||||
</Link>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600 dark:text-gray-300">
|
||||
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||
{isZh ? '接入方式' : 'Integration'}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{isZh ? '网站开发者主动集成' : 'Site developer integrates the library'}
|
||||
</td>
|
||||
<td className="py-3 pl-4">
|
||||
{isZh ? '用户安装浏览器扩展' : 'User installs a browser extension'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||
{isZh ? '可操作范围' : 'Scope'}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{isZh ? '当前页面(为 SPA 设计)' : 'Current page (designed for SPAs)'}
|
||||
</td>
|
||||
<td className="py-3 pl-4">
|
||||
{isZh ? '任意网页、多标签页' : 'Any web page, multi-tab'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||
{isZh ? '额外能力' : 'Extra capabilities'}
|
||||
</td>
|
||||
<td className="py-3 px-4">—</td>
|
||||
<td className="py-3 pl-4">
|
||||
{isZh ? '新建/切换/关闭标签页' : 'Open / switch / close tabs'}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Interaction Limitations */}
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{isZh ? '交互行为限制' : 'Interaction Limitations'}
|
||||
{isZh ? '交互能力' : 'Interaction Capabilities'}
|
||||
</h2>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
|
||||
<h3 className="font-semibold mb-4">{isZh ? '支持的操作' : 'Supported Operations'}</h3>
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '点击操作' : 'Click'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '文本输入' : 'Text input'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '页面滚动' : 'Scroll'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '表单提交' : 'Form submit'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '选择操作' : 'Select'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
||||
<span className="mr-2">✅</span>
|
||||
<span>{isZh ? '焦点切换' : 'Focus'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-semibold mb-4">{isZh ? '不支持的操作' : 'Unsupported Operations'}</h3>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '鼠标悬停(hover)' : 'Mouse hover'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '拖拽操作' : 'Drag & drop'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '右键菜单' : 'Right-click menu'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '图形绘制' : 'Drawing'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '键盘快捷键' : 'Keyboard shortcuts'}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
||||
<span className="mr-2">❌</span>
|
||||
<span>{isZh ? '基于点击区域或鼠标位置的控制' : 'Position-based control'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{isZh ? '网页理解限制' : 'Understanding Limitations'}
|
||||
</h2>
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6">
|
||||
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
|
||||
{isZh ? '无视觉能力' : 'No Visual Recognition'}
|
||||
</h3>
|
||||
<p className="text-red-700 dark:text-red-300 mb-3">
|
||||
{isZh
|
||||
? 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:'
|
||||
: 'page-agent operates based on DOM structure with no visual recognition. Cannot understand:'}
|
||||
</p>
|
||||
<ul className="text-red-700 dark:text-red-300 space-y-1">
|
||||
<li>
|
||||
{isZh
|
||||
? '• 图片内容:无法识别图片中的文字、图标或视觉元素'
|
||||
: '• Image content: Cannot recognize text, icons, or visual elements in images'}
|
||||
</li>
|
||||
<li>
|
||||
{isZh
|
||||
? '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容'
|
||||
: '• Canvas: Cannot understand graphics drawn on Canvas'}
|
||||
</li>
|
||||
<li>
|
||||
{isZh
|
||||
? '• WebGL 3D 内容:无法操作 3D 场景中的元素'
|
||||
: '• WebGL 3D: Cannot operate elements in 3D scenes'}
|
||||
</li>
|
||||
<li>
|
||||
{isZh
|
||||
? '• SVG 图形:无法理解 SVG 中的视觉内容和路径'
|
||||
: '• SVG graphics: Cannot understand visual content and paths in SVG'}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{isZh ? '被操作网站要求' : 'Website Requirements'}
|
||||
</h2>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
|
||||
<div className="space-y-4">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">
|
||||
{isZh ? '语义化和易用性' : 'Semantic & Usability'}
|
||||
<h3 className="font-semibold mb-3 text-green-700 dark:text-green-400">
|
||||
{isZh ? '支持' : 'Supported'}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{isZh
|
||||
? '所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。'
|
||||
: 'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.'}
|
||||
</p>
|
||||
<ul className="space-y-1.5 text-sm">
|
||||
{[
|
||||
isZh ? '点击、文本输入、选择' : 'Click, text input, select',
|
||||
isZh ? '页面滚动(垂直 / 水平)' : 'Scroll (vertical / horizontal)',
|
||||
isZh ? '表单提交、焦点切换' : 'Form submit, focus',
|
||||
isZh ? '执行 JavaScript(可选)' : 'Execute JavaScript (opt-in)',
|
||||
].map((text) => (
|
||||
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<span className="mr-2 text-green-600 dark:text-green-400">✓</span>
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">{isZh ? 'UI/UX' : 'UI/UX'}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
{isZh
|
||||
? '反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。'
|
||||
: 'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">{isZh ? '环境要求' : 'Environment'}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300">modern browser</p>
|
||||
<h3 className="font-semibold mb-3 text-red-700 dark:text-red-400">
|
||||
{isZh ? '不支持' : 'Not supported'}
|
||||
</h3>
|
||||
<ul className="space-y-1.5 text-sm">
|
||||
{[
|
||||
isZh ? '悬停、拖拽、右键菜单' : 'Hover, drag & drop, right-click',
|
||||
isZh ? '键盘快捷键' : 'Keyboard shortcuts',
|
||||
isZh ? '坐标定位操作' : 'Position-based control',
|
||||
isZh ? '绘图操作' : 'Drawing',
|
||||
].map((text) => (
|
||||
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<span className="mr-2 text-red-600 dark:text-red-400">✗</span>
|
||||
{text}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>{isZh ? '未来规划' : 'Future Plans'}</h2>
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
|
||||
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
{isZh ? '即将支持' : 'Coming Soon'}
|
||||
</h3>
|
||||
<ul className="text-green-700 dark:text-green-300 space-y-1">
|
||||
<li>{isZh ? '• 多页面接力操作能力' : '• Multi-page relay capabilities'}</li>
|
||||
<li>{isZh ? '• 更丰富的鼠标交互支持' : '• Richer mouse interaction support'}</li>
|
||||
<li>{isZh ? '• 基础的视觉理解能力' : '• Basic visual understanding'}</li>
|
||||
<li>{isZh ? '• 更智能的错误恢复机制' : '• Smarter error recovery'}</li>
|
||||
</ul>
|
||||
{/* Understanding Limitations */}
|
||||
<h2 className="text-2xl font-bold mb-3">{isZh ? '理解能力' : 'Understanding'}</h2>
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-400 p-4 mb-6">
|
||||
<p className="text-amber-800 dark:text-amber-200 mb-2 font-medium">
|
||||
{isZh
|
||||
? 'Page Agent 不使用多模态模型,不截图,没有视觉能力。仅通过 DOM 结构理解页面。'
|
||||
: 'Page Agent does not use multimodal models, does not take screenshots, and has no visual capability. It reads pages through DOM structure only.'}
|
||||
</p>
|
||||
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
||||
{isZh
|
||||
? '图片、Canvas、WebGL、SVG 等视觉内容无法被识别。页面的语义化程度和可访问性直接影响 AI 的理解准确性。'
|
||||
: 'Images, Canvas, WebGL, SVG and other visual content cannot be recognized. Page semantic quality and accessibility directly affect AI accuracy.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Website Quality */}
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{isZh ? '网页质量影响' : 'Page Quality Matters'}
|
||||
</h2>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{isZh
|
||||
? '反常识的交互逻辑、纯视觉的操作提示、快速出现消失的元素等都会降低自动化成功率。语义化的 HTML 和良好的可访问性会显著提升效果。'
|
||||
: 'Counter-intuitive interactions, visual-only cues, and rapidly appearing/disappearing elements reduce automation success. Semantic HTML and good accessibility significantly improve results.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
import { useLanguage } from '@/i18n/context'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data: each section is a typed object for easy extension
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface TroubleshootingSection {
|
||||
id: string
|
||||
title: { en: string; zh: string }
|
||||
symptom: { en: string; zh: string }
|
||||
color: 'red' | 'amber' | 'orange' | 'violet'
|
||||
content: (isZh: boolean) => React.ReactNode
|
||||
}
|
||||
|
||||
const SECTIONS: TroubleshootingSection[] = [
|
||||
{
|
||||
id: 'format-errors',
|
||||
title: { en: 'Model Response Format Errors', zh: '模型返回格式错误' },
|
||||
symptom: {
|
||||
en: 'The model returns malformed tool calls, plain text, or unexpected JSON instead of structured actions.',
|
||||
zh: '模型返回了格式错误的 tool call、纯文本或非预期的 JSON,而非结构化的操作指令。',
|
||||
},
|
||||
color: 'amber',
|
||||
content: FormatErrorsContent,
|
||||
},
|
||||
{
|
||||
id: 'low-success-rate',
|
||||
title: { en: 'Low Task Success Rate', zh: '任务成功率低' },
|
||||
symptom: {
|
||||
en: 'The agent appears to understand the task but frequently fails to complete it, or produces incorrect results.',
|
||||
zh: 'Agent 似乎理解了任务,但频繁执行失败或产生不正确的结果。',
|
||||
},
|
||||
color: 'amber',
|
||||
content: LowSuccessRateContent,
|
||||
},
|
||||
{
|
||||
id: 'wrong-element',
|
||||
title: { en: "Can't Hit Target Elements", zh: '无法点击目标元素' },
|
||||
symptom: {
|
||||
en: 'The agent repeatedly retries but keeps interacting with the wrong element, or fails to locate the correct one.',
|
||||
zh: 'Agent 反复重试,但始终点击在错误的元素上,或无法定位到正确的目标元素。',
|
||||
},
|
||||
color: 'amber',
|
||||
content: WrongElementContent,
|
||||
},
|
||||
{
|
||||
id: 'api-errors',
|
||||
title: { en: 'API Request Errors', zh: 'API 请求错误' },
|
||||
symptom: {
|
||||
en: 'HTTP 400 Bad Request or similar errors when calling the LLM API.',
|
||||
zh: '调用 LLM API 时出现 HTTP 400 Bad Request 或类似的参数错误。',
|
||||
},
|
||||
color: 'amber',
|
||||
content: ApiErrorsContent,
|
||||
},
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section content components
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function FormatErrorsContent(isZh: boolean) {
|
||||
return (
|
||||
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
|
||||
<li>
|
||||
<strong>{isZh ? '确认模型是否支持' : 'Verify model compatibility'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '并非所有模型都能正确处理 page-agent 的 tool 定义。请查看'
|
||||
: 'Not all models can handle page-agent tool definitions correctly. Check the '}
|
||||
<Link
|
||||
href="/features/models"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
{isZh ? '已测试模型列表' : 'tested models list'}
|
||||
</Link>
|
||||
{isZh ? '。' : '.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
{isZh ? '检查代理/网关的参数转发' : 'Check proxy/gateway parameter forwarding'}
|
||||
</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '如果使用了 API 代理或网关,请确保请求中的 '
|
||||
: 'If using an API proxy or gateway, make sure the '}
|
||||
<code>tools</code>
|
||||
{isZh
|
||||
? ' 字段被完整、无修改地转发给模型供应商。部分代理可能会剥离或修改此字段。'
|
||||
: ' parameter is forwarded to the model provider intact. Some proxies may strip or alter this field.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '寻求社区帮助' : 'Get community help'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh ? (
|
||||
<>
|
||||
如果以上步骤无法解决问题,欢迎在{' '}
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
GitHub Discussions
|
||||
</a>{' '}
|
||||
中反馈,附上模型名称和错误信息。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
If the above steps don't help, join the{' '}
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
GitHub Discussions
|
||||
</a>{' '}
|
||||
with your model name and error details.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
function LowSuccessRateContent(isZh: boolean) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 italic">
|
||||
{isZh
|
||||
? '按以下顺序逐步排查,从最简单的情况开始:'
|
||||
: 'Follow this diagnostic funnel from simplest to most advanced:'}
|
||||
</p>
|
||||
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
|
||||
<li>
|
||||
<strong>{isZh ? '先从简单指令开始' : 'Start with a simple instruction'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '给一个具体的、单步的简单指令(如"点击登录按钮"),看 Agent 能否完成。如果连简单操作都失败了,问题可能不在模型能力上。'
|
||||
: 'Give a concrete, single-step instruction (e.g. "click the login button"). If even simple actions fail, the issue is likely not model capability.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '尝试最强模型' : 'Try the strongest model available'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '切换到你能获取到的最先进、最大的模型,以排除是否是模型智能水平不足导致的问题。'
|
||||
: "Switch to the most capable model you have access to, to isolate whether it's a model intelligence issue."}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '优化指令质量' : 'Improve instruction quality'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '给出尽可能具体的指令。对于复杂任务,建议使用另一个 LLM 来预先拆分和细化用户的需求,然后逐步执行。'
|
||||
: "Be as specific as possible. For complex tasks, consider using another LLM to decompose and refine the user's request before execution."}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '提供充足的上下文' : 'Provide sufficient context'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '通过 instructions 配置注入网站背景描述、关键术语解释等上下文信息,帮助 Agent 更好地理解页面。'
|
||||
: 'Use the instructions config to inject website descriptions, key terminology, and background context to help the agent understand the page.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '检查 HTML 清洗结果' : 'Check HTML sanitization output'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '使用开发者工具检查清洗后的 HTML,确认关键信息、文本和可操作元素是否被正确保留。'
|
||||
: 'Inspect the sanitized HTML in dev tools to confirm that key information, text, and interactive elements are preserved correctly.'}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function WrongElementContent(isZh: boolean) {
|
||||
return (
|
||||
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
|
||||
<li>
|
||||
<strong>{isZh ? '了解现实局限' : 'Understand the reality'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '并非所有网站都提供了完善的语义化 HTML 和 accessibility 标签。对于此类网站,DOM 清洗可能无法产出足够好的结果。'
|
||||
: 'Not all websites provide proper semantic HTML and accessibility labels. For such sites, DOM sanitization may not produce good enough results.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '检查目标元素类型' : 'Check target element type'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '确认目标元素是否为图片、Canvas、或需要复杂交互(如拖拽、基于坐标的点击)的元素。这些本身就超出了当前的能力范围。'
|
||||
: 'Verify if the target is an image, Canvas, or requires complex interactions (drag-and-drop, coordinate-based clicking). These are beyond current capabilities.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '检查清洗后的 HTML' : 'Inspect sanitized HTML'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '检查清洗结果中是否存在关键信息丢失、可操作元素未被编号等问题。'
|
||||
: 'Look for missing key information or unnumbered interactive elements in the sanitized output.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '注入 accessibility 增强' : 'Inject accessibility improvements'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh
|
||||
? '通过注入脚本为网站添加 aria-label、语义化标签等 accessibility 属性,改善 DOM 清洗质量。'
|
||||
: 'Inject scripts to add aria-labels, semantic attributes, and other a11y improvements to enhance DOM sanitization quality.'}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{isZh ? '开发专用 Tool' : 'Build a custom Tool'}</strong>
|
||||
<p className="mt-1">
|
||||
{isZh ? (
|
||||
<>
|
||||
对于特定的、持续难以操作的元素,考虑开发{' '}
|
||||
<Link
|
||||
href="/features/custom-tools"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
自定义 Tool
|
||||
</Link>{' '}
|
||||
来直接操作这些元素。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
For consistently difficult elements, consider building a{' '}
|
||||
<Link
|
||||
href="/features/custom-tools"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
custom Tool
|
||||
</Link>{' '}
|
||||
to interact with them directly.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
function ApiErrorsContent(isZh: boolean) {
|
||||
return (
|
||||
<div className="space-y-4 text-gray-700 dark:text-gray-300">
|
||||
<p>
|
||||
{isZh
|
||||
? '一些 LLM 供应商使用了与 OpenAI 不完全兼容的参数格式,导致请求参数校验失败。'
|
||||
: 'Some LLM providers use parameter formats that are not fully compatible with the OpenAI spec, causing request validation failures.'}
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4">
|
||||
<p className="font-medium mb-2">
|
||||
{isZh ? '解决方案:使用 customFetch' : 'Solution: use customFetch'}
|
||||
</p>
|
||||
<p className="text-sm mb-3">
|
||||
{isZh
|
||||
? '通过 customFetch 配置拦截请求,在发送前调整参数格式以适配目标供应商的要求。'
|
||||
: 'Use the customFetch config to intercept requests and adapt parameters before sending them to the target provider.'}
|
||||
</p>
|
||||
<CodeEditor
|
||||
code={`const agent = new PageAgent({
|
||||
// ...
|
||||
customFetch: async (url, init) => {
|
||||
const body = JSON.parse(init.body)
|
||||
// Adapt parameters for your provider
|
||||
delete body.stream_options
|
||||
return fetch(url, { ...init, body: JSON.stringify(body) })
|
||||
},
|
||||
})`}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
{isZh ? '参见 ' : 'See '}
|
||||
<Link
|
||||
href="/advanced/page-agent-core"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
PageAgentCore API
|
||||
</Link>
|
||||
{isZh ? ' 了解 customFetch 的完整用法。' : ' for full customFetch documentation.'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Color mapping for symptom callouts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SYMPTOM_COLORS = {
|
||||
red: 'border-red-400 bg-red-50 dark:bg-red-900/15 text-red-800 dark:text-red-200',
|
||||
amber: 'border-amber-400 bg-amber-50 dark:bg-amber-900/15 text-amber-800 dark:text-amber-200',
|
||||
orange:
|
||||
'border-orange-400 bg-orange-50 dark:bg-orange-900/15 text-orange-800 dark:text-orange-200',
|
||||
violet:
|
||||
'border-violet-400 bg-violet-50 dark:bg-violet-900/15 text-violet-800 dark:text-violet-200',
|
||||
} as const
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Right-side TOC with IntersectionObserver
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function useActiveSection(ids: string[]) {
|
||||
const [activeId, setActiveId] = useState(ids[0])
|
||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
observerRef.current?.disconnect()
|
||||
|
||||
const visibleEntries = new Map<string, number>()
|
||||
|
||||
observerRef.current = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
visibleEntries.set(entry.target.id, entry.intersectionRatio)
|
||||
} else {
|
||||
visibleEntries.delete(entry.target.id)
|
||||
}
|
||||
}
|
||||
// Pick the first visible section in document order
|
||||
const firstVisible = ids.find((id) => visibleEntries.has(id))
|
||||
if (firstVisible) setActiveId(firstVisible)
|
||||
},
|
||||
{ rootMargin: '-80px 0px -60% 0px', threshold: [0, 0.25] }
|
||||
)
|
||||
|
||||
for (const id of ids) {
|
||||
const el = document.getElementById(id)
|
||||
if (el) observerRef.current.observe(el)
|
||||
}
|
||||
|
||||
return () => observerRef.current?.disconnect()
|
||||
}, [ids])
|
||||
|
||||
return activeId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function TroubleshootingPage() {
|
||||
const { isZh } = useLanguage()
|
||||
const sectionIds = SECTIONS.map((s) => s.id)
|
||||
const activeId = useActiveSection(sectionIds)
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">Troubleshooting</h1>
|
||||
</div>
|
||||
|
||||
{/* Two-column: content + TOC */}
|
||||
<div className="flex gap-8">
|
||||
{/* Main content */}
|
||||
<div className="flex-1 min-w-0 space-y-12">
|
||||
{SECTIONS.map((section) => (
|
||||
<section key={section.id} id={section.id} className="scroll-mt-24">
|
||||
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">
|
||||
{isZh ? section.title.zh : section.title.en}
|
||||
</h2>
|
||||
|
||||
{/* Symptom callout */}
|
||||
<div
|
||||
className={`border-l-4 px-4 py-3 rounded-r-lg mb-6 ${SYMPTOM_COLORS[section.color]}`}
|
||||
>
|
||||
<span className="text-xs font-semibold uppercase tracking-wider opacity-70">
|
||||
{isZh ? '症状' : 'Symptom'}
|
||||
</span>
|
||||
<p className="mt-1 text-sm">{isZh ? section.symptom.zh : section.symptom.en}</p>
|
||||
</div>
|
||||
|
||||
{/* Diagnostic steps */}
|
||||
<div className="prose-sm">{section.content(isZh)}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right TOC */}
|
||||
<aside className="hidden lg:block w-48 shrink-0">
|
||||
<div className="sticky top-24">
|
||||
<h4 className="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3">
|
||||
{isZh ? '目录' : 'On this page'}
|
||||
</h4>
|
||||
<nav className="space-y-1">
|
||||
{SECTIONS.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
document
|
||||
.getElementById(section.id)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
className={`block text-sm py-1 text-left transition-colors ${
|
||||
activeId === section.id
|
||||
? 'text-blue-600 dark:text-blue-400 font-medium'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{isZh ? section.title.zh : section.title.en}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -28,20 +28,5 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
|
||||
// "paths": {
|
||||
// // Simplified monorepo solution (raw npm workspace with hoisting)
|
||||
// "@page-agent/page-controller": ["./packages/page-controller/src/PageController.ts"],
|
||||
// "page-agent": ["./packages/page-agent/src/PageAgent.ts"]
|
||||
// }
|
||||
}
|
||||
// "references": [
|
||||
// { "path": "./packages/page-controller" },
|
||||
// { "path": "./packages/page-agent" },
|
||||
// { "path": "./packages/website" }
|
||||
// ],
|
||||
// "include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
|
||||
// "exclude": ["node_modules", "dist", "packages/*/dist"]
|
||||
// "files": ["env.d.ts"]
|
||||
// "files": []
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// this is only for IDE ts language server to work.
|
||||
// do not use this for building or linting.
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"references": [
|
||||
|
||||
Reference in New Issue
Block a user