Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e7851849f | |||
| dffcb53db9 | |||
| 4dc332a32c | |||
| f19b3cc2cc | |||
| 11d66f42c4 | |||
| 20e947a1c1 | |||
| 7c87c90258 | |||
| fcb9ec4e57 | |||
| 1fcf931685 | |||
| c71ffcec1c | |||
| 430be466fc | |||
| 3f284cf2e6 | |||
| 6f0655478a | |||
| 7d5449967b | |||
| fd47888d1b | |||
| 10f470ce7d | |||
| cb29db1cc4 | |||
| 8c08f639dd | |||
| 488f080e68 | |||
| 6e07af422b | |||
| e9d721f6b9 | |||
| 840162df8c | |||
| 1517bc4868 |
Vendored
+3
-2
@@ -18,7 +18,7 @@
|
|||||||
"wouter"
|
"wouter"
|
||||||
],
|
],
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"packages/*/node_modules": true,
|
"packages/*/node_modules": true
|
||||||
},
|
},
|
||||||
"markdownlint.config": {
|
"markdownlint.config": {
|
||||||
// "comment": "Relaxed rules",
|
// "comment": "Relaxed rules",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"first-line-h1": false,
|
"first-line-h1": false,
|
||||||
"block-spacing": false,
|
"block-spacing": false,
|
||||||
"blanks-around-lists": 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 # Build all packages
|
||||||
npm run build:libs # Build all libraries
|
npm run build:libs # Build all libraries
|
||||||
npm run lint # ESLint with TypeScript strict rules
|
npm run lint # ESLint with TypeScript strict rules
|
||||||
|
npm run zip -w @page-agent/ext # Zip the extension package
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -36,7 +37,7 @@ packages/
|
|||||||
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
||||||
├── website/ # @page-agent/website (private)
|
├── website/ # @page-agent/website (private)
|
||||||
├── llms/ # @page-agent/llms
|
├── llms/ # @page-agent/llms
|
||||||
├── extension/ # 🚧 WIP: Browser extension (WXT + React)
|
├── extension/ # Browser extension (WXT + React)
|
||||||
├── page-controller/ # @page-agent/page-controller
|
├── page-controller/ # @page-agent/page-controller
|
||||||
└── ui/ # @page-agent/ui
|
└── 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/),
|
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).
|
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
|
## [1.0.0] - 2026-01-19
|
||||||
|
|
||||||
### 🎉 First Stable Release
|
### 🎉 First Stable Release
|
||||||
|
|||||||
+12
-9
@@ -20,10 +20,11 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
|
|||||||
|
|
||||||
### Project Structure
|
### 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
|
- **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`)
|
- **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`
|
- **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.
|
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
|
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
|
### Testing on Other Websites
|
||||||
|
|
||||||
- Start and serve a local `iife` script
|
- 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.
|
> 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?
|
## 💬 Questions?
|
||||||
|
|
||||||
- Open a GitHub issue for technical questions
|
- Open a GitHub issue for technical questions
|
||||||
|
|||||||
+22
-27
@@ -1,4 +1,4 @@
|
|||||||
# PageAgent 🤖🪄
|
# Page Agent
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||||
@@ -20,17 +20,15 @@
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **🎯 轻松集成**
|
- **🎯 轻松集成**
|
||||||
- 无需 Python,无需无头浏览器,无需浏览器插件。纯页面内脚本。
|
- 无需 `浏览器插件` / `Python` / `无头浏览器`。
|
||||||
- **🔐 端侧运行**
|
- 纯页面内 JavaScript,一切都在你的网页中完成。
|
||||||
- **🧠 HTML 脱水**
|
- The best tool for your agent to control web pages.
|
||||||
- **💬 自然语言接口**
|
- **📖 基于文本的 DOM 操作**
|
||||||
- **🎨 HITL 交互界面**
|
- 无需截图,无需 OCR 或多模态模型。
|
||||||
|
- 无需特殊权限。
|
||||||
以及 😉
|
- **🧠 用你自己的 LLM**
|
||||||
|
- **🎨 精美 UI,支持人机协同**
|
||||||
- **🧪 实验性的 Chrome 扩展,支持跨页面控制** - `packages/extension`
|
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension),支持跨页面任务。**
|
||||||
|
|
||||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -39,19 +37,15 @@
|
|||||||
通过我们免费的 Demo LLM 快速体验 PageAgent:
|
通过我们免费的 Demo LLM 快速体验 PageAgent:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script
|
<script src="{URL}" crossorigin="true"></script>
|
||||||
src="https://registry.npmmirror.com/page-agent/1.1.2/files/dist/iife/page-agent.demo.js"
|
|
||||||
crossorigin="true"
|
|
||||||
></script>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> - **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,可能随时变更。
|
|
||||||
> - **🌷 建议使用自己的 LLM API。**
|
|
||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.1.2/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.1.2/files/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 安装
|
### NPM 安装
|
||||||
|
|
||||||
@@ -72,7 +66,7 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('点击登录按钮')
|
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/
|
packages/
|
||||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
├── core/ # ** Core agent logic (npm: @page-agent/core) **
|
||||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
|
||||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
||||||
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
|
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
|
||||||
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
|
├── ui/ # 面板 UI (npm: @page-agent/ui)
|
||||||
└── website/ # 文档站点
|
├── page-agent/ # 入口类 & iife 包 (npm: page-agent)
|
||||||
|
├── extension/ # Chrome 扩展,支持跨页面任务
|
||||||
|
└── website/ # 网站 & 文档站点
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 贡献
|
## 🤝 贡献
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# PageAgent 🤖🪄
|
# Page Agent
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
<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
|
## ✨ Features
|
||||||
|
|
||||||
- **🎯 Easy Integration**
|
- **🎯 Easy integration**
|
||||||
- No python. No headless browser. No browser extension. Just in-page scripts.
|
- No need for `browser extension` / `python` / `headless browser`.
|
||||||
- **🔐 Client-Side Processing**
|
- Just in-page javascript. Everything happens in your web page.
|
||||||
- **🧠 DOM Extraction**
|
- The best tool for your agent to control web pages.
|
||||||
- **💬 Natural Language Interface**
|
- **📖 Text-based DOM manipulation**
|
||||||
- **🎨 UI with Human in the loop**
|
- No screenshots. No OCR or multi-modal LLMs needed.
|
||||||
|
- No special permissions required.
|
||||||
And 😉
|
- **🧠 Bring your own LLMs**
|
||||||
|
- **🎨 Pretty UI with human-in-the-loop**
|
||||||
- **🧪 `cross-page` control with an experimental chrome extension** - `packages/extension`
|
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension) for multi-page tasks.**
|
||||||
|
|
||||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -39,19 +37,15 @@ And 😉
|
|||||||
Fastest way to try PageAgent with our free Demo LLM:
|
Fastest way to try PageAgent with our free Demo LLM:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script
|
<script src="{URL}" crossorigin="true"></script>
|
||||||
src="https://cdn.jsdelivr.net/npm/page-agent@1.1.2/dist/iife/page-agent.demo.js"
|
|
||||||
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 |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.1.2/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.1.2/files/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
|
### NPM Installation
|
||||||
|
|
||||||
@@ -72,18 +66,21 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('Click the login button')
|
await agent.execute('Click the login button')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/#/docs/introduction/overview).
|
||||||
|
|
||||||
## 🏗️ Structure
|
## 🏗️ Structure
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
PageAgent adopts a simplified monorepo structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
├── core/ # ** Core agent logic (npm: @page-agent/core) **
|
||||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
|
||||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
├── llms/ # LLM client (npm: @page-agent/llms)
|
||||||
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
|
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||||
├── ui/ # Panel & i18n (npm: @page-agent/ui)
|
├── ui/ # Panel UI (npm: @page-agent/ui)
|
||||||
└── website/ # Demo & Documentation site
|
├── page-agent/ # Entry class and iife builds(npm: page-agent)
|
||||||
|
├── extension/ # Chrome extension for multi-page tasks
|
||||||
|
└── website/ # Website & Documentation site
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|||||||
Generated
+376
-376
File diff suppressed because it is too large
Load Diff
+10
-9
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0",
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
"npm": ">=10.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf packages/*/dist",
|
"cleanup": "rm -rf packages/*/dist",
|
||||||
@@ -39,24 +38,26 @@
|
|||||||
"@commitlint/cli": "^20.3.1",
|
"@commitlint/cli": "^20.3.1",
|
||||||
"@commitlint/config-conventional": "^20.3.1",
|
"@commitlint/config-conventional": "^20.3.1",
|
||||||
"@eslint/js": "^9.39.2",
|
"@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",
|
"@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",
|
"chalk": "^5.6.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.4",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"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-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"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",
|
"globals": "^17.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.2.4",
|
"lint-staged": "^16.2.4",
|
||||||
"prettier": "^3.8.0",
|
"prettier": "^3.8.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.54.0",
|
"typescript-eslint": "^8.55.0",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent-core.js",
|
"main": "./dist/esm/page-agent-core.js",
|
||||||
"module": "./dist/esm/page-agent-core.js",
|
"module": "./dist/esm/page-agent-core.js",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
"@page-agent/llms": "1.1.2",
|
"@page-agent/llms": "1.3.0",
|
||||||
"@page-agent/page-controller": "1.1.2"
|
"@page-agent/page-controller": "1.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*/
|
*/
|
||||||
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
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 chalk from 'chalk'
|
||||||
import * as zod from 'zod'
|
import * as zod from 'zod'
|
||||||
|
|
||||||
@@ -21,16 +21,25 @@ import type {
|
|||||||
MacroToolInput,
|
MacroToolInput,
|
||||||
MacroToolResult,
|
MacroToolResult,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { assert, normalizeResponse, trimLines, uid, waitFor } from './utils'
|
import { assert, normalizeResponse, uid, waitFor } from './utils'
|
||||||
|
|
||||||
export { type PageAgentConfig }
|
export { type PageAgentConfig }
|
||||||
export { tool, type PageAgentTool } from './tools'
|
export { tool, type PageAgentTool } from './tools'
|
||||||
export type * from './types'
|
export type * from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI agent for browser DOM automation.
|
* AI agent for browser automation.
|
||||||
*
|
*
|
||||||
* @remarks
|
* @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
|
* ## Event System
|
||||||
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
* - `statuschange` - Agent status transitions (idle → running → completed/error)
|
||||||
* - `historychange` - History events updated (persistent, part of agent memory)
|
* - `historychange` - History events updated (persistent, part of agent memory)
|
||||||
@@ -49,14 +58,18 @@ export type * from './types'
|
|||||||
* - Types: thinking, executing, executed, retrying, error
|
* - Types: thinking, executing, executed, retrying, error
|
||||||
*/
|
*/
|
||||||
export class PageAgentCore extends EventTarget {
|
export class PageAgentCore extends EventTarget {
|
||||||
config: PageAgentConfig & { maxSteps: number }
|
readonly id = uid()
|
||||||
id = uid()
|
readonly config: PageAgentConfig & { maxSteps: number }
|
||||||
tools: typeof tools
|
readonly tools: typeof tools
|
||||||
|
/** PageController for DOM operations */
|
||||||
|
readonly pageController: PageController
|
||||||
|
|
||||||
task = ''
|
task = ''
|
||||||
taskId = ''
|
taskId = ''
|
||||||
|
/** History events */
|
||||||
/** Agent execution status */
|
history: HistoricalEvent[] = []
|
||||||
#status: AgentStatus = 'idle'
|
/** Whether this agent has been disposed */
|
||||||
|
disposed = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for when agent needs user input (ask_user tool)
|
* Callback for when agent needs user input (ask_user tool)
|
||||||
@@ -65,23 +78,21 @@ export class PageAgentCore extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
onAskUser?: (question: string) => Promise<string>
|
onAskUser?: (question: string) => Promise<string>
|
||||||
|
|
||||||
|
#status: AgentStatus = 'idle'
|
||||||
#llm: LLM
|
#llm: LLM
|
||||||
#abortController = new AbortController()
|
#abortController = new AbortController()
|
||||||
|
#observations: string[] = []
|
||||||
|
|
||||||
/** PageController for DOM operations */
|
/** internal states during a single task execution */
|
||||||
pageController: PageController
|
#states = {
|
||||||
|
/** Accumulated wait time in seconds */
|
||||||
/** Runtime states for tracking across steps */
|
|
||||||
states = {
|
|
||||||
/** Accumulated wait time in seconds, used by wait tool */
|
|
||||||
totalWaitTime: 0,
|
totalWaitTime: 0,
|
||||||
/** Last known URL for detecting navigation */
|
/** For detecting navigation */
|
||||||
lastURL: '',
|
lastURL: '',
|
||||||
|
/** Browser state */
|
||||||
|
browserState: null as BrowserState | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** History events */
|
|
||||||
history: HistoricalEvent[] = []
|
|
||||||
|
|
||||||
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -165,15 +176,24 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push a persistent observation to the history event stream.
|
* Push a observation message to the history event stream.
|
||||||
* This will be visible in <agent_history> and remain in memory across steps.
|
* 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 {
|
pushObservation(content: string): void {
|
||||||
this.history.push({ type: 'observation', content })
|
this.#observations.push(content)
|
||||||
this.#emitHistoryChange()
|
}
|
||||||
|
|
||||||
|
/** Stop the current task. Agent remains reusable. */
|
||||||
|
stop() {
|
||||||
|
this.pageController.cleanUpHighlights()
|
||||||
|
this.pageController.hideMask()
|
||||||
|
this.#abortController.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(task: string): Promise<ExecutionResult> {
|
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')
|
if (!task) throw new Error('Task is required')
|
||||||
this.task = task
|
this.task = task
|
||||||
this.taskId = uid()
|
this.taskId = uid()
|
||||||
@@ -202,11 +222,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
this.#setStatus('running')
|
this.#setStatus('running')
|
||||||
this.#emitHistoryChange()
|
this.#emitHistoryChange()
|
||||||
|
|
||||||
// Reset states
|
// Reset internal states
|
||||||
this.states = {
|
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
|
||||||
totalWaitTime: 0,
|
|
||||||
lastURL: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
let step = 0
|
let step = 0
|
||||||
|
|
||||||
@@ -214,18 +231,16 @@ export class PageAgentCore extends EventTarget {
|
|||||||
try {
|
try {
|
||||||
console.group(`step: ${step}`)
|
console.group(`step: ${step}`)
|
||||||
|
|
||||||
await this.#generateObservations(step)
|
|
||||||
|
|
||||||
await onBeforeStep?.(this, step)
|
await onBeforeStep?.(this, step)
|
||||||
|
|
||||||
// abort
|
// observe
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
||||||
|
|
||||||
// status
|
console.log(chalk.blue.bold('👀 Observing...'))
|
||||||
console.log(chalk.blue('Thinking...'))
|
|
||||||
this.#emitActivity({ type: 'thinking' })
|
|
||||||
|
|
||||||
// invoke LLM
|
this.#states.browserState = await this.pageController.getBrowserState()
|
||||||
|
await this.#handleObservations(step)
|
||||||
|
|
||||||
|
// assemble prompts
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
||||||
@@ -234,12 +249,17 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
const tools = { AgentOutput: this.#packMacroTool() }
|
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, {
|
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
||||||
toolChoiceName: 'AgentOutput',
|
toolChoiceName: 'AgentOutput',
|
||||||
normalizeResponse,
|
normalizeResponse,
|
||||||
})
|
})
|
||||||
|
|
||||||
// assemble history event
|
// assemble history
|
||||||
|
|
||||||
const macroResult = result.toolResult as MacroToolResult
|
const macroResult = result.toolResult as MacroToolResult
|
||||||
const input = macroResult.input
|
const input = macroResult.input
|
||||||
@@ -271,7 +291,6 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
await onAfterStep?.(this, this.history)
|
await onAfterStep?.(this, this.history)
|
||||||
|
|
||||||
console.log(chalk.green('Step finished:'), actionName)
|
|
||||||
console.groupEnd()
|
console.groupEnd()
|
||||||
|
|
||||||
// finish task if done
|
// finish task if done
|
||||||
@@ -291,9 +310,13 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.groupEnd() // to prevent nested groups
|
console.groupEnd() // to prevent nested groups
|
||||||
|
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
|
||||||
|
|
||||||
console.error('Task failed', error)
|
console.error('Task failed', error)
|
||||||
const errorMessage = String(error)
|
const errorMessage = isAbortError ? 'Task stopped' : String(error)
|
||||||
this.#emitActivity({ type: 'error', message: errorMessage })
|
this.#emitActivity({ type: 'error', message: errorMessage })
|
||||||
|
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
|
||||||
|
this.#emitHistoryChange()
|
||||||
this.#onDone(false)
|
this.#onDone(false)
|
||||||
const result: ExecutionResult = {
|
const result: ExecutionResult = {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -306,10 +329,13 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
step++
|
step++
|
||||||
if (step > this.config.maxSteps) {
|
if (step > this.config.maxSteps) {
|
||||||
|
const errorMessage = 'Step count exceeded maximum limit'
|
||||||
|
this.history.push({ type: 'error', message: errorMessage })
|
||||||
|
this.#emitHistoryChange()
|
||||||
this.#onDone(false)
|
this.#onDone(false)
|
||||||
const result: ExecutionResult = {
|
const result: ExecutionResult = {
|
||||||
success: false,
|
success: false,
|
||||||
data: 'Step count exceeded maximum limit',
|
data: errorMessage,
|
||||||
history: this.history,
|
history: this.history,
|
||||||
}
|
}
|
||||||
await onAfterTask?.(this, result)
|
await onAfterTask?.(this, result)
|
||||||
@@ -347,13 +373,13 @@ export class PageAgentCore extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
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>,
|
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
||||||
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
||||||
// abort
|
// abort
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
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 action = input.action
|
||||||
|
|
||||||
const toolName = Object.keys(action)[0]
|
const toolName = Object.keys(action)[0]
|
||||||
@@ -374,7 +400,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
// Find the corresponding tool
|
// Find the corresponding tool
|
||||||
const tool = tools.get(toolName)
|
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)
|
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
||||||
|
|
||||||
@@ -398,9 +424,11 @@ export class PageAgentCore extends EventTarget {
|
|||||||
duration,
|
duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset wait time for non-wait tools
|
// counting wait time
|
||||||
if (toolName !== 'wait') {
|
if (toolName === 'wait') {
|
||||||
this.states.totalWaitTime = 0
|
this.#states.totalWaitTime += toolInput?.seconds || 0
|
||||||
|
} else {
|
||||||
|
this.#states.totalWaitTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return structured result
|
// Return structured result
|
||||||
@@ -430,17 +458,17 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instructions from config and format as XML block
|
* Get instructions from config
|
||||||
*/
|
*/
|
||||||
async #getInstructions(): Promise<string> {
|
async #getInstructions(): Promise<string> {
|
||||||
const { instructions } = this.config
|
const { instructions } = this.config
|
||||||
if (!instructions) return ''
|
if (!instructions) return ''
|
||||||
|
|
||||||
const systemInstructions = instructions.system?.trim()
|
const systemInstructions = instructions.system?.trim()
|
||||||
const url = await this.pageController.getCurrentUrl()
|
|
||||||
let pageInstructions: string | undefined
|
let pageInstructions: string | undefined
|
||||||
|
|
||||||
if (instructions.getPageInstructions) {
|
const url = this.#states.browserState?.url || ''
|
||||||
|
if (instructions.getPageInstructions && url) {
|
||||||
try {
|
try {
|
||||||
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -468,23 +496,28 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate observations before each step
|
* Generate system observations before each step
|
||||||
* - URL change detection
|
|
||||||
* - Too many steps warning
|
|
||||||
* @todo loop detection
|
* @todo loop detection
|
||||||
* @todo console error
|
* @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
|
// Detect URL change
|
||||||
const currentURL = await this.pageController.getCurrentUrl()
|
const currentURL = this.#states.browserState?.url || ''
|
||||||
if (currentURL !== this.states.lastURL) {
|
if (currentURL !== this.#states.lastURL) {
|
||||||
this.pushObservation(`Page navigated to → ${currentURL}`)
|
this.pushObservation(`Page navigated to → ${currentURL}`)
|
||||||
this.states.lastURL = currentURL
|
this.#states.lastURL = currentURL
|
||||||
await waitFor(0.5) // wait for page to stabilize
|
await waitFor(0.5) // wait for page to stabilize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn about remaining steps
|
// Remaining steps warning
|
||||||
const remaining = this.config.maxSteps - stepCount
|
const remaining = this.config.maxSteps - step
|
||||||
if (remaining === 5) {
|
if (remaining === 5) {
|
||||||
this.pushObservation(
|
this.pushObservation(
|
||||||
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
||||||
@@ -494,12 +527,25 @@ export class PageAgentCore extends EventTarget {
|
|||||||
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
|
`⚠️ 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> {
|
async #assembleUserPrompt(): Promise<string> {
|
||||||
|
const browserState = this.#states.browserState!
|
||||||
|
|
||||||
let prompt = ''
|
let prompt = ''
|
||||||
|
|
||||||
// <instructions> (optional)
|
// <instructions> (optional)
|
||||||
|
|
||||||
prompt += await this.#getInstructions()
|
prompt += await this.#getInstructions()
|
||||||
|
|
||||||
// <agent_state>
|
// <agent_state>
|
||||||
@@ -509,38 +555,36 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
const stepCount = this.history.filter((e) => e.type === 'step').length
|
const stepCount = this.history.filter((e) => e.type === 'step').length
|
||||||
|
|
||||||
prompt += `<agent_state>
|
prompt += '<agent_state>\n'
|
||||||
<user_request>
|
prompt += '<user_request>\n'
|
||||||
${this.task}
|
prompt += `${this.task}\n`
|
||||||
</user_request>
|
prompt += '</user_request>\n'
|
||||||
<step_info>
|
prompt += '<step_info>\n'
|
||||||
Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
|
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`
|
||||||
Current date and time: ${new Date().toLocaleString()}
|
prompt += `Current time: ${new Date().toLocaleString()}\n`
|
||||||
</step_info>
|
prompt += '</step_info>\n'
|
||||||
</agent_state>
|
prompt += '</agent_state>\n\n'
|
||||||
`
|
|
||||||
|
|
||||||
// <agent_history>
|
// <agent_history>
|
||||||
// - <step_N> for steps
|
// - <step_N> for steps
|
||||||
// - <sys> for observations and system messages
|
// - <sys> for observations and system messages
|
||||||
|
|
||||||
prompt += '\n<agent_history>\n'
|
prompt += '<agent_history>\n'
|
||||||
|
|
||||||
let stepIndex = 0
|
let stepIndex = 0
|
||||||
for (const event of this.history) {
|
for (const event of this.history) {
|
||||||
if (event.type === 'step') {
|
if (event.type === 'step') {
|
||||||
stepIndex++
|
stepIndex++
|
||||||
prompt += `<step_${stepIndex}>
|
prompt += `<step_${stepIndex}>\n`
|
||||||
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
|
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`
|
||||||
Memory: ${event.reflection.memory}
|
prompt += `Memory: ${event.reflection.memory}\n`
|
||||||
Next Goal: ${event.reflection.next_goal}
|
prompt += `Next Goal: ${event.reflection.next_goal}\n`
|
||||||
Action Results: ${event.action.output}
|
prompt += `Action Results: ${event.action.output}\n`
|
||||||
</step_${stepIndex}>
|
prompt += `</step_${stepIndex}>\n`
|
||||||
`
|
|
||||||
} else if (event.type === 'observation') {
|
} else if (event.type === 'observation') {
|
||||||
prompt += `<sys>${event.content}</sys>\n`
|
prompt += `<sys>${event.content}</sys>\n`
|
||||||
} else if (event.type === 'user_takeover') {
|
} 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') {
|
} else if (event.type === 'error') {
|
||||||
// Error events are mainly for panel rendering, not included in LLM context
|
// Error events are mainly for panel rendering, not included in LLM context
|
||||||
// to avoid polluting the agent's reasoning with transient errors
|
// to avoid polluting the agent's reasoning with transient errors
|
||||||
@@ -551,9 +595,18 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
// <browser_state>
|
// <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) {
|
#onDone(success = true) {
|
||||||
@@ -563,25 +616,9 @@ export class PageAgentCore extends EventTarget {
|
|||||||
this.#abortController.abort()
|
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() {
|
dispose() {
|
||||||
console.log('Disposing PageAgent...')
|
console.log('Disposing PageAgent...')
|
||||||
|
this.disposed = true
|
||||||
this.pageController.dispose()
|
this.pageController.dispose()
|
||||||
// this.history = []
|
// this.history = []
|
||||||
this.#abortController.abort()
|
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.
|
Here are examples of good output patterns. Use them as reference but never copy them directly.
|
||||||
|
|
||||||
<evaluation_examples>
|
<evaluation_examples>
|
||||||
- Positive Examples:
|
|
||||||
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
|
"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_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
|
||||||
</evaluation_examples>
|
</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_examples>
|
||||||
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
|
"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>
|
</next_goal_examples>
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,13 @@ tools.set(
|
|||||||
'done',
|
'done',
|
||||||
tool({
|
tool({
|
||||||
description:
|
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({
|
inputSchema: zod.object({
|
||||||
text: zod.string(),
|
text: zod.string(),
|
||||||
success: zod.boolean().default(true),
|
success: zod.boolean().default(true),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
// @note main loop will handle this one
|
// @note main loop will handle this one
|
||||||
// this.onDone(input.text, input.success)
|
|
||||||
return Promise.resolve('Task completed')
|
return Promise.resolve('Task completed')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -47,25 +46,17 @@ tools.set(
|
|||||||
tools.set(
|
tools.set(
|
||||||
'wait',
|
'wait',
|
||||||
tool({
|
tool({
|
||||||
description:
|
description: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',
|
||||||
'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.',
|
|
||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
seconds: zod.number().min(1).max(10).default(1),
|
seconds: zod.number().min(1).max(10).default(1),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
|
// try to subtract LLM calling time from the actual wait time
|
||||||
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
||||||
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
||||||
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
||||||
await waitFor(actualWaitTime)
|
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.`
|
return `✅ Waited for ${input.seconds} seconds.`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -106,7 +97,7 @@ tools.set(
|
|||||||
tools.set(
|
tools.set(
|
||||||
'input_text',
|
'input_text',
|
||||||
tool({
|
tool({
|
||||||
description: 'Click and input text into a input interactive element',
|
description: 'Click and type text into an interactive input element',
|
||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
index: zod.int().min(0),
|
index: zod.int().min(0),
|
||||||
text: zod.string(),
|
text: zod.string(),
|
||||||
@@ -140,8 +131,7 @@ tools.set(
|
|||||||
tools.set(
|
tools.set(
|
||||||
'scroll',
|
'scroll',
|
||||||
tool({
|
tool({
|
||||||
description:
|
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
||||||
'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.',
|
|
||||||
inputSchema: zod.object({
|
inputSchema: zod.object({
|
||||||
down: zod.boolean().default(true),
|
down: zod.boolean().default(true),
|
||||||
num_pages: zod.number().min(0).max(10).optional().default(0.1),
|
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(
|
tools.set(
|
||||||
'scroll_horizontally',
|
'scroll_horizontally',
|
||||||
tool({
|
tool({
|
||||||
description:
|
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({
|
inputSchema: zod.object({
|
||||||
right: zod.boolean().default(true),
|
right: zod.boolean().default(true),
|
||||||
pixels: zod.number().int().min(0),
|
pixels: zod.number().int().min(0),
|
||||||
@@ -190,8 +183,6 @@ tools.set(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// @todo get_dropdown_options
|
|
||||||
// @todo select_dropdown_option
|
|
||||||
// @todo send_keys
|
// @todo send_keys
|
||||||
// @todo upload_file
|
// @todo upload_file
|
||||||
// @todo go_back
|
// @todo go_back
|
||||||
|
|||||||
@@ -2,28 +2,6 @@ import chalk from 'chalk'
|
|||||||
|
|
||||||
export { normalizeResponse } from './autoFixer'
|
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> {
|
export async function waitFor(seconds: number): Promise<void> {
|
||||||
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
||||||
}
|
}
|
||||||
@@ -39,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 {
|
export function randomID(existingIDs?: string[]): string {
|
||||||
let id = Math.random().toString(36).substring(2, 11)
|
let id = Math.random().toString(36).substring(2, 11)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
# Page Agent Extension API
|
# 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
|
## Installation
|
||||||
|
|
||||||
### 1. Install the browser extension
|
### 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)
|
### 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
|
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:
|
2. Set the token in your page:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -60,36 +74,36 @@ if (await waitForExtension()) {
|
|||||||
|
|
||||||
## Global API
|
## 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`
|
### `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`
|
### `window.PAGE_AGENT_EXT`
|
||||||
|
|
||||||
Main API namespace object containing:
|
Main namespace object.
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.execute(task, config)`
|
#### `PAGE_AGENT_EXT.execute(task, config)`
|
||||||
|
|
||||||
Execute an agent task.
|
Execute one agent task.
|
||||||
|
|
||||||
**Parameters:**
|
Parameters:
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
| Name | Type | Required | Description |
|
||||||
|------|------|----------|-------------|
|
| ---- | ---- | -------- | ----------- |
|
||||||
| `task` | `string` | Yes | Task 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
|
## Types
|
||||||
|
|
||||||
Install `@page-agent/core` for full type definitions:
|
Install `@page-agent/core` for complete types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type {
|
||||||
@@ -104,35 +118,24 @@ export interface ExecuteConfig {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
model: string
|
model: string
|
||||||
|
|
||||||
/**
|
// Include the initial tab where page JS starts. Default: true.
|
||||||
* Whether to include the initial tab (that holds this main world script) in the task.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
onDispose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
```
|
```
|
||||||
|
|
||||||
### AgentStatus
|
`AgentStatus`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
||||||
```
|
```
|
||||||
|
|
||||||
| Status | Description |
|
`AgentActivity`
|
||||||
|--------|-------------|
|
|
||||||
| `idle` | Agent is idle, ready to execute |
|
|
||||||
| `running` | Agent is executing a task |
|
|
||||||
| `completed` | Task completed successfully |
|
|
||||||
| `error` | Task failed with an error |
|
|
||||||
|
|
||||||
### AgentActivity
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentActivity =
|
type AgentActivity =
|
||||||
@@ -143,15 +146,7 @@ type AgentActivity =
|
|||||||
| { type: 'error'; message: string }
|
| { type: 'error'; message: string }
|
||||||
```
|
```
|
||||||
|
|
||||||
| Type | Description |
|
`HistoricalEvent`
|
||||||
|------|-------------|
|
|
||||||
| `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
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type HistoricalEvent =
|
type HistoricalEvent =
|
||||||
@@ -162,7 +157,7 @@ type HistoricalEvent =
|
|||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||||
```
|
```
|
||||||
|
|
||||||
### ExecutionResult
|
`ExecutionResult`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ExecutionResult {
|
interface ExecutionResult {
|
||||||
@@ -183,81 +178,22 @@ const result = await window.PAGE_AGENT_EXT!.execute(
|
|||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
}
|
includeInitialTab: false, // Optional: exclude current tab
|
||||||
)
|
onStatusChange: (status) => console.log(status),
|
||||||
|
onActivity: (activity) => console.log(activity),
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Event Callbacks
|
### Stop the Current Task
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await window.PAGE_AGENT_EXT!.execute('Navigate to the settings page', {
|
window.PAGE_AGENT_EXT!.stop()
|
||||||
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 Type Declaration
|
## Window Type Declaration
|
||||||
|
|
||||||
If not using `@page-agent/core`, add this to your project:
|
If you are not importing `@page-agent/core`, add:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type {
|
||||||
@@ -275,7 +211,6 @@ interface ExecuteConfig {
|
|||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
onDispose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -283,8 +218,8 @@ declare global {
|
|||||||
PAGE_AGENT_EXT_VERSION?: string
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
PAGE_AGENT_EXT?: {
|
PAGE_AGENT_EXT?: {
|
||||||
version: string
|
version: string
|
||||||
execute: (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
execute: Execute
|
||||||
dispose: () => void
|
stop: () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
# Page Agent 浏览器插件 API
|
# Page Agent 浏览器插件 API
|
||||||
|
|
||||||
本文档介绍如何在网页应用中接入 Page Agent 浏览器插件。
|
在你的网页应用中接入 Page Agent 插件,并通过页面 JavaScript 发起多页面浏览器任务。
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
### 1. 安装浏览器插件
|
### 1. 安装浏览器插件
|
||||||
|
|
||||||
从 Chrome 应用商店安装 Page Agent 插件。
|
首选渠道:
|
||||||
|
|
||||||
|
- Chrome 应用商店:https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
|
||||||
|
|
||||||
|
通常更快提供最新构建的渠道:
|
||||||
|
|
||||||
|
- GitHub Releases:https://github.com/alibaba/page-agent/releases
|
||||||
|
|
||||||
### 2. 安装类型定义(推荐)
|
### 2. 安装类型定义(推荐)
|
||||||
|
|
||||||
@@ -14,11 +20,19 @@
|
|||||||
npm install @page-agent/core --save-dev
|
npm install @page-agent/core --save-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置认证
|
### 3. 授权(Token)
|
||||||
|
|
||||||
插件在页面加载后检测 `localStorage` 中的 token,匹配时才会注入 API。
|
token 用于让页面 JS 调用扩展 API(`window.PAGE_AGENT_EXT`)并执行多页面任务。
|
||||||
|
|
||||||
1. 打开插件的侧边栏面板,获取授权 token
|
为什么必须使用 token:
|
||||||
|
|
||||||
|
- 插件具备较广的浏览器权限(页面访问、导航、多标签控制)。
|
||||||
|
- 若被滥用,可能危害用户隐私与安全。
|
||||||
|
- 用户必须主动将 token 提供给其信任的应用。
|
||||||
|
|
||||||
|
配置步骤:
|
||||||
|
|
||||||
|
1. 在扩展侧边栏中复制 auth token。
|
||||||
2. 在页面中设置 token:
|
2. 在页面中设置 token:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -60,32 +74,32 @@ if (await waitForExtension()) {
|
|||||||
|
|
||||||
## 全局 API
|
## 全局 API
|
||||||
|
|
||||||
插件在 `window` 对象上注入以下 API:
|
token 匹配后,插件会在 `window` 上注入 API。
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT_VERSION`
|
### `window.PAGE_AGENT_EXT_VERSION`
|
||||||
|
|
||||||
插件版本号字符串(例如 `"1.0.0"`)。单独暴露版本号,方便在访问主 API 对象前进行版本检查。
|
插件版本号字符串,可用于在访问主 API 前做能力检查。
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT`
|
### `window.PAGE_AGENT_EXT`
|
||||||
|
|
||||||
主 API 命名空间对象,包含:
|
主命名空间对象。
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.execute(task, config)`
|
#### `PAGE_AGENT_EXT.execute(task, config)`
|
||||||
|
|
||||||
执行 Agent 任务。
|
执行 Agent 任务。
|
||||||
|
|
||||||
**参数:**
|
参数:
|
||||||
|
|
||||||
| 名称 | 类型 | 必填 | 说明 |
|
| 名称 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
| ---- | ---- | ---- | ---- |
|
||||||
| `task` | `string` | 是 | 任务描述 |
|
| `task` | `string` | 是 | 任务描述 |
|
||||||
| `config` | `ExecuteConfig` | 是 | 执行配置(LLM 设置、选项和事件回调) |
|
| `config` | `ExecuteConfig` | 是 | LLM 设置、执行选项和回调 |
|
||||||
|
|
||||||
**返回:** `Promise<ExecutionResult>`
|
返回:`Promise<ExecutionResult>`
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.dispose()`
|
#### `PAGE_AGENT_EXT.stop()`
|
||||||
|
|
||||||
停止并销毁当前运行的 Agent。
|
停止当前任务。
|
||||||
|
|
||||||
## 类型定义
|
## 类型定义
|
||||||
|
|
||||||
@@ -104,35 +118,24 @@ export interface ExecuteConfig {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
model: string
|
model: string
|
||||||
|
|
||||||
/**
|
// 是否包含启动脚本所在标签页。默认 true。
|
||||||
* 是否将初始标签页(运行此脚本的页面)包含在任务中。
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
onDispose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
```
|
```
|
||||||
|
|
||||||
### AgentStatus
|
`AgentStatus`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
||||||
```
|
```
|
||||||
|
|
||||||
| 状态 | 说明 |
|
`AgentActivity`
|
||||||
|------|------|
|
|
||||||
| `idle` | 空闲,准备执行 |
|
|
||||||
| `running` | 正在执行任务 |
|
|
||||||
| `completed` | 任务成功完成 |
|
|
||||||
| `error` | 任务执行失败 |
|
|
||||||
|
|
||||||
### AgentActivity
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentActivity =
|
type AgentActivity =
|
||||||
@@ -143,15 +146,7 @@ type AgentActivity =
|
|||||||
| { type: 'error'; message: string }
|
| { type: 'error'; message: string }
|
||||||
```
|
```
|
||||||
|
|
||||||
| 类型 | 说明 |
|
`HistoricalEvent`
|
||||||
|------|------|
|
|
||||||
| `thinking` | Agent 正在分析页面并规划 |
|
|
||||||
| `executing` | 正在执行工具操作 |
|
|
||||||
| `executed` | 工具执行完成 |
|
|
||||||
| `retrying` | 失败后重试 |
|
|
||||||
| `error` | 发生错误 |
|
|
||||||
|
|
||||||
### HistoricalEvent
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type HistoricalEvent =
|
type HistoricalEvent =
|
||||||
@@ -162,7 +157,7 @@ type HistoricalEvent =
|
|||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||||
```
|
```
|
||||||
|
|
||||||
### ExecutionResult
|
`ExecutionResult`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ExecutionResult {
|
interface ExecutionResult {
|
||||||
@@ -183,81 +178,22 @@ const result = await window.PAGE_AGENT_EXT!.execute(
|
|||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
}
|
includeInitialTab: false, // 可选:排除当前标签页
|
||||||
)
|
onStatusChange: (status) => console.log(status),
|
||||||
|
onActivity: (activity) => console.log(activity),
|
||||||
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
|
```typescript
|
||||||
await window.PAGE_AGENT_EXT!.execute('导航到设置页面', {
|
window.PAGE_AGENT_EXT!.stop()
|
||||||
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 类型声明
|
## Window 类型声明
|
||||||
|
|
||||||
如果不使用 `@page-agent/core`,可以添加以下声明:
|
如果你不直接引入 `@page-agent/core`,可添加以下声明:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type {
|
||||||
@@ -275,7 +211,6 @@ interface ExecuteConfig {
|
|||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
onDispose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -283,8 +218,8 @@ declare global {
|
|||||||
PAGE_AGENT_EXT_VERSION?: string
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
PAGE_AGENT_EXT?: {
|
PAGE_AGENT_EXT?: {
|
||||||
version: string
|
version: string
|
||||||
execute: (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
execute: Execute
|
||||||
dispose: () => void
|
stop: () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
@@ -10,23 +10,6 @@
|
|||||||
"postinstall": "wxt prepare"
|
"postinstall": "wxt prepare"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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.2",
|
|
||||||
"@page-agent/llms": "1.1.2",
|
|
||||||
"@page-agent/page-controller": "1.1.2",
|
|
||||||
"@page-agent/ui": "1.1.2",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
@@ -34,14 +17,32 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"motion": "^12.30.0",
|
"motion": "^12.34.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.7.0",
|
"simple-icons": "^16.8.0",
|
||||||
"sonner": "^2.0.7",
|
"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",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: () => {
|
onDispose: () => {
|
||||||
if (heartBeatInterval) {
|
if (heartBeatInterval) {
|
||||||
window.clearInterval(heartBeatInterval)
|
window.clearInterval(heartBeatInterval)
|
||||||
@@ -84,8 +89,7 @@ export class MultiPageAgent extends PageAgentCore {
|
|||||||
isAgentRunning: false,
|
isAgentRunning: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// no need to dispose tabsController and pageController
|
tabsController.dispose()
|
||||||
// as they do not keep references
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,21 @@ export function handlePageControlMessage(
|
|||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
sendResponse: (response: unknown) => void
|
sendResponse: (response: unknown) => void
|
||||||
): true | undefined {
|
): true | undefined {
|
||||||
|
const PREFIX = '[RemotePageController.background]'
|
||||||
|
|
||||||
|
function debug(...messages: any[]) {
|
||||||
|
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||||
|
}
|
||||||
|
|
||||||
const { action, payload, targetTabId } = message
|
const { action, payload, targetTabId } = message
|
||||||
|
|
||||||
if (action === 'get_my_tab_id') {
|
if (action === 'get_my_tab_id') {
|
||||||
|
debug('get_my_tab_id', sender.tab?.id)
|
||||||
sendResponse({ tabId: sender.tab?.id || null })
|
sendResponse({ tabId: sender.tab?.id || null })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proxy to content script
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.sendMessage(targetTabId, {
|
.sendMessage(targetTabId, {
|
||||||
type: 'PAGE_CONTROL',
|
type: 'PAGE_CONTROL',
|
||||||
@@ -25,6 +33,7 @@ export function handlePageControlMessage(
|
|||||||
sendResponse(result)
|
sendResponse(result)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.error(PREFIX, error)
|
||||||
sendResponse({
|
sendResponse({
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ export function initPageController() {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
return (response as { tabId: number | null }).tabId
|
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 {
|
function getPC(): PageController {
|
||||||
if (!pageController) {
|
if (!pageController) {
|
||||||
pageController = new PageController({ enableMask: false })
|
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
|
||||||
}
|
}
|
||||||
return pageController
|
return pageController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,31 @@ import type { BrowserState } from '@page-agent/page-controller'
|
|||||||
|
|
||||||
import type { TabsController } from './TabsController'
|
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.
|
* Agent side page controller.
|
||||||
* - live in the agent env (extension page or content script)
|
* - live in the agent env (extension page or content script)
|
||||||
* - communicates with remote PageController via sw
|
* - communicates with remote PageController via sw
|
||||||
*/
|
*/
|
||||||
export class RemotePageController {
|
export class RemotePageController {
|
||||||
private tabsController: TabsController
|
tabsController: TabsController
|
||||||
|
|
||||||
constructor(tabsController: TabsController) {
|
constructor(tabsController: TabsController) {
|
||||||
this.tabsController = tabsController
|
this.tabsController = tabsController
|
||||||
@@ -18,46 +36,46 @@ export class RemotePageController {
|
|||||||
return this.tabsController.currentTabId
|
return this.tabsController.currentTabId
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentUrl(): Promise<string> {
|
private async getCurrentUrl(): Promise<string> {
|
||||||
if (!this.currentTabId) return ''
|
if (!this.currentTabId) return ''
|
||||||
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
|
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||||
return url || ''
|
return url || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentTitle(): Promise<string> {
|
private async getCurrentTitle(): Promise<string> {
|
||||||
if (!this.currentTabId) return ''
|
if (!this.currentTabId) return ''
|
||||||
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
|
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
|
||||||
return title || ''
|
return title || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentTabTitle(): Promise<string> {
|
|
||||||
return this.getCurrentTitle()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLastUpdateTime(): Promise<number> {
|
async getLastUpdateTime(): Promise<number> {
|
||||||
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
||||||
|
return sendMessage({
|
||||||
return await chrome.runtime.sendMessage({
|
|
||||||
type: 'PAGE_CONTROL',
|
type: 'PAGE_CONTROL',
|
||||||
action: 'get_last_update_time',
|
action: 'get_last_update_time',
|
||||||
targetTabId: this.currentTabId,
|
targetTabId: this.currentTabId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBrowserState
|
|
||||||
async getBrowserState(): Promise<BrowserState> {
|
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 = {
|
browserState = {
|
||||||
url: await this.currentTabUrl,
|
url: currentUrl,
|
||||||
title: await this.currentTabTitle,
|
title: currentTitle,
|
||||||
header: '',
|
header: '',
|
||||||
content: '(empty page)',
|
content: '(empty page. either current page is not readable or not loaded yet.)',
|
||||||
footer: '',
|
footer: '',
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
browserState = await chrome.runtime.sendMessage({
|
browserState = await sendMessage({
|
||||||
type: 'PAGE_CONTROL',
|
type: 'PAGE_CONTROL',
|
||||||
action: 'get_browser_state',
|
action: 'get_browser_state',
|
||||||
targetTabId: this.currentTabId,
|
targetTabId: this.currentTabId,
|
||||||
@@ -67,61 +85,58 @@ export class RemotePageController {
|
|||||||
const sum = await this.tabsController.summarizeTabs()
|
const sum = await this.tabsController.summarizeTabs()
|
||||||
browserState.header = sum + '\n\n' + (browserState.header || '')
|
browserState.header = sum + '\n\n' + (browserState.header || '')
|
||||||
|
|
||||||
|
debug('getBrowserState: success', this.currentTabId, browserState)
|
||||||
|
|
||||||
return browserState
|
return browserState
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTree
|
|
||||||
async updateTree(): Promise<void> {
|
async updateTree(): Promise<void> {
|
||||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await chrome.runtime.sendMessage({
|
await sendMessage({
|
||||||
type: 'PAGE_CONTROL',
|
type: 'PAGE_CONTROL',
|
||||||
action: 'update_tree',
|
action: 'update_tree',
|
||||||
targetTabId: this.currentTabId,
|
targetTabId: this.currentTabId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanUpHighlights
|
|
||||||
async cleanUpHighlights(): Promise<void> {
|
async cleanUpHighlights(): Promise<void> {
|
||||||
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
|
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await chrome.runtime.sendMessage({
|
await sendMessage({
|
||||||
type: 'PAGE_CONTROL',
|
type: 'PAGE_CONTROL',
|
||||||
action: 'clean_up_highlights',
|
action: 'clean_up_highlights',
|
||||||
targetTabId: this.currentTabId,
|
targetTabId: this.currentTabId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// clickElement
|
|
||||||
async clickElement(...args: any[]): Promise<DomActionReturn> {
|
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> {
|
async inputText(...args: any[]): Promise<DomActionReturn> {
|
||||||
return this.remoteCallDomAction('input_text', args)
|
return this.remoteCallDomAction('input_text', args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectOption
|
|
||||||
async selectOption(...args: any[]): Promise<DomActionReturn> {
|
async selectOption(...args: any[]): Promise<DomActionReturn> {
|
||||||
return this.remoteCallDomAction('select_option', args)
|
return this.remoteCallDomAction('select_option', args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// scroll
|
|
||||||
async scroll(...args: any[]): Promise<DomActionReturn> {
|
async scroll(...args: any[]): Promise<DomActionReturn> {
|
||||||
return this.remoteCallDomAction('scroll', args)
|
return this.remoteCallDomAction('scroll', args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// scrollHorizontally
|
|
||||||
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
|
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
|
||||||
return this.remoteCallDomAction('scroll_horizontally', args)
|
return this.remoteCallDomAction('scroll_horizontally', args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeJavascript
|
|
||||||
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
|
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
|
||||||
return this.remoteCallDomAction('execute_javascript', args)
|
return this.remoteCallDomAction('execute_javascript', args)
|
||||||
}
|
}
|
||||||
@@ -133,35 +148,26 @@ export class RemotePageController {
|
|||||||
/** @note Managed by content script via storage polling. */
|
/** @note Managed by content script via storage polling. */
|
||||||
dispose(): void {}
|
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> {
|
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
|
||||||
const preCheckError = await this.preCheck()
|
if (!this.currentTabId) {
|
||||||
if (preCheckError) {
|
return { success: false, message: 'RemotePageController not initialized.' }
|
||||||
return { success: false, message: preCheckError }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
type: 'PAGE_CONTROL',
|
||||||
action: action,
|
action: action,
|
||||||
targetTabId: this.currentTabId!,
|
targetTabId: this.currentTabId!,
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private get currentTabUrl(): Promise<string> {
|
|
||||||
return this.getCurrentUrl()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DomActionReturn {
|
interface DomActionReturn {
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
import type { TabAction } from './TabsController'
|
import type { TabAction } from './TabsController'
|
||||||
|
|
||||||
|
const PREFIX = '[TabsController.background]'
|
||||||
|
|
||||||
|
function debug(...messages: any[]) {
|
||||||
|
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
|
||||||
|
}
|
||||||
|
|
||||||
export function handleTabControlMessage(
|
export function handleTabControlMessage(
|
||||||
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
|
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
@@ -12,10 +18,12 @@ export function handleTabControlMessage(
|
|||||||
|
|
||||||
switch (action as TabAction) {
|
switch (action as TabAction) {
|
||||||
case 'get_active_tab': {
|
case 'get_active_tab': {
|
||||||
|
debug('get_active_tab')
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.query({ active: true, currentWindow: true })
|
.query({ active: true, currentWindow: true })
|
||||||
.then((tabs) => {
|
.then((tabs) => {
|
||||||
const tabId = tabs.length > 0 ? tabs[0].id || null : null
|
const tabId = tabs.length > 0 ? tabs[0].id || null : null
|
||||||
|
debug('get_active_tab: success', tabId)
|
||||||
sendResponse({ success: true, tabId })
|
sendResponse({ success: true, tabId })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -25,11 +33,12 @@ export function handleTabControlMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'get_tab_info': {
|
case 'get_tab_info': {
|
||||||
|
debug('get_tab_info', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.get(payload.tabId)
|
.get(payload.tabId)
|
||||||
.then((tab) => {
|
.then((tab) => {
|
||||||
const result = { title: tab.title || '', url: tab.url || '' }
|
debug('get_tab_info: success', tab)
|
||||||
sendResponse(result)
|
sendResponse(tab)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
||||||
@@ -38,10 +47,11 @@ export function handleTabControlMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'open_new_tab': {
|
case 'open_new_tab': {
|
||||||
|
debug('open_new_tab', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.create({ url: payload.url, active: false })
|
.create({ url: payload.url, active: false })
|
||||||
.then((newTab) => {
|
.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 })
|
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -51,20 +61,22 @@ export function handleTabControlMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'create_tab_group': {
|
case 'create_tab_group': {
|
||||||
|
debug('create_tab_group', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
|
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
|
||||||
.then((groupId) => {
|
.then((groupId) => {
|
||||||
console.log('Created tab group', groupId)
|
debug('create_tab_group: success', groupId)
|
||||||
sendResponse({ success: true, groupId })
|
sendResponse({ success: true, groupId })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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) })
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
||||||
})
|
})
|
||||||
return true // async response
|
return true // async response
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'update_tab_group': {
|
case 'update_tab_group': {
|
||||||
|
debug('update_tab_group', payload)
|
||||||
chrome.tabGroups
|
chrome.tabGroups
|
||||||
.update(payload.groupId, payload.properties)
|
.update(payload.groupId, payload.properties)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -77,6 +89,7 @@ export function handleTabControlMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'add_tab_to_group': {
|
case 'add_tab_to_group': {
|
||||||
|
debug('add_tab_to_group', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.group({ tabIds: payload.tabId, groupId: payload.groupId })
|
.group({ tabIds: payload.tabId, groupId: payload.groupId })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -89,6 +102,7 @@ export function handleTabControlMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'close_tab': {
|
case 'close_tab': {
|
||||||
|
debug('close_tab', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.remove(payload.tabId)
|
.remove(payload.tabId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -105,3 +119,42 @@ export function handleTabControlMessage(
|
|||||||
return
|
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.
|
* Controller for managing browser tabs.
|
||||||
* - live in the agent env (extension page or content script)
|
* - live in the agent env (extension page or content script)
|
||||||
@@ -13,6 +30,8 @@ export class TabsController extends EventTarget {
|
|||||||
private windowId: number | null = null
|
private windowId: number | null = null
|
||||||
|
|
||||||
async init(task: string, includeInitialTab: boolean = true) {
|
async init(task: string, includeInitialTab: boolean = true) {
|
||||||
|
debug('init', task, includeInitialTab)
|
||||||
|
|
||||||
this.task = task
|
this.task = task
|
||||||
this.tabs = []
|
this.tabs = []
|
||||||
this.currentTabId = null
|
this.currentTabId = null
|
||||||
@@ -20,7 +39,7 @@ export class TabsController extends EventTarget {
|
|||||||
this.initialTabId = null
|
this.initialTabId = null
|
||||||
this.windowId = null
|
this.windowId = null
|
||||||
|
|
||||||
const result = await chrome.runtime.sendMessage({
|
const result = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'get_active_tab',
|
action: 'get_active_tab',
|
||||||
})
|
})
|
||||||
@@ -33,9 +52,20 @@ export class TabsController extends EventTarget {
|
|||||||
|
|
||||||
if (includeInitialTab) {
|
if (includeInitialTab) {
|
||||||
this.currentTabId = this.initialTabId
|
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({
|
this.tabs.push({
|
||||||
id: result.tabId,
|
id: result.tabId,
|
||||||
isInitial: true,
|
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 }> {
|
async openNewTab(url: string): Promise<string> {
|
||||||
const result = await chrome.runtime.sendMessage({
|
debug('openNewTab', url)
|
||||||
|
|
||||||
|
const result = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'open_new_tab',
|
action: 'open_new_tab',
|
||||||
payload: { url },
|
payload: { url },
|
||||||
@@ -104,7 +144,7 @@ export class TabsController extends EventTarget {
|
|||||||
await this.switchToTab(tabId)
|
await this.switchToTab(tabId)
|
||||||
|
|
||||||
if (!this.tabGroupId) {
|
if (!this.tabGroupId) {
|
||||||
const result = await chrome.runtime.sendMessage({
|
const result = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'create_tab_group',
|
action: 'create_tab_group',
|
||||||
payload: { tabIds: [tabId], windowId: this.windowId },
|
payload: { tabIds: [tabId], windowId: this.windowId },
|
||||||
@@ -118,7 +158,7 @@ export class TabsController extends EventTarget {
|
|||||||
|
|
||||||
this.tabGroupId = groupId
|
this.tabGroupId = groupId
|
||||||
|
|
||||||
await chrome.runtime.sendMessage({
|
await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'update_tab_group',
|
action: 'update_tab_group',
|
||||||
payload: {
|
payload: {
|
||||||
@@ -131,57 +171,43 @@ export class TabsController extends EventTarget {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await chrome.runtime.sendMessage({
|
await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'add_tab_to_group',
|
action: 'add_tab_to_group',
|
||||||
payload: { tabId: result.tabId, groupId: this.tabGroupId },
|
payload: { tabId: result.tabId, groupId: this.tabGroupId },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for the new tab to be fully loaded
|
await this.waitUntilTabLoaded(tabId)
|
||||||
// @todo
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
return {
|
return `✅ Opened new tab ID ${tabId} with URL ${url}`
|
||||||
success: true,
|
|
||||||
tabId,
|
|
||||||
message: `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)
|
const targetTab = this.tabs.find((t) => t.id === tabId)
|
||||||
if (!targetTab) {
|
if (!targetTab) {
|
||||||
return {
|
throw new Error(`Tab ID ${tabId} not found in tab list.`)
|
||||||
success: false,
|
|
||||||
message: `Tab ID ${tabId} not found in tab list.`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateCurrentTabId(tabId)
|
await this.updateCurrentTabId(tabId)
|
||||||
|
|
||||||
return {
|
return `✅ Switched to tab ID ${tabId}.`
|
||||||
success: true,
|
|
||||||
message: `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)
|
const targetTab = this.tabs.find((t) => t.id === tabId)
|
||||||
if (!targetTab) {
|
if (!targetTab) {
|
||||||
return {
|
throw new Error(`Tab ID ${tabId} not found in tab list.`)
|
||||||
success: false,
|
|
||||||
message: `Tab ID ${tabId} not found in tab list.`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (targetTab.isInitial) {
|
if (targetTab.isInitial) {
|
||||||
return {
|
throw new Error(`Cannot close the initial tab ID ${tabId}.`)
|
||||||
success: false,
|
|
||||||
message: `Cannot close the initial tab ID ${tabId}.`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await chrome.runtime.sendMessage({
|
const result = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'close_tab',
|
action: 'close_tab',
|
||||||
payload: { tabId },
|
payload: { tabId },
|
||||||
@@ -198,29 +224,39 @@ export class TabsController extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return `✅ Closed tab ID ${tabId}.`
|
||||||
success: true,
|
|
||||||
message: `Closed tab ID ${tabId}.`,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
throw new Error(`Failed to close tab ID ${tabId}: ${result.error}`)
|
||||||
success: false,
|
|
||||||
message: `Failed to close tab ID ${tabId}: ${result.error}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCurrentTabId(tabId: number | null) {
|
async updateCurrentTabId(tabId: number | null) {
|
||||||
|
debug('updateCurrentTabId', tabId)
|
||||||
|
|
||||||
this.currentTabId = tabId
|
this.currentTabId = tabId
|
||||||
await chrome.storage.local.set({ currentTabId: tabId })
|
await chrome.storage.local.set({ currentTabId: tabId })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTabInfo(tabId: number): Promise<{ title: string; url: string }> {
|
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',
|
type: 'TAB_CONTROL',
|
||||||
action: 'get_tab_info',
|
action: 'get_tab_info',
|
||||||
payload: { tabId },
|
payload: { tabId },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tabMeta) {
|
||||||
|
tabMeta.url = result.url
|
||||||
|
tabMeta.title = result.title
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +275,17 @@ export class TabsController extends EventTarget {
|
|||||||
return summaries.join('\n')
|
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() {
|
dispose() {
|
||||||
this.dispatchEvent(new Event('dispose'))
|
this.dispatchEvent(new Event('dispose'))
|
||||||
}
|
}
|
||||||
@@ -257,6 +304,9 @@ export type TabAction =
|
|||||||
interface TabMeta {
|
interface TabMeta {
|
||||||
id: number
|
id: number
|
||||||
isInitial: boolean
|
isInitial: boolean
|
||||||
|
url?: string
|
||||||
|
title?: string
|
||||||
|
status?: 'loading' | 'unloaded' | 'complete'
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAB_GROUP_COLORS = [
|
const TAB_GROUP_COLORS = [
|
||||||
@@ -275,3 +325,33 @@ type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
|
|||||||
function randomColor(): TabGroupColor {
|
function randomColor(): TabGroupColor {
|
||||||
return TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]
|
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.
|
Here are examples of good output patterns. Use them as reference but never copy them directly.
|
||||||
|
|
||||||
<evaluation_examples>
|
<evaluation_examples>
|
||||||
- Positive Examples:
|
|
||||||
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
|
"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_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
|
||||||
</evaluation_examples>
|
</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_examples>
|
||||||
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
|
"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>
|
</next_goal_examples>
|
||||||
</examples>
|
</examples>
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
|||||||
}),
|
}),
|
||||||
execute: async (input: unknown) => {
|
execute: async (input: unknown) => {
|
||||||
const { url } = input as { url: string }
|
const { url } = input as { url: string }
|
||||||
const result = await tabsController.openNewTab(url)
|
try {
|
||||||
return result.message
|
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) => {
|
execute: async (input: unknown) => {
|
||||||
const { tab_id } = input as { tab_id: number }
|
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) => {
|
execute: async (input: unknown) => {
|
||||||
const { tab_id } = input as { tab_id: number }
|
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)}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function useAgent(): UseAgentResult {
|
|||||||
|
|
||||||
const execute = useCallback(async (task: string) => {
|
const execute = useCallback(async (task: string) => {
|
||||||
const agent = agentRef.current
|
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')
|
if (!agent) throw new Error('Agent not initialized')
|
||||||
|
|
||||||
setCurrentTask(task)
|
setCurrentTask(task)
|
||||||
@@ -84,7 +84,7 @@ export function useAgent(): UseAgentResult {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
agentRef.current?.dispose()
|
agentRef.current?.stop()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const configure = useCallback(async (newConfig: LLMConfig) => {
|
const configure = useCallback(async (newConfig: LLMConfig) => {
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
|
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
|
||||||
import { handleTabControlMessage } from '@/agent/TabsController.background'
|
import { handleTabControlMessage, setupTabChangeEvents } 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
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export default defineBackground(() => {
|
export default defineBackground(() => {
|
||||||
console.log('[Background] Service Worker started')
|
console.log('[Background] Service Worker started')
|
||||||
|
|
||||||
|
// tab change events
|
||||||
|
|
||||||
|
setupTabChangeEvents()
|
||||||
|
|
||||||
// generate user auth token
|
// generate user auth token
|
||||||
|
|
||||||
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
|
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
|
||||||
@@ -28,21 +17,20 @@ export default defineBackground(() => {
|
|||||||
chrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })
|
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
|
// setup
|
||||||
|
|
||||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
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({
|
export default defineContentScript({
|
||||||
matches: ['<all_urls>'],
|
matches: ['<all_urls>'],
|
||||||
runAt: 'document_idle',
|
runAt: 'document_end',
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
|
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
|
||||||
@@ -71,7 +71,8 @@ async function exposeAgentToPage() {
|
|||||||
try {
|
try {
|
||||||
const { task, config } = payload
|
const { task, config } = payload
|
||||||
|
|
||||||
// create when used
|
// Dispose old instance before creating new one
|
||||||
|
multiPageAgent?.dispose()
|
||||||
|
|
||||||
multiPageAgent = new MultiPageAgent(config)
|
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
|
// result
|
||||||
|
|
||||||
const result = await multiPageAgent.execute(task)
|
const result = await multiPageAgent.execute(task)
|
||||||
@@ -155,9 +145,8 @@ async function exposeAgentToPage() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'dispose': {
|
case 'stop': {
|
||||||
// @note stop ongoing processes but can still be re-used later
|
multiPageAgent?.stop()
|
||||||
multiPageAgent?.dispose()
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface ExecuteConfig {
|
|||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
onDispose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineUnlistedScript(() => {
|
export default defineUnlistedScript(() => {
|
||||||
@@ -60,12 +59,6 @@ export default defineUnlistedScript(() => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action === 'dispose_event' && config.onDispose) {
|
|
||||||
config.onDispose()
|
|
||||||
window.removeEventListener('message', handleMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.action !== 'execute_result') return
|
if (data.action !== 'execute_result') return
|
||||||
|
|
||||||
// execute_result
|
// execute_result
|
||||||
@@ -104,14 +97,14 @@ export default defineUnlistedScript(() => {
|
|||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispose = () => {
|
const stop = () => {
|
||||||
const id = getId()
|
const id = getId()
|
||||||
|
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
channel: 'PAGE_AGENT_EXT_REQUEST',
|
channel: 'PAGE_AGENT_EXT_REQUEST',
|
||||||
id,
|
id,
|
||||||
action: 'dispose',
|
action: 'stop',
|
||||||
},
|
},
|
||||||
'*'
|
'*'
|
||||||
)
|
)
|
||||||
@@ -121,6 +114,6 @@ export default defineUnlistedScript(() => {
|
|||||||
;(window as any).PAGE_AGENT_EXT = {
|
;(window as any).PAGE_AGENT_EXT = {
|
||||||
version: __EXT_VERSION__,
|
version: __EXT_VERSION__,
|
||||||
execute,
|
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 { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -8,20 +8,47 @@ import {
|
|||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
} from '@/components/ui/input-group'
|
} from '@/components/ui/input-group'
|
||||||
|
import { saveSession } from '@/lib/db'
|
||||||
|
|
||||||
import { useAgent } from '../../agent/useAgent'
|
import { useAgent } from '../../agent/useAgent'
|
||||||
import { ConfigPanel } from './components/ConfigPanel'
|
import { ConfigPanel } from './components/ConfigPanel'
|
||||||
|
import { HistoryDetail } from './components/HistoryDetail'
|
||||||
|
import { HistoryList } from './components/HistoryList'
|
||||||
import { ActivityCard, EventCard } from './components/cards'
|
import { ActivityCard, EventCard } from './components/cards'
|
||||||
import { EmptyState, Logo, StatusDot } from './components/misc'
|
import { EmptyState, Logo, StatusDot } from './components/misc'
|
||||||
|
|
||||||
|
type View =
|
||||||
|
| { name: 'chat' }
|
||||||
|
| { name: 'config' }
|
||||||
|
| { name: 'history' }
|
||||||
|
| { name: 'history-detail'; sessionId: string }
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [showConfig, setShowConfig] = useState(false)
|
const [view, setView] = useState<View>({ name: 'chat' })
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const historyRef = useRef<HTMLDivElement>(null)
|
const historyRef = useRef<HTMLDivElement>(null)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
|
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
|
// Auto-scroll to bottom on new events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (historyRef.current) {
|
if (historyRef.current) {
|
||||||
@@ -56,19 +83,36 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showConfig) {
|
// --- View routing ---
|
||||||
|
|
||||||
|
if (view.name === 'config') {
|
||||||
return (
|
return (
|
||||||
<ConfigPanel
|
<ConfigPanel
|
||||||
config={config}
|
config={config}
|
||||||
onSave={async (newConfig) => {
|
onSave={async (newConfig) => {
|
||||||
await configure(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 isRunning = status === 'running'
|
||||||
const showEmptyState = !currentTask && history.length === 0 && !isRunning
|
const showEmptyState = !currentTask && history.length === 0 && !isRunning
|
||||||
|
|
||||||
@@ -85,7 +129,15 @@ export default function App() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
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"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
<Settings className="size-3.5" />
|
<Settings className="size-3.5" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ export class OpenAIClient implements LLMClient {
|
|||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error)
|
const isAbortError = (error as any)?.name === 'AbortError'
|
||||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
|
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
|
// 3. Handle HTTP errors
|
||||||
|
|||||||
@@ -34,12 +34,15 @@ export class InvokeError extends Error {
|
|||||||
super(message)
|
super(message)
|
||||||
this.name = 'InvokeError'
|
this.name = 'InvokeError'
|
||||||
this.type = type
|
this.type = type
|
||||||
this.retryable = this.isRetryable(type)
|
this.retryable = this.isRetryable(type, rawError)
|
||||||
this.rawError = rawError
|
this.rawError = rawError
|
||||||
this.rawResponse = rawResponse
|
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[] = [
|
const retryableTypes: InvokeErrorType[] = [
|
||||||
InvokeErrorType.NETWORK_ERROR,
|
InvokeErrorType.NETWORK_ERROR,
|
||||||
InvokeErrorType.RATE_LIMIT,
|
InvokeErrorType.RATE_LIMIT,
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export class LLM extends EventTarget {
|
|||||||
): Promise<InvokeResult> {
|
): Promise<InvokeResult> {
|
||||||
return await withRetry(
|
return await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
|
// in case user aborted before invoking
|
||||||
|
if (abortSignal.aborted) throw new Error('AbortError')
|
||||||
|
|
||||||
const result = await this.client.invoke(messages, tools, abortSignal, options)
|
const result = await this.client.invoke(messages, tools, abortSignal, options)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./dist/esm/page-agent.js",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"module": "./dist/esm/page-agent.js",
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.1.2",
|
"@page-agent/core": "1.3.0",
|
||||||
"@page-agent/llms": "1.1.2",
|
"@page-agent/llms": "1.3.0",
|
||||||
"@page-agent/page-controller": "1.1.2",
|
"@page-agent/page-controller": "1.3.0",
|
||||||
"@page-agent/ui": "1.1.2",
|
"@page-agent/ui": "1.3.0",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./dist/lib/page-controller.js",
|
||||||
"module": "./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') {
|
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
|
||||||
return {
|
return {
|
||||||
success: true,
|
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",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const enUS = {
|
|||||||
question: 'Question: {{question}}',
|
question: 'Question: {{question}}',
|
||||||
waitingPlaceholder: 'Waiting for task to start...',
|
waitingPlaceholder: 'Waiting for task to start...',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
|
close: 'Close',
|
||||||
expand: 'Expand history',
|
expand: 'Expand history',
|
||||||
collapse: 'Collapse history',
|
collapse: 'Collapse history',
|
||||||
step: 'Step {{number}} · {{time}}{{duration}}',
|
step: 'Step {{number}} · {{time}}{{duration}}',
|
||||||
@@ -59,6 +60,7 @@ const zhCN = {
|
|||||||
question: '询问: {{question}}',
|
question: '询问: {{question}}',
|
||||||
waitingPlaceholder: '等待任务开始...',
|
waitingPlaceholder: '等待任务开始...',
|
||||||
stop: '终止',
|
stop: '终止',
|
||||||
|
close: '关闭',
|
||||||
expand: '展开历史',
|
expand: '展开历史',
|
||||||
collapse: '收起历史',
|
collapse: '收起历史',
|
||||||
step: '步骤 {{number}} · {{time}}{{duration}}',
|
step: '步骤 {{number}} · {{time}}{{duration}}',
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { Panel, type PanelConfig } from './panel/Panel'
|
export { Panel, type PanelConfig } from './panel/Panel'
|
||||||
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
|
|
||||||
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
|
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class Panel {
|
|||||||
#statusText: HTMLElement
|
#statusText: HTMLElement
|
||||||
#historySection: HTMLElement
|
#historySection: HTMLElement
|
||||||
#expandButton: HTMLElement
|
#expandButton: HTMLElement
|
||||||
#stopButton: HTMLElement
|
#actionButton: HTMLElement
|
||||||
#inputSection: HTMLElement
|
#inputSection: HTMLElement
|
||||||
#taskInput: HTMLInputElement
|
#taskInput: HTMLInputElement
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export class Panel {
|
|||||||
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
||||||
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
|
||||||
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
|
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.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
|
||||||
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
|
||||||
|
|
||||||
@@ -105,6 +105,15 @@ export class Panel {
|
|||||||
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
|
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
|
||||||
this.#updateStatusIndicator(indicatorType)
|
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
|
// Show/hide based on status
|
||||||
if (status === 'running') {
|
if (status === 'running') {
|
||||||
this.show()
|
this.show()
|
||||||
@@ -266,10 +275,14 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop Agent
|
* Action button handler: stop when running, close (dispose) when idle
|
||||||
*/
|
*/
|
||||||
#stopAgent(): void {
|
#handleActionButton(): void {
|
||||||
this.#agent.dispose()
|
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 class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
||||||
▼
|
▼
|
||||||
</button>
|
</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
|
X
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,10 +433,10 @@ export class Panel {
|
|||||||
this.#toggle()
|
this.#toggle()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stop button
|
// Action button (stop / close)
|
||||||
this.#stopButton.addEventListener('click', (e) => {
|
this.#actionButton.addEventListener('click', (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
this.#stopAgent()
|
this.#handleActionButton()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Submit on Enter key in input field
|
// Submit on Enter key in input field
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export interface PanelAgentAdapter extends EventTarget {
|
|||||||
/** Execute a task */
|
/** Execute a task */
|
||||||
execute(task: string): Promise<unknown>
|
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
|
dispose(): void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.2",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
@@ -10,30 +10,26 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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-icons": "^1.3.2",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@types/react": "^19.2.13",
|
||||||
|
"@types/react-dom": "^19.2.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"motion": "^12.30.0",
|
"motion": "^12.34.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.7.0",
|
"simple-icons": "^16.8.0",
|
||||||
"sonner": "^2.0.7",
|
"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)
|
// Demo build (auto-init with demo LLM, for quick testing)
|
||||||
export const CDN_DEMO_URL =
|
export const CDN_DEMO_URL =
|
||||||
'https://cdn.jsdelivr.net/npm/page-agent@1.1.2/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 =
|
export const CDN_DEMO_CN_URL =
|
||||||
'https://registry.npmmirror.com/page-agent/1.1.2/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
|
// Demo LLM for website testing
|
||||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
||||||
|
|||||||
@@ -376,13 +376,6 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
? '执行任务并返回结果。包含 success、data 和 history 字段。'
|
? '执行任务并返回结果。包含 success、data 和 history 字段。'
|
||||||
: 'Execute a task and return result. Contains success, data, and history fields.',
|
: '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)',
|
name: 'dispose(reason?: string)',
|
||||||
type: 'void',
|
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 CodeEditor from '@/components/CodeEditor'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function ChromeExtension() {
|
export default function ChromeExtension() {
|
||||||
const { isZh } = useLanguage()
|
const { isZh } = useLanguage()
|
||||||
|
const chromeWebStoreUrl =
|
||||||
|
'https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj'
|
||||||
|
const githubReleasesUrl = 'https://github.com/alibaba/page-agent/releases'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -13,70 +15,92 @@ export default function ChromeExtension() {
|
|||||||
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
|
||||||
{isZh
|
{isZh
|
||||||
? '可选的 Chrome 扩展,解锁多页任务和第三方 API 集成。'
|
? '可选的 Chrome 扩展。PageAgent.js 继续负责页面内自动化;扩展 API 额外提供多页面任务、浏览器级控制,以及从浏览器外部发起任务的能力。'
|
||||||
: 'Optional Chrome extension that unlocks multi-page tasks and third-party API integration.'}
|
: '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>
|
</p>
|
||||||
|
|
||||||
<BetaNotice />
|
|
||||||
|
|
||||||
<div className="space-y-8 mt-8">
|
<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 */}
|
{/* Features */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '核心特性' : 'Key Features'}</h2>
|
<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">
|
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
<h3 className="font-semibold mb-2">🔓 {isZh ? '多页任务' : 'Multi-Page Tasks'}</h3>
|
<h3 className="font-semibold mb-2">🔓 {isZh ? '多页任务' : 'Multi-Page Tasks'}</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
{isZh
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
<h3 className="font-semibold mb-2">
|
<h3 className="font-semibold mb-2">
|
||||||
🔌 {isZh ? '开放第三方接口' : 'Third-Party API'}
|
🧭 {isZh ? '浏览器级控制' : 'Browser-Level Control'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
{isZh
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Download */}
|
{/* Install */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '下载测试版' : 'Download Beta'}</h2>
|
<h2 className="text-2xl font-bold mb-4">{isZh ? '获取扩展' : 'Get the Extension'}</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<div className="flex flex-wrap gap-3">
|
||||||
{isZh
|
<a
|
||||||
? '扩展目前处于 Beta 阶段,请从 GitHub Releases 下载最新版本。'
|
href={chromeWebStoreUrl}
|
||||||
: 'The extension is currently in beta. Download the latest version from GitHub Releases.'}
|
target="_blank"
|
||||||
</p>
|
rel="noopener noreferrer"
|
||||||
<a
|
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"
|
||||||
href="https://github.com/alibaba/page-agent/releases"
|
>
|
||||||
target="_blank"
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
rel="noopener noreferrer"
|
<path d={siChromewebstore.path} />
|
||||||
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>
|
||||||
>
|
{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
</a>
|
||||||
<path d={siGithub.path} />
|
<a
|
||||||
</svg>
|
href={githubReleasesUrl}
|
||||||
{isZh ? '前往 GitHub Releases 下载' : 'Download from GitHub Releases'}
|
target="_blank"
|
||||||
</a>
|
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>
|
</section>
|
||||||
|
|
||||||
{/* Third-party Integration */}
|
{/* Third-party Integration */}
|
||||||
@@ -86,32 +110,33 @@ export default function ChromeExtension() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '用户授权后,外部应用可以调用扩展 API 来控制浏览器。'
|
? '通过页面 JavaScript 调用 `window.PAGE_AGENT_EXT`,你的应用可以发起跨页面任务并控制浏览器行为。'
|
||||||
: 'After user authorization, external applications can call the extension API to control the browser.'}
|
: 'By calling `window.PAGE_AGENT_EXT` from page JavaScript, your app can trigger multi-page tasks and control browser behavior.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Auth Flow */}
|
<h3 className="text-xl font-semibold mb-3">
|
||||||
<h3 className="text-xl font-semibold mb-3">{isZh ? '授权流程' : 'Authorization Flow'}</h3>
|
{isZh ? '授权与安全' : 'Authorization and Security'}
|
||||||
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '扩展使用基于 Token 的授权机制,扩展端和页面端必须持有匹配的 Token。'
|
? '扩展权限范围较广(例如页面访问、导航、多标签控制)。若被滥用,可能危害用户隐私。为此,调用能力由 Token 保护,用户必须主动将 Token 提供给其信任的应用。'
|
||||||
: 'The extension uses a token-based authorization mechanism. Both extension and page must have matching tokens.'}
|
: '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>
|
</p>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={
|
code={
|
||||||
isZh
|
isZh
|
||||||
? `// 1. 用户安装扩展并在扩展设置中配置 auth token
|
? `// 1) 用户在扩展侧边栏获取 auth token
|
||||||
// 2. 你的页面读取相同的 token 并存入 localStorage
|
// 2) 仅在可信应用中设置该 token
|
||||||
// 3. Token 匹配后,扩展会暴露 window.PAGE_AGENT_EXT 对象
|
// 3) token 匹配后,扩展会暴露 window.PAGE_AGENT_EXT
|
||||||
|
|
||||||
// ⚠️ 请在扩展弹窗中查看你的 auth token,然后填入下方
|
// ⚠️ 不要把 token 提供给不可信页面或脚本
|
||||||
localStorage.setItem('PageAgentExtUserAuthToken', '<从扩展中获取的-token>')`
|
localStorage.setItem('PageAgentExtUserAuthToken', '<从扩展中获取的-token>')`
|
||||||
: `// 1. User installs extension and sets an auth token in extension settings
|
: `// 1) Get auth token from the extension side panel
|
||||||
// 2. Your page reads the same token and stores it in localStorage
|
// 2) Set it only in trusted applications
|
||||||
// 3. After token match, extension exposes window.PAGE_AGENT_EXT object
|
// 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>')`
|
localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')`
|
||||||
}
|
}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
@@ -152,13 +177,85 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '使用配置执行任务。返回一个 Promise,在任务完成时 resolve。config 参数包含 LLM 设置、选项和事件回调。'
|
? '推荐把 `execute` 的类型声明加入你的项目,获得完整类型提示。'
|
||||||
: 'Execute a task with configuration. Returns a Promise that resolves when the task completes. Config includes LLM settings, options, and event callbacks.'}
|
: 'Add this `execute` declaration to your project for full type support.'}
|
||||||
</p>
|
</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
|
<CodeEditor
|
||||||
code={
|
code={
|
||||||
isZh
|
isZh
|
||||||
@@ -168,12 +265,11 @@ const result = await window.PAGE_AGENT_EXT.execute(
|
|||||||
{
|
{
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'gpt-5-2',
|
model: 'gpt-5.2',
|
||||||
// includeInitialTab: false, // 设为 false 排除初始标签页
|
// includeInitialTab: false, // 设为 false 排除初始标签页
|
||||||
onStatusChange: status => console.log('状态变化:', status),
|
onStatusChange: status => console.log('状态变化:', status),
|
||||||
onActivity: activity => console.log('活动:', activity),
|
onActivity: activity => console.log('活动:', activity),
|
||||||
onHistoryUpdate: history => console.log('历史更新:', history),
|
onHistoryUpdate: history => console.log('历史更新:', history)
|
||||||
onDispose: () => console.log('已停止')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -184,12 +280,11 @@ const result = await window.PAGE_AGENT_EXT.execute(
|
|||||||
{
|
{
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'gpt-5-2',
|
model: 'gpt-5.2',
|
||||||
// includeInitialTab: false, // Set to false to exclude initial tab
|
// includeInitialTab: false, // Set to false to exclude initial tab
|
||||||
onStatusChange: status => console.log('Status change:', status),
|
onStatusChange: status => console.log('Status change:', status),
|
||||||
onActivity: activity => console.log('Activity:', activity),
|
onActivity: activity => console.log('Activity:', activity),
|
||||||
onHistoryUpdate: history => console.log('History update:', history),
|
onHistoryUpdate: history => console.log('History update:', history)
|
||||||
onDispose: () => console.log('Disposed')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -198,130 +293,46 @@ console.log(result) // Task execution result`
|
|||||||
language="javascript"
|
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">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh ? '停止当前正在运行的任务。' : 'Stop the current running task.'}
|
||||||
? '停止当前正在运行的任务。停止后 Agent 可以重新使用。'
|
|
||||||
: 'Stop the current running task. The agent can be reused after disposal.'}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={
|
code={
|
||||||
isZh
|
isZh
|
||||||
? `// 停止当前任务
|
? `// 停止当前任务
|
||||||
window.PAGE_AGENT_EXT.dispose()`
|
window.PAGE_AGENT_EXT.stop()`
|
||||||
: `// Stop current task execution
|
: `// Stop current task execution
|
||||||
window.PAGE_AGENT_EXT.dispose()`
|
window.PAGE_AGENT_EXT.stop()`
|
||||||
}
|
}
|
||||||
language="javascript"
|
language="javascript"
|
||||||
/>
|
/>
|
||||||
</section>
|
</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 */}
|
{/* Integration Guide */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<h2 className="text-2xl font-bold mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '将 MultiPageAgent 融入你自己的插件'
|
? '将 MultiPageAgent 集成你自己的插件'
|
||||||
: 'Integrate MultiPageAgent into Your Extension'}
|
: 'Integrate MultiPageAgent into Your Extension'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '你可以将 MultiPageAgent 集成到自己的浏览器扩展中,实现跨页面的 AI 自动化能力。'
|
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
|
||||||
: 'You can integrate MultiPageAgent into your own browser extension for cross-page AI automation capabilities.'}
|
: '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>
|
||||||
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,20 +28,5 @@
|
|||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": 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",
|
"extends": "./tsconfig.base.json",
|
||||||
"references": [
|
"references": [
|
||||||
|
|||||||
Reference in New Issue
Block a user