Compare commits
80 Commits
EXT_v0.1.2
..
v1.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4807d550a3 | |||
| f9722f0619 | |||
| e1557a5d2e | |||
| 06280c2ba5 | |||
| 645a7ceb52 | |||
| a47c72ce84 | |||
| dcc6cd1cf3 | |||
| b4b9f6ce2d | |||
| 14e31e6ec4 | |||
| 69f2478fd7 | |||
| fe792f1ceb | |||
| d1d27a76de | |||
| 3e7e99420a | |||
| 0ffd221a61 | |||
| ee208b9779 | |||
| 6ad20c8f45 | |||
| ff4caaec22 | |||
| 91ae86f56e | |||
| c2daf13ead | |||
| ce2258fe16 | |||
| 68d68182e2 | |||
| 5f74f98a97 | |||
| 07b4dc32f7 | |||
| 9d5c8b2302 | |||
| 0c61ead38f | |||
| d92ad8f959 | |||
| a51772e191 | |||
| 89c83f454d | |||
| 7f67463a24 | |||
| 2311c1918a | |||
| b45a2af9bf | |||
| b4377ed152 | |||
| b7be29d792 | |||
| cbe223dac5 | |||
| 22fe448d95 | |||
| 49bdc70c78 | |||
| d6bd6a7170 | |||
| 2b99620de3 | |||
| d913dd7bd0 | |||
| 8cb57befb3 | |||
| b1be05309b | |||
| e5cf60df1b | |||
| 58a3d5fbc6 | |||
| 6abe75f199 | |||
| e27ebda960 | |||
| 9b603d90ba | |||
| b89228693d | |||
| 66cc9e0a3c | |||
| 53144a8021 | |||
| 43b7c1b136 | |||
| 3e7851849f | |||
| dffcb53db9 | |||
| 4dc332a32c | |||
| f19b3cc2cc | |||
| 11d66f42c4 | |||
| 20e947a1c1 | |||
| 7c87c90258 | |||
| fcb9ec4e57 | |||
| 1fcf931685 | |||
| c71ffcec1c | |||
| 430be466fc | |||
| 3f284cf2e6 | |||
| 6f0655478a | |||
| 7d5449967b | |||
| fd47888d1b | |||
| 10f470ce7d | |||
| cb29db1cc4 | |||
| 8c08f639dd | |||
| 488f080e68 | |||
| 6e07af422b | |||
| e9d721f6b9 | |||
| 840162df8c | |||
| 1517bc4868 | |||
| 67f6a56a87 | |||
| 6f0ff1fd33 | |||
| 6fdb8d03f2 | |||
| d00a8dcc21 | |||
| 851e9ef158 | |||
| 8ea8ce9acd | |||
| 9bd4a47d35 |
@@ -9,7 +9,7 @@ body:
|
|||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ contact_links:
|
|||||||
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
||||||
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
||||||
- name: Code of Conduct / 行为准则
|
- name: Code of Conduct / 行为准则
|
||||||
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
|
url: https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md
|
||||||
about: Community expectations and standards. 社区行为期望与标准。
|
about: Community expectations and standards. 社区行为期望与标准。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ body:
|
|||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ Closes #(issue)
|
|||||||
|
|
||||||
## Requirements / 要求
|
## Requirements / 要求
|
||||||
|
|
||||||
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20, 24, 25]
|
node-version: [24]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -29,11 +29,6 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
# Copied files for npm publish (generated by prepublishOnly)
|
|
||||||
packages/page-agent/README.md
|
|
||||||
packages/page-agent/README-zh.md
|
|
||||||
packages/page-agent/LICENSE
|
|
||||||
|
|
||||||
# extension
|
# extension
|
||||||
.output
|
.output
|
||||||
.wxt
|
.wxt
|
||||||
|
|||||||
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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [1.0.0] - 2026-01-19
|
|
||||||
|
|
||||||
### 🎉 First Stable Release
|
|
||||||
|
|
||||||
PageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
#### Core
|
|
||||||
|
|
||||||
- **PageAgent** - Main entry class with built-in UI Panel
|
|
||||||
- **PageAgentCore** - Headless agent class for custom UI or programmatic use
|
|
||||||
- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration
|
|
||||||
- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs
|
|
||||||
- **Tool System** - Built-in tools for click, input, scroll, select, and more
|
|
||||||
- **Custom Tools** - Extend agent capabilities with your own tools (experimental)
|
|
||||||
- **Lifecycle Hooks** - Hook into agent execution (experimental)
|
|
||||||
- **Instructions System** - System-level and page-level instructions to guide agent behavior
|
|
||||||
- **Data Masking** - Transform page content before sending to LLM
|
|
||||||
|
|
||||||
#### Page Controller
|
|
||||||
|
|
||||||
- **Element Interactions** - Click, input text, select options, scroll
|
|
||||||
- **Visual Mask** - Blocks user interaction during automation
|
|
||||||
- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption
|
|
||||||
|
|
||||||
#### UI
|
|
||||||
|
|
||||||
- **Interactive Panel** - Real-time task progress and agent thinking display
|
|
||||||
- **Ask User Tool** - Agent can ask users for clarification
|
|
||||||
- **i18n Support** - English and Chinese localization
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface PageAgentConfig {
|
|
||||||
// LLM Configuration (required)
|
|
||||||
baseURL: string
|
|
||||||
apiKey: string
|
|
||||||
model: string
|
|
||||||
temperature?: number
|
|
||||||
maxRetries?: number
|
|
||||||
customFetch?: typeof fetch
|
|
||||||
|
|
||||||
// Agent Configuration
|
|
||||||
language?: 'en-US' | 'zh-CN'
|
|
||||||
maxSteps?: number // default: 20
|
|
||||||
customTools?: Record<string, PageAgentTool> // experimental
|
|
||||||
instructions?: InstructionsConfig
|
|
||||||
transformPageContent?: (content: string) => string | Promise<string>
|
|
||||||
experimentalScriptExecutionTool?: boolean // default: false
|
|
||||||
|
|
||||||
// Lifecycle Hooks (experimental)
|
|
||||||
onBeforeTask?: (agent, result) => void
|
|
||||||
onAfterTask?: (agent, result) => void
|
|
||||||
onBeforeStep?: (agent, stepCount) => void
|
|
||||||
onAfterStep?: (agent, history) => void
|
|
||||||
onDispose?: (agent, reason?) => void
|
|
||||||
|
|
||||||
// Page Controller Configuration
|
|
||||||
enableMask?: boolean // default: true
|
|
||||||
viewportExpansion?: number
|
|
||||||
interactiveBlacklist?: Element[]
|
|
||||||
interactiveWhitelist?: Element[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Packages
|
|
||||||
|
|
||||||
| Package | Description |
|
|
||||||
| ----------------------------- | ---------------------------------- |
|
|
||||||
| `page-agent` | Main entry with UI Panel |
|
|
||||||
| `@page-agent/core` | Core agent logic without UI |
|
|
||||||
| `@page-agent/llms` | LLM client with retry logic |
|
|
||||||
| `@page-agent/page-controller` | DOM operations and visual feedback |
|
|
||||||
| `@page-agent/ui` | Panel and i18n |
|
|
||||||
|
|
||||||
### Known Limitations
|
|
||||||
|
|
||||||
- Single-page application only (cannot navigate across pages)
|
|
||||||
- No visual recognition (relies on DOM structure)
|
|
||||||
- Limited interaction support (no hover, drag-drop, canvas operations)
|
|
||||||
- See [Limitations](https://alibaba.github.io/page-agent/#/docs/introduction/limitations) for details
|
|
||||||
|
|
||||||
### Acknowledgments
|
|
||||||
|
|
||||||
This project builds upon the excellent work of [browser-use](https://github.com/browser-use/browser-use). DOM processing components and prompts are adapted from browser-use (MIT License).
|
|
||||||
+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
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -9,28 +9,33 @@
|
|||||||
|
|
||||||
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
||||||
|
|
||||||
🌐 **English** | [中文](./README-zh.md)
|
🌐 **English** | [中文](./docs/README-zh.md)
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ 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.
|
||||||
|
- **🧠 Bring your own LLMs**
|
||||||
|
- **🎨 Pretty UI with human-in-the-loop**
|
||||||
|
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
|
||||||
|
|
||||||
And 😉
|
## 💡 Use Cases
|
||||||
|
|
||||||
- **🧪 `cross-page` control with an experimental chrome extension** - `packages/extension`
|
- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite needed.
|
||||||
|
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
|
||||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
|
||||||
|
- **Multi-page Agent** — Extend your agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -39,19 +44,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.0/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.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.4.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.1.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.4.2/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
|
||||||
|
|
||||||
### NPM Installation
|
### NPM Installation
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ npm install page-agent
|
|||||||
import { PageAgent } from 'page-agent'
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'qwen3.5-plus',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
})
|
})
|
||||||
@@ -72,25 +73,13 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('Click the login button')
|
await agent.execute('Click the login button')
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ Structure
|
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/
|
|
||||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
|
||||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
|
||||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
|
||||||
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
|
|
||||||
├── ui/ # Panel & i18n (npm: @page-agent/ui)
|
|
||||||
└── website/ # Demo & Documentation site
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development.
|
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development.
|
||||||
|
|
||||||
Please read [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
|
Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.
|
||||||
|
|
||||||
## 👏 Acknowledgments
|
## 👏 Acknowledgments
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.4.0] - 2026-02-27
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Update Terms of Use and Privacy Policy
|
||||||
|
- **Robust tool-call validation** - Action inputs are now validated against tool schemas individually, producing clear error messages (e.g. `Invalid input for action "click_element_by_index"`) instead of unreadable union parse errors
|
||||||
|
- **Primitive action input coercion** - Small models that output `{"click_element_by_index": 2}` instead of `{"click_element_by_index": {"index": 2}}` are now auto-corrected using tool schemas
|
||||||
|
- **Qwen model updates** - Added `qwen3.5-plus` as the default free testing model; disabled `enable_thinking` for Qwen models to avoid incompatible responses
|
||||||
|
- **Updated default LLM endpoint** - Migrated demo and extension to a new testing endpoint with legacy endpoint auto-migration
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Unified zod imports (`* as z`) across all packages for consistency
|
||||||
|
- Better Zod error formatting with `z.prettifyError()` in LLM client
|
||||||
|
- Exported `InvokeError` and `InvokeErrorType` as values (not just types) from `@page-agent/llms`
|
||||||
|
- Exported `SupportedLanguage` type from `@page-agent/core`
|
||||||
|
|
||||||
|
### Extension v0.1.8
|
||||||
|
|
||||||
|
- **Language setting** - Added language selector (System / English / 中文) in config panel
|
||||||
|
- **UI makeover** - New empty state with breathing glow and typing animation; ai-motion glow overlay while running; refined focus styles
|
||||||
|
- **Testing endpoint notice** - Shows terms of use notice when using the free testing API
|
||||||
|
- **Legacy endpoint migration** - Auto-migrates old Supabase testing endpoint to new endpoint on startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-02-13
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- **Lifecycle: `stop()` vs `dispose()`** - New `stop()` method to cancel the current task while keeping the agent reusable. `dispose()` is now terminal — a disposed agent cannot be reused. This affects both `PageAgentCore` and `PanelAgentAdapter`.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status
|
||||||
|
- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean "Task stopped" message instead of a raw error stack
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-02-11
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages
|
||||||
|
- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Pruned system prompts for lower token usage and faster responses
|
||||||
|
- Improved error handling during agent steps with better error messages
|
||||||
|
- Zod tree-shaking for smaller bundle size
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed indentation lost in DOM extraction caused by `trimLines`
|
||||||
|
- Fixed `gpt-5-mini` temperature configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.0] - 2026-02-02
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt
|
||||||
|
- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)
|
||||||
|
- Lazy-loaded mask module for faster initialization
|
||||||
|
- Better date formatting and error messages from LLM client
|
||||||
|
- Added `rawRequest` to step history for easier debugging
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles
|
||||||
|
- Fixed `AbortError` being incorrectly retried and shown to users
|
||||||
|
- Fixed mask not working correctly when starting a new task after stopping a previous one
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-01-19
|
||||||
|
|
||||||
|
### 🎉 First Stable Release
|
||||||
|
|
||||||
|
PageAgent is now ready for production use. The API is stable and breaking changes will follow semantic versioning.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
#### Core
|
||||||
|
|
||||||
|
- **PageAgent** - Main entry class with built-in UI Panel
|
||||||
|
- **PageAgentCore** - Headless agent class for custom UI or programmatic use
|
||||||
|
- **DOM Analysis** - Text-based DOM extraction with high-intensity dehydration
|
||||||
|
- **LLM Support** - Works with OpenAI, Claude, DeepSeek, Qwen, and other OpenAI-compatible APIs
|
||||||
|
- **Tool System** - Built-in tools for click, input, scroll, select, and more
|
||||||
|
- **Custom Tools** - Extend agent capabilities with your own tools (experimental)
|
||||||
|
- **Lifecycle Hooks** - Hook into agent execution (experimental)
|
||||||
|
- **Instructions System** - System-level and page-level instructions to guide agent behavior
|
||||||
|
- **Data Masking** - Transform page content before sending to LLM
|
||||||
|
|
||||||
|
#### Page Controller
|
||||||
|
|
||||||
|
- **Element Interactions** - Click, input text, select options, scroll
|
||||||
|
- **Visual Mask** - Blocks user interaction during automation
|
||||||
|
- **DOM Tree Extraction** - Efficient page structure extraction for LLM consumption
|
||||||
|
|
||||||
|
#### UI
|
||||||
|
|
||||||
|
- **Interactive Panel** - Real-time task progress and agent thinking display
|
||||||
|
- **Ask User Tool** - Agent can ask users for clarification
|
||||||
|
- **i18n Support** - English and Chinese localization
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PageAgentConfig {
|
||||||
|
// LLM Configuration (required)
|
||||||
|
baseURL: string
|
||||||
|
apiKey: string
|
||||||
|
model: string
|
||||||
|
temperature?: number
|
||||||
|
maxRetries?: number
|
||||||
|
customFetch?: typeof fetch
|
||||||
|
|
||||||
|
// Agent Configuration
|
||||||
|
language?: 'en-US' | 'zh-CN'
|
||||||
|
maxSteps?: number // default: 20
|
||||||
|
customTools?: Record<string, PageAgentTool> // experimental
|
||||||
|
instructions?: InstructionsConfig
|
||||||
|
transformPageContent?: (content: string) => string | Promise<string>
|
||||||
|
experimentalScriptExecutionTool?: boolean // default: false
|
||||||
|
|
||||||
|
// Lifecycle Hooks (experimental)
|
||||||
|
onBeforeTask?: (agent, result) => void
|
||||||
|
onAfterTask?: (agent, result) => void
|
||||||
|
onBeforeStep?: (agent, stepCount) => void
|
||||||
|
onAfterStep?: (agent, history) => void
|
||||||
|
onDispose?: (agent, reason?) => void
|
||||||
|
|
||||||
|
// Page Controller Configuration
|
||||||
|
enableMask?: boolean // default: true
|
||||||
|
viewportExpansion?: number
|
||||||
|
interactiveBlacklist?: Element[]
|
||||||
|
interactiveWhitelist?: Element[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
|
||||||
|
| Package | Description |
|
||||||
|
| ----------------------------- | ---------------------------------- |
|
||||||
|
| `page-agent` | Main entry with UI Panel |
|
||||||
|
| `@page-agent/core` | Core agent logic without UI |
|
||||||
|
| `@page-agent/llms` | LLM client with retry logic |
|
||||||
|
| `@page-agent/page-controller` | DOM operations and visual feedback |
|
||||||
|
| `@page-agent/ui` | Panel and i18n |
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
|
||||||
|
- Single-page application only (cannot navigate across pages)
|
||||||
|
- No visual recognition (relies on DOM structure)
|
||||||
|
- Limited interaction support (no hover, drag-drop, canvas operations)
|
||||||
|
- See [Limitations](https://alibaba.github.io/page-agent/docs/introduction/limitations) for details
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
|
||||||
|
This project builds upon the excellent work of [browser-use](https://github.com/browser-use/browser-use). DOM processing components and prompts are adapted from browser-use (MIT License).
|
||||||
@@ -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">
|
||||||
@@ -9,28 +9,33 @@
|
|||||||
|
|
||||||
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
||||||
|
|
||||||
🌐 [English](./README.md) | **中文**
|
🌐 [English](../README.md) | **中文**
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **🎯 轻松集成**
|
- **🎯 轻松集成**
|
||||||
- 无需 Python,无需无头浏览器,无需浏览器插件。纯页面内脚本。
|
- 无需 `浏览器插件` / `Python` / `无头浏览器`。
|
||||||
- **🔐 端侧运行**
|
- 纯页面内 JavaScript,一切都在你的网页中完成。
|
||||||
- **🧠 HTML 脱水**
|
- The best tool for your agent to control web pages.
|
||||||
- **💬 自然语言接口**
|
- **📖 基于文本的 DOM 操作**
|
||||||
- **🎨 HITL 交互界面**
|
- 无需截图,无需 OCR 或多模态模型。
|
||||||
|
- 无需特殊权限。
|
||||||
|
- **🧠 用你自己的 LLM**
|
||||||
|
- **🎨 精美 UI,支持人机协同**
|
||||||
|
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
|
||||||
|
|
||||||
以及 😉
|
## 💡 应用场景
|
||||||
|
|
||||||
- **🧪 实验性的 Chrome 扩展,支持跨页面控制** - `packages/extension`
|
- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,不需要重写后端。
|
||||||
|
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
|
||||||
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
|
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
|
||||||
|
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你的 Agent 跨标签页工作。
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -39,19 +44,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.0/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.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.4.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.1.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.4.2/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
|
||||||
|
|
||||||
### NPM 安装
|
### NPM 安装
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ npm install page-agent
|
|||||||
import { PageAgent } from 'page-agent'
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'qwen3.5-plus',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
})
|
})
|
||||||
@@ -72,25 +73,11 @@ 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)。
|
||||||
|
|
||||||
## 🏗️ 架构设计
|
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/
|
|
||||||
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
|
|
||||||
├── page-agent/ # Exported agent and demo(npm: page-agent)
|
|
||||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
|
||||||
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
|
|
||||||
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
|
|
||||||
└── website/ # 文档站点
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤝 贡献
|
## 🤝 贡献
|
||||||
|
|
||||||
欢迎社区贡献!请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解环境配置和本地开发说明。
|
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解环境配置和本地开发说明。
|
||||||
|
|
||||||
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
|
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
|
||||||
|
|
||||||
@@ -119,7 +106,7 @@ file and in the node_modules directory after installation.
|
|||||||
|
|
||||||
## 📄 许可证
|
## 📄 许可证
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
[MIT License](../LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Terms of Use & Privacy
|
||||||
|
|
||||||
|
**Last updated:** February 2026
|
||||||
|
|
||||||
|
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). This document covers the Page Agent software itself and the testing services we provide — **not** any third-party product or service built with it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Open Source Software Privacy
|
||||||
|
|
||||||
|
Page Agent (PageAgent.js and Page Agent Extension) is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and the maintainers do **not** have access to your browsing activity, page content, or task instructions through the software.
|
||||||
|
|
||||||
|
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
|
||||||
|
|
||||||
|
- You choose which LLM provider to use
|
||||||
|
- You may configure your own API endpoint at any time
|
||||||
|
- The project is open source and can be audited: https://github.com/alibaba/page-agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Testing API and Demo Disclaimer & Terms of Use
|
||||||
|
|
||||||
|
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of page-agent.
|
||||||
|
|
||||||
|
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this service, you agree to the following terms:
|
||||||
|
|
||||||
|
**No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
|
||||||
|
|
||||||
|
**Zero Data Retention**: We do not store, log, or use your prompts or webpage data (DOM) for model training. All data is processed in-transit and immediately discarded. However, we do temporarily process necessary, identity-agnostic network data (such as IP addresses) solely for rate-limiting and anti-abuse purposes.
|
||||||
|
|
||||||
|
**Independent Infrastructure**: The `page-agent` open-source software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and no built-in backend. To facilitate easy testing, the maintainers have independently purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) FC and BaiLian Qwen models). This project is not affiliated with, nor endorsed by, Alibaba Cloud.
|
||||||
|
|
||||||
|
**No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
|
||||||
|
|
||||||
|
**"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this demo.
|
||||||
|
|
||||||
|
**Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the default BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
|
||||||
|
|
||||||
|
**Note**: This service processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this demo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Browser Extension (Page Agent Ext)
|
||||||
|
|
||||||
|
### Data Processing
|
||||||
|
|
||||||
|
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
|
||||||
|
|
||||||
|
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
|
||||||
|
|
||||||
|
- Your task instructions (natural language commands)
|
||||||
|
- Simplified page structure (cleaned DOM) of all pages under the extension's control
|
||||||
|
|
||||||
|
are sent to the LLM API endpoint configured in **your settings**.
|
||||||
|
|
||||||
|
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
|
||||||
|
|
||||||
|
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
|
||||||
|
|
||||||
|
**If you use the built-in testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local`
|
||||||
|
- **No cloud sync**: Configuration is not synced to any external server
|
||||||
|
- **No analytics**: The extension does not include any analytics or tracking code
|
||||||
|
|
||||||
|
### Your Control
|
||||||
|
|
||||||
|
- The extension is open source and can be audited by anyone
|
||||||
|
- You choose which LLM provider to use
|
||||||
|
- You may configure your own API endpoint at any time
|
||||||
|
- You can clear all stored data by removing the extension
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
We may update these terms as the project evolves.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
https://github.com/alibaba/page-agent/issues
|
||||||
Generated
+735
-625
File diff suppressed because it is too large
Load Diff
+15
-13
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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",
|
||||||
@@ -36,30 +35,33 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.3.1",
|
"@commitlint/cli": "^20.4.2",
|
||||||
"@commitlint/config-conventional": "^20.3.1",
|
"@commitlint/config-conventional": "^20.4.2",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@microsoft/api-extractor": "^7.55.2",
|
"@microsoft/api-extractor": "^7.57.3",
|
||||||
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.3.0",
|
||||||
|
"@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.3.1",
|
||||||
"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.13.0",
|
||||||
"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.2",
|
||||||
"eslint-plugin-react-x": "^2.7.2",
|
"eslint-plugin-react-x": "^2.13.0",
|
||||||
"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.56.1",
|
||||||
"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",
|
||||||
|
"vite-bundle-analyzer": "^1.3.6"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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.0",
|
"@page-agent/llms": "1.4.2",
|
||||||
"@page-agent/page-controller": "1.1.0"
|
"@page-agent/page-controller": "1.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-138
@@ -3,15 +3,15 @@
|
|||||||
* 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 zod from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { type PageAgentConfig } from './config'
|
import { type PageAgentConfig, type SupportedLanguage } from './config'
|
||||||
import { DEFAULT_MAX_STEPS } from './config/constants'
|
import { DEFAULT_MAX_STEPS } from './config/constants'
|
||||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||||
import { tools } from './tools'
|
import { tools } from './tools'
|
||||||
import {
|
import type {
|
||||||
AgentActivity,
|
AgentActivity,
|
||||||
AgentReflection,
|
AgentReflection,
|
||||||
AgentStatus,
|
AgentStatus,
|
||||||
@@ -21,17 +21,26 @@ import {
|
|||||||
MacroToolInput,
|
MacroToolInput,
|
||||||
MacroToolResult,
|
MacroToolResult,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { normalizeResponse, trimLines, uid, waitFor } from './utils'
|
import { assert, normalizeResponse, uid, waitFor } from './utils'
|
||||||
import { assert } from './utils/assert'
|
|
||||||
|
|
||||||
export { type PageAgentConfig }
|
export { type PageAgentConfig }
|
||||||
|
export type { SupportedLanguage }
|
||||||
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)
|
||||||
@@ -50,14 +59,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)
|
||||||
@@ -66,23 +79,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()
|
||||||
|
|
||||||
@@ -166,15 +177,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()
|
||||||
@@ -203,44 +223,44 @@ 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: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
let step = 0
|
||||||
let step = 0
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await this.#generateObservations(step)
|
try {
|
||||||
|
console.group(`step: ${step}`)
|
||||||
|
|
||||||
await onBeforeStep?.(this, step)
|
await onBeforeStep?.(this, step)
|
||||||
|
|
||||||
console.group(`step: ${step}`)
|
// observe
|
||||||
|
|
||||||
// abort
|
console.log(chalk.blue.bold('👀 Observing...'))
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
|
||||||
|
|
||||||
// Thinking
|
this.#states.browserState = await this.pageController.getBrowserState()
|
||||||
console.log(chalk.blue('Thinking...'))
|
await this.#handleObservations(step)
|
||||||
this.#emitActivity({ type: 'thinking' })
|
|
||||||
|
|
||||||
// invoke LLM
|
// assemble prompts
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
{ role: 'system' as const, content: this.#getSystemPrompt() },
|
||||||
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
|
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
|
||||||
]
|
]
|
||||||
|
|
||||||
const tools = { AgentOutput: this.#packMacroTool() }
|
const macroTool = { AgentOutput: this.#packMacroTool() }
|
||||||
|
|
||||||
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
// invoke LLM
|
||||||
|
|
||||||
|
console.log(chalk.blue.bold('🧠 Thinking...'))
|
||||||
|
this.#emitActivity({ type: 'thinking' })
|
||||||
|
|
||||||
|
const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {
|
||||||
toolChoiceName: 'AgentOutput',
|
toolChoiceName: 'AgentOutput',
|
||||||
normalizeResponse,
|
normalizeResponse: (res) => normalizeResponse(res, this.tools),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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
|
||||||
@@ -270,22 +290,12 @@ export class PageAgentCore extends EventTarget {
|
|||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
console.log(chalk.green('Step finished:'), actionName)
|
|
||||||
console.groupEnd()
|
|
||||||
|
|
||||||
await onAfterStep?.(this, this.history)
|
await onAfterStep?.(this, this.history)
|
||||||
|
|
||||||
step++
|
console.groupEnd()
|
||||||
if (step > this.config.maxSteps) {
|
|
||||||
this.#onDone(false)
|
// finish task if done
|
||||||
const result: ExecutionResult = {
|
|
||||||
success: false,
|
|
||||||
data: 'Step count exceeded maximum limit',
|
|
||||||
history: this.history,
|
|
||||||
}
|
|
||||||
await onAfterTask?.(this, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
if (actionName === 'done') {
|
if (actionName === 'done') {
|
||||||
const success = action.input?.success ?? false
|
const success = action.input?.success ?? false
|
||||||
const text = action.input?.text || 'no text provided'
|
const text = action.input?.text || 'no text provided'
|
||||||
@@ -299,19 +309,41 @@ export class PageAgentCore extends EventTarget {
|
|||||||
await onAfterTask?.(this, result)
|
await onAfterTask?.(this, result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.groupEnd() // to prevent nested groups
|
||||||
|
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
|
||||||
|
|
||||||
|
console.error('Task failed', error)
|
||||||
|
const errorMessage = isAbortError ? 'Task stopped' : String(error)
|
||||||
|
this.#emitActivity({ type: 'error', message: errorMessage })
|
||||||
|
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
|
||||||
|
this.#emitHistoryChange()
|
||||||
|
this.#onDone(false)
|
||||||
|
const result: ExecutionResult = {
|
||||||
|
success: false,
|
||||||
|
data: errorMessage,
|
||||||
|
history: this.history,
|
||||||
|
}
|
||||||
|
await onAfterTask?.(this, result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Task failed', error)
|
step++
|
||||||
const errorMessage = String(error)
|
if (step > this.config.maxSteps) {
|
||||||
this.#emitActivity({ type: 'error', message: errorMessage })
|
const errorMessage = 'Step count exceeded maximum limit'
|
||||||
this.#onDone(false)
|
this.history.push({ type: 'error', message: errorMessage })
|
||||||
const result: ExecutionResult = {
|
this.#emitHistoryChange()
|
||||||
success: false,
|
this.#onDone(false)
|
||||||
data: errorMessage,
|
const result: ExecutionResult = {
|
||||||
history: this.history,
|
success: false,
|
||||||
|
data: errorMessage,
|
||||||
|
history: this.history,
|
||||||
|
}
|
||||||
|
await onAfterTask?.(this, result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
await onAfterTask?.(this, result)
|
|
||||||
return result
|
await waitFor(0.4) // @TODO: configurable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,29 +360,27 @@ export class PageAgentCore extends EventTarget {
|
|||||||
const tools = this.tools
|
const tools = this.tools
|
||||||
|
|
||||||
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
||||||
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
return z.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionSchema = zod.union(
|
const actionSchema = z.union(actionSchemas as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])
|
||||||
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
|
|
||||||
)
|
|
||||||
|
|
||||||
const macroToolSchema = zod.object({
|
const macroToolSchema = z.object({
|
||||||
// thinking: zod.string().optional(),
|
// thinking: z.string().optional(),
|
||||||
evaluation_previous_goal: zod.string().optional(),
|
evaluation_previous_goal: z.string().optional(),
|
||||||
memory: zod.string().optional(),
|
memory: z.string().optional(),
|
||||||
next_goal: zod.string().optional(),
|
next_goal: z.string().optional(),
|
||||||
action: actionSchema,
|
action: actionSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
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 z.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]
|
||||||
@@ -371,7 +401,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)
|
||||||
|
|
||||||
@@ -395,9 +425,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
|
||||||
@@ -427,17 +459,17 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instructions from config and format as XML block
|
* Get instructions from config
|
||||||
*/
|
*/
|
||||||
async #getInstructions(): Promise<string> {
|
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) {
|
||||||
@@ -465,23 +497,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.`
|
||||||
@@ -491,12 +528,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>
|
||||||
@@ -506,38 +556,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
|
||||||
@@ -548,9 +596,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) {
|
||||||
@@ -560,25 +617,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,17 +138,16 @@ 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>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
You must ALWAYS respond with a valid JSON in this exact format:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
|
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
|
||||||
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
|
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
|
||||||
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence."
|
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
|
||||||
"action":{"action": {// action-specific parameter}}
|
"action":{
|
||||||
|
"Action name": {// Action parameters}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</output>
|
</output>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Internal tools for PageAgent.
|
* Internal tools for PageAgent.
|
||||||
* @note Adapted from browser-use
|
* @note Adapted from browser-use
|
||||||
*/
|
*/
|
||||||
import zod, { type z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import type { PageAgentCore } from '../PageAgentCore'
|
import type { PageAgentCore } from '../PageAgentCore'
|
||||||
import { waitFor } from '../utils'
|
import { waitFor } from '../utils'
|
||||||
@@ -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: z.object({
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
success: zod.boolean().default(true),
|
success: z.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: z.object({
|
||||||
inputSchema: zod.object({
|
seconds: z.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.`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -76,8 +67,8 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
|
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
question: zod.string(),
|
question: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
if (!this.onAskUser) {
|
if (!this.onAskUser) {
|
||||||
@@ -93,8 +84,8 @@ tools.set(
|
|||||||
'click_element_by_index',
|
'click_element_by_index',
|
||||||
tool({
|
tool({
|
||||||
description: 'Click element by index',
|
description: 'Click element by index',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.clickElement(input.index)
|
const result = await this.pageController.clickElement(input.index)
|
||||||
@@ -106,10 +97,10 @@ 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: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.inputText(input.index, input.text)
|
const result = await this.pageController.inputText(input.index, input.text)
|
||||||
@@ -123,9 +114,9 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Select dropdown option for interactive element index by the text of the option you want to select',
|
'Select dropdown option for interactive element index by the text of the option you want to select',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.selectOption(input.index, input.text)
|
const result = await this.pageController.selectOption(input.index, input.text)
|
||||||
@@ -140,13 +131,12 @@ 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: z.object({
|
||||||
inputSchema: zod.object({
|
down: z.boolean().default(true),
|
||||||
down: zod.boolean().default(true),
|
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
||||||
num_pages: zod.number().min(0).max(10).optional().default(0.1),
|
pixels: z.number().int().min(0).optional(),
|
||||||
pixels: zod.number().int().min(0).optional(),
|
index: z.number().int().min(0).optional(),
|
||||||
index: zod.number().int().min(0).optional(),
|
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scroll({
|
const result = await this.pageController.scroll({
|
||||||
@@ -158,15 +148,18 @@ 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: z.object({
|
||||||
right: zod.boolean().default(true),
|
right: z.boolean().default(true),
|
||||||
pixels: zod.number().int().min(0),
|
pixels: z.number().int().min(0),
|
||||||
index: zod.number().int().min(0).optional(),
|
index: z.number().int().min(0).optional(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scrollHorizontally(input)
|
const result = await this.pageController.scrollHorizontally(input)
|
||||||
@@ -180,8 +173,8 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
|
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
script: zod.string(),
|
script: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.executeJavascript(input.script)
|
const result = await this.pageController.executeJavascript(input.script)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import chalk from 'chalk'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple assertion function that throws an error if the condition is falsy
|
|
||||||
* @param condition - The condition to assert
|
|
||||||
* @param message - Optional error message
|
|
||||||
* @throws Error if condition is falsy
|
|
||||||
*/
|
|
||||||
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
|
|
||||||
if (!condition) {
|
|
||||||
const errorMessage = message ?? 'Assertion failed'
|
|
||||||
|
|
||||||
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
|
|
||||||
|
|
||||||
throw new Error(errorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import type { PageAgentTool } from '../tools'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize LLM response and fix common format issues.
|
* Normalize LLM response and fix common format issues.
|
||||||
@@ -9,9 +13,10 @@ import chalk from 'chalk'
|
|||||||
* - Arguments wrapped as double JSON string
|
* - Arguments wrapped as double JSON string
|
||||||
* - Nested function call format
|
* - Nested function call format
|
||||||
* - Missing action field (fallback to wait)
|
* - Missing action field (fallback to wait)
|
||||||
|
* - Primitive action input for single-field tools (e.g. `{"click_element_by_index": 2}`)
|
||||||
* - etc.
|
* - etc.
|
||||||
*/
|
*/
|
||||||
export function normalizeResponse(response: any): any {
|
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
|
||||||
let resolvedArguments = null as any
|
let resolvedArguments = null as any
|
||||||
|
|
||||||
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
||||||
@@ -78,6 +83,11 @@ export function normalizeResponse(response: any): any {
|
|||||||
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate and fix action input using tool schemas
|
||||||
|
if (resolvedArguments.action && tools) {
|
||||||
|
resolvedArguments.action = validateAction(resolvedArguments.action, tools)
|
||||||
|
}
|
||||||
|
|
||||||
// fix incomplete formats
|
// fix incomplete formats
|
||||||
if (!resolvedArguments.action) {
|
if (!resolvedArguments.action) {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
|
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
|
||||||
@@ -108,6 +118,55 @@ export function normalizeResponse(response: any): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate action against tool schemas. Provides clear error messages
|
||||||
|
* instead of letting the union schema produce unreadable errors.
|
||||||
|
*
|
||||||
|
* Also coerces primitive inputs for single-field tools:
|
||||||
|
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
|
||||||
|
*/
|
||||||
|
function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
||||||
|
if (typeof action !== 'object' || action === null) return action
|
||||||
|
|
||||||
|
const toolName = Object.keys(action)[0]
|
||||||
|
if (!toolName) return action
|
||||||
|
|
||||||
|
const tool = tools.get(toolName)
|
||||||
|
if (!tool) {
|
||||||
|
const available = Array.from(tools.keys()).join(', ')
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Unknown action "${toolName}". Available: ${available}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = action[toolName]
|
||||||
|
const schema = tool.inputSchema
|
||||||
|
|
||||||
|
// coerce primitive input for single-field tools
|
||||||
|
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
|
||||||
|
const requiredKey = Object.keys(schema.shape).find(
|
||||||
|
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
|
||||||
|
)
|
||||||
|
if (requiredKey) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(`[normalizeResponse] coercing primitive action input for "${toolName}"`)
|
||||||
|
)
|
||||||
|
value = { [requiredKey]: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = schema.safeParse(value)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [toolName]: result.data }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely parse JSON, return original input if not json.
|
* Safely parse JSON, return original input if not json.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,27 +1,7 @@
|
|||||||
|
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))
|
||||||
}
|
}
|
||||||
@@ -37,15 +17,6 @@ export function truncate(text: string, maxLength: number): string {
|
|||||||
|
|
||||||
//
|
//
|
||||||
|
|
||||||
export function trimLines(text: string): string {
|
|
||||||
return text
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
export function randomID(existingIDs?: string[]): string {
|
export function randomID(existingIDs?: string[]): string {
|
||||||
let id = Math.random().toString(36).substring(2, 11)
|
let id = Math.random().toString(36).substring(2, 11)
|
||||||
|
|
||||||
@@ -85,3 +56,19 @@ export function uid() {
|
|||||||
ids.push(id)
|
ids.push(id)
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple assertion function that throws an error if the condition is falsy
|
||||||
|
* @param condition - The condition to assert
|
||||||
|
* @param message - Optional error message
|
||||||
|
* @throws Error if condition is falsy
|
||||||
|
*/
|
||||||
|
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
const errorMessage = message ?? 'Assertion failed'
|
||||||
|
|
||||||
|
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
|
||||||
|
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,57 +1,5 @@
|
|||||||
# Privacy Policy for Page Agent Extension
|
# Privacy Policy for Page Agent Extension
|
||||||
|
|
||||||
**Last updated:** January 2026
|
This document has moved. Please see our full **[Terms of Use & Privacy](../../docs/terms-and-privacy.md)**.
|
||||||
|
|
||||||
## Overview
|
Online: https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md
|
||||||
|
|
||||||
Page Agent Extension is a browser automation tool that uses AI to help you interact with web pages. This policy explains what data is processed and under what circumstances.
|
|
||||||
|
|
||||||
## Data Processing
|
|
||||||
|
|
||||||
### Local Processing
|
|
||||||
|
|
||||||
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension itself.
|
|
||||||
|
|
||||||
### Data Transmission
|
|
||||||
|
|
||||||
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
|
|
||||||
|
|
||||||
- Your task instructions (natural language commands)
|
|
||||||
- Sanitized page structure (simplified DOM, excluding sensitive form values)
|
|
||||||
|
|
||||||
are sent to the LLM API endpoint configured in **your settings**.
|
|
||||||
|
|
||||||
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
|
|
||||||
|
|
||||||
**If you use our testing endpoint**, your requests are proxied to [DeepSeek](https://deepseek.com) for AI processing. Regarding this test endpoint:
|
|
||||||
|
|
||||||
- This endpoint is provided for evaluation purposes only and is not recommended for production or daily use
|
|
||||||
- We do **not** store your task content, page content, or visited URLs
|
|
||||||
- Minimal logging (timestamps, request metadata, IP addresses) may occur for abuse prevention and service stability
|
|
||||||
- DeepSeek's [Privacy Policy](https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html) applies to their processing of your requests
|
|
||||||
|
|
||||||
## Data Storage
|
|
||||||
|
|
||||||
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local`
|
|
||||||
- **No cloud sync**: Configuration is not synced to any external server
|
|
||||||
- **No analytics**: The extension does not include any analytics or tracking code
|
|
||||||
|
|
||||||
## Your Control
|
|
||||||
|
|
||||||
- The extension is open source and can be audited by anyone
|
|
||||||
- You choose which LLM provider to use
|
|
||||||
- You may configure your own API endpoint at any time
|
|
||||||
- You can clear all stored data by removing the extension
|
|
||||||
|
|
||||||
## Changes to This Policy
|
|
||||||
|
|
||||||
We may update this policy as the extension evolves. Significant changes will be noted in the extension's release notes.
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
For questions about this privacy policy:
|
|
||||||
https://github.com/alibaba/page-agent/issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Source code: https://github.com/alibaba/page-agent
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/index.css",
|
"css": "src/assets/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/lib/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}.json"
|
"@magicui": "https://magicui.design/r/{name}.json"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,7 +47,6 @@ import type {
|
|||||||
AgentStatus,
|
AgentStatus,
|
||||||
ExecutionResult,
|
ExecutionResult,
|
||||||
HistoricalEvent,
|
HistoricalEvent,
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
} from '@page-agent/core'
|
||||||
|
|
||||||
// Wait for extension injection (up to 1 second)
|
// Wait for extension injection (up to 1 second)
|
||||||
@@ -48,55 +61,49 @@ async function waitForExtension(timeout = 1000): Promise<boolean> {
|
|||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
if (await waitForExtension()) {
|
if (await waitForExtension()) {
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
|
||||||
'Click the login button',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
{
|
apiKey: 'your-api-key',
|
||||||
baseURL: 'https://api.openai.com/v1',
|
model: 'gpt-5.2',
|
||||||
apiKey: 'your-api-key',
|
onStatusChange: (status) => console.log('Status:', status),
|
||||||
model: 'gpt-5.2',
|
onActivity: (activity) => console.log('Activity:', activity),
|
||||||
},
|
})
|
||||||
{
|
|
||||||
onStatusChange: (status) => console.log('Status:', status),
|
|
||||||
onActivity: (activity) => console.log('Activity:', activity),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
console.log('Result:', result)
|
console.log('Result:', result)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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, llmConfig, hooks?)`
|
#### `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 |
|
||||||
| `llmConfig` | `LLMConfig` | Yes | LLM configuration |
|
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
||||||
| `hooks` | `ExecuteHooks` | No | Event 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,37 +111,31 @@ import type {
|
|||||||
AgentStatus,
|
AgentStatus,
|
||||||
ExecutionResult,
|
ExecutionResult,
|
||||||
HistoricalEvent,
|
HistoricalEvent,
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
} from '@page-agent/core'
|
||||||
|
|
||||||
export interface ExecuteHooks {
|
export interface ExecuteConfig {
|
||||||
|
baseURL: string
|
||||||
|
apiKey: string
|
||||||
|
model: string
|
||||||
|
|
||||||
|
// Include the initial tab where page JS starts. Default: true.
|
||||||
|
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 = (
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
task: string,
|
|
||||||
llmConfig: LLMConfig,
|
|
||||||
hooks?: ExecuteHooks
|
|
||||||
) => 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 =
|
||||||
@@ -145,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 =
|
||||||
@@ -164,17 +157,7 @@ type HistoricalEvent =
|
|||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||||
```
|
```
|
||||||
|
|
||||||
### LLMConfig
|
`ExecutionResult`
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface LLMConfig {
|
|
||||||
baseURL: string // e.g. 'https://api.openai.com/v1'
|
|
||||||
apiKey: string
|
|
||||||
model: string // e.g. 'gpt-5.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ExecutionResult
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ExecutionResult {
|
interface ExecutionResult {
|
||||||
@@ -195,62 +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')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Event Hooks
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await window.PAGE_AGENT_EXT!.execute(
|
|
||||||
'Navigate to the settings page',
|
|
||||||
llmConfig,
|
|
||||||
{
|
|
||||||
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
|
### Stop the Current Task
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Start a task
|
window.PAGE_AGENT_EXT!.stop()
|
||||||
window.PAGE_AGENT_EXT!.execute('Scroll through all pages', llmConfig)
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -258,25 +201,25 @@ import type {
|
|||||||
AgentStatus,
|
AgentStatus,
|
||||||
ExecutionResult,
|
ExecutionResult,
|
||||||
HistoricalEvent,
|
HistoricalEvent,
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
} from '@page-agent/core'
|
||||||
|
|
||||||
|
interface ExecuteConfig {
|
||||||
|
baseURL: string
|
||||||
|
apiKey: string
|
||||||
|
model: string
|
||||||
|
includeInitialTab?: boolean
|
||||||
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
onActivity?: (activity: AgentActivity) => void
|
||||||
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
PAGE_AGENT_EXT_VERSION?: string
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
PAGE_AGENT_EXT?: {
|
PAGE_AGENT_EXT?: {
|
||||||
version: string
|
version: string
|
||||||
execute: (
|
execute: Execute
|
||||||
task: string,
|
stop: () => void
|
||||||
llmConfig: LLMConfig,
|
|
||||||
hooks?: {
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
onDispose?: () => void
|
|
||||||
}
|
|
||||||
) => Promise<ExecutionResult>
|
|
||||||
dispose: () => void
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,283 +0,0 @@
|
|||||||
# Page Agent 浏览器插件 API
|
|
||||||
|
|
||||||
本文档介绍如何在网页应用中接入 Page Agent 浏览器插件。
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
### 1. 安装浏览器插件
|
|
||||||
|
|
||||||
从 Chrome 应用商店安装 Page Agent 插件。
|
|
||||||
|
|
||||||
### 2. 安装类型定义(推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @page-agent/core --save-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 配置认证
|
|
||||||
|
|
||||||
插件在页面加载后检测 `localStorage` 中的 token,匹配时才会注入 API。
|
|
||||||
|
|
||||||
1. 打开插件的侧边栏面板,获取授权 token
|
|
||||||
2. 在页面中设置 token:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
// 等待插件注入(最多 1 秒)
|
|
||||||
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
|
||||||
const start = Date.now()
|
|
||||||
while (Date.now() - start < timeout) {
|
|
||||||
if (window.PAGE_AGENT_EXT) return true
|
|
||||||
await new Promise((r) => setTimeout(r, 100))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用
|
|
||||||
if (await waitForExtension()) {
|
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
|
||||||
'点击登录按钮',
|
|
||||||
{
|
|
||||||
baseURL: 'https://api.openai.com/v1',
|
|
||||||
apiKey: 'your-api-key',
|
|
||||||
model: 'gpt-5.2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onStatusChange: (status) => console.log('状态:', status),
|
|
||||||
onActivity: (activity) => console.log('活动:', activity),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
console.log('结果:', result)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 全局 API
|
|
||||||
|
|
||||||
插件在 `window` 对象上注入以下 API:
|
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT_VERSION`
|
|
||||||
|
|
||||||
插件版本号字符串(例如 `"1.0.0"`)。单独暴露版本号,方便在访问主 API 对象前进行版本检查。
|
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT`
|
|
||||||
|
|
||||||
主 API 命名空间对象,包含:
|
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.execute(task, llmConfig, hooks?)`
|
|
||||||
|
|
||||||
执行 Agent 任务。
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
|
|
||||||
| 名称 | 类型 | 必填 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `task` | `string` | 是 | 任务描述 |
|
|
||||||
| `llmConfig` | `LLMConfig` | 是 | LLM 配置 |
|
|
||||||
| `hooks` | `ExecuteHooks` | 否 | 事件回调 |
|
|
||||||
|
|
||||||
**返回:** `Promise<ExecutionResult>`
|
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.dispose()`
|
|
||||||
|
|
||||||
停止并销毁当前运行的 Agent。
|
|
||||||
|
|
||||||
## 类型定义
|
|
||||||
|
|
||||||
安装 `@page-agent/core` 获取完整类型:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
export interface ExecuteHooks {
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
onDispose?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Execute = (
|
|
||||||
task: string,
|
|
||||||
llmConfig: LLMConfig,
|
|
||||||
hooks?: ExecuteHooks
|
|
||||||
) => Promise<ExecutionResult>
|
|
||||||
```
|
|
||||||
|
|
||||||
### AgentStatus
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
|
||||||
```
|
|
||||||
|
|
||||||
| 状态 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `idle` | 空闲,准备执行 |
|
|
||||||
| `running` | 正在执行任务 |
|
|
||||||
| `completed` | 任务成功完成 |
|
|
||||||
| `error` | 任务执行失败 |
|
|
||||||
|
|
||||||
### AgentActivity
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type AgentActivity =
|
|
||||||
| { type: 'thinking' }
|
|
||||||
| { type: 'executing'; tool: string; input: unknown }
|
|
||||||
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
|
||||||
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
|
||||||
| { type: 'error'; message: string }
|
|
||||||
```
|
|
||||||
|
|
||||||
| 类型 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `thinking` | Agent 正在分析页面并规划 |
|
|
||||||
| `executing` | 正在执行工具操作 |
|
|
||||||
| `executed` | 工具执行完成 |
|
|
||||||
| `retrying` | 失败后重试 |
|
|
||||||
| `error` | 发生错误 |
|
|
||||||
|
|
||||||
### HistoricalEvent
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type HistoricalEvent =
|
|
||||||
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
|
||||||
| { type: 'observation'; content: string }
|
|
||||||
| { type: 'user_takeover' }
|
|
||||||
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
|
||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
|
||||||
```
|
|
||||||
|
|
||||||
### LLMConfig
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface LLMConfig {
|
|
||||||
baseURL: string // 例如 'https://api.openai.com/v1'
|
|
||||||
apiKey: string
|
|
||||||
model: string // 例如 'gpt-5.2'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ExecutionResult
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ExecutionResult {
|
|
||||||
success: boolean
|
|
||||||
data: string
|
|
||||||
history: HistoricalEvent[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 基础执行
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
|
||||||
'在邮箱输入框填入 test@example.com 然后点击提交',
|
|
||||||
{
|
|
||||||
baseURL: 'https://api.openai.com/v1',
|
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
|
||||||
model: 'gpt-5.2',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('任务完成:', result.data)
|
|
||||||
} else {
|
|
||||||
console.error('任务失败')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用事件回调
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await window.PAGE_AGENT_EXT!.execute(
|
|
||||||
'导航到设置页面',
|
|
||||||
llmConfig,
|
|
||||||
{
|
|
||||||
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('滚动浏览所有页面', llmConfig)
|
|
||||||
|
|
||||||
// 稍后停止
|
|
||||||
window.PAGE_AGENT_EXT!.dispose()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Window 类型声明
|
|
||||||
|
|
||||||
如果不使用 `@page-agent/core`,可以添加以下声明:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
LLMConfig,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
PAGE_AGENT_EXT_VERSION?: string
|
|
||||||
PAGE_AGENT_EXT?: {
|
|
||||||
version: string
|
|
||||||
execute: (
|
|
||||||
task: string,
|
|
||||||
llmConfig: LLMConfig,
|
|
||||||
hooks?: {
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
onDispose?: () => void
|
|
||||||
}
|
|
||||||
) => Promise<ExecutionResult>
|
|
||||||
dispose: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.2",
|
"version": "0.1.8",
|
||||||
"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.0",
|
|
||||||
"@page-agent/llms": "1.1.0",
|
|
||||||
"@page-agent/page-controller": "1.1.0",
|
|
||||||
"@page-agent/ui": "1.1.0",
|
|
||||||
"@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,33 @@
|
|||||||
"@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.37",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@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",
|
||||||
"lucide-react": "^0.563.0",
|
"idb": "^8.0.3",
|
||||||
"motion": "^12.30.0",
|
"lucide-react": "^0.575.0",
|
||||||
|
"motion": "^12.34.3",
|
||||||
"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.9.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"wxt": "^0.20.18"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@page-agent/core": "1.4.2",
|
||||||
|
"@page-agent/llms": "1.4.2",
|
||||||
|
"@page-agent/page-controller": "1.4.2",
|
||||||
|
"@page-agent/ui": "1.4.2",
|
||||||
|
"ai-motion": "^0.4.8",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Page Agent Ext"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Open Page Agent"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Page Agent Ext"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "打开 Page Agent"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ function detectLanguage(): 'en-US' | 'zh-CN' {
|
|||||||
* - can be used from a side panel or a content script
|
* - can be used from a side panel or a content script
|
||||||
*/
|
*/
|
||||||
export class MultiPageAgent extends PageAgentCore {
|
export class MultiPageAgent extends PageAgentCore {
|
||||||
constructor(config: Omit<PageAgentConfig, 'pageController'>) {
|
constructor(config: Omit<PageAgentConfig, 'pageController'> & { includeInitialTab?: boolean }) {
|
||||||
// multi page controller
|
// multi page controller
|
||||||
const tabsController = new TabsController()
|
const tabsController = new TabsController()
|
||||||
const pageController = new RemotePageController(tabsController)
|
const pageController = new RemotePageController(tabsController)
|
||||||
@@ -31,6 +31,9 @@ export class MultiPageAgent extends PageAgentCore {
|
|||||||
`Default working language: **${targetLanguage}**`
|
`Default working language: **${targetLanguage}**`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// include initial tab for controlling
|
||||||
|
const includeInitialTab = config.includeInitialTab ?? true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the agent is in side-panel and user closed the side-panel.
|
* When the agent is in side-panel and user closed the side-panel.
|
||||||
* There is no chance for isAgentRunning to be set false.
|
* There is no chance for isAgentRunning to be set false.
|
||||||
@@ -47,7 +50,7 @@ export class MultiPageAgent extends PageAgentCore {
|
|||||||
customSystemPrompt: systemPrompt,
|
customSystemPrompt: systemPrompt,
|
||||||
|
|
||||||
onBeforeTask: async (agent) => {
|
onBeforeTask: async (agent) => {
|
||||||
await tabsController.init(agent.task)
|
await tabsController.init(agent.task, includeInitialTab)
|
||||||
|
|
||||||
heartBeatInterval = window.setInterval(() => {
|
heartBeatInterval = window.setInterval(() => {
|
||||||
chrome.storage.local.set({
|
chrome.storage.local.set({
|
||||||
@@ -71,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)
|
||||||
@@ -81,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)
|
||||||
@@ -12,7 +29,9 @@ export class TabsController extends EventTarget {
|
|||||||
private task: string = ''
|
private task: string = ''
|
||||||
private windowId: number | null = null
|
private windowId: number | null = null
|
||||||
|
|
||||||
async init(task: string) {
|
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,23 +39,36 @@ 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',
|
||||||
})
|
})
|
||||||
|
|
||||||
this.initialTabId = result.tabId
|
this.initialTabId = result.tabId
|
||||||
this.currentTabId = result.tabId
|
|
||||||
|
|
||||||
this.tabs.push({
|
|
||||||
id: result.tabId,
|
|
||||||
isInitial: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!this.initialTabId) {
|
if (!this.initialTabId) {
|
||||||
throw new Error('Failed to get initial tab ID')
|
throw new Error('Failed to get initial tab ID')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeInitialTab) {
|
||||||
|
this.currentTabId = this.initialTabId
|
||||||
|
|
||||||
|
// update tab status immediately
|
||||||
|
const info = await sendMessage({
|
||||||
|
type: 'TAB_CONTROL',
|
||||||
|
action: 'get_tab_info',
|
||||||
|
payload: { tabId: this.initialTabId },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.tabs.push({
|
||||||
|
id: result.tabId,
|
||||||
|
isInitial: true,
|
||||||
|
url: info.url,
|
||||||
|
title: info.title,
|
||||||
|
status: info.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateCurrentTabId(this.currentTabId)
|
await this.updateCurrentTabId(this.currentTabId)
|
||||||
|
|
||||||
const tabChangeHandler = (message: any): void => {
|
const tabChangeHandler = (message: any): void => {
|
||||||
@@ -68,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,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 },
|
||||||
@@ -102,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 },
|
||||||
@@ -116,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: {
|
||||||
@@ -129,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 },
|
||||||
@@ -196,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,9 +268,24 @@ export class TabsController extends EventTarget {
|
|||||||
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`
|
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!this.tabs.length) {
|
||||||
|
summaries.push('\nNo tabs available. Open a tab if needed.')
|
||||||
|
}
|
||||||
|
|
||||||
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'))
|
||||||
}
|
}
|
||||||
@@ -251,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 = [
|
||||||
@@ -269,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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
|
|
||||||
// Demo LLM for testing
|
// Demo LLM for testing
|
||||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
export const DEMO_MODEL = 'qwen3.5-plus'
|
||||||
export const DEMO_BASE_URL =
|
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
export const DEMO_API_KEY = 'NA'
|
||||||
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
|
||||||
|
|
||||||
export const DEMO_CONFIG: LLMConfig = {
|
export const DEMO_CONFIG: LLMConfig = {
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: DEMO_API_KEY,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: DEMO_BASE_URL,
|
||||||
model: DEMO_MODEL,
|
model: DEMO_MODEL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
|
||||||
|
export const LEGACY_TESTING_ENDPOINTS = [
|
||||||
|
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function isTestingEndpoint(url: string): boolean {
|
||||||
|
const normalized = url.replace(/\/+$/, '')
|
||||||
|
return normalized === DEMO_BASE_URL || LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {
|
||||||
|
const normalized = config.baseURL.replace(/\/+$/, '')
|
||||||
|
if (LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)) {
|
||||||
|
return { ...DEMO_CONFIG }
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,17 +130,16 @@ 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>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
You must ALWAYS respond with a valid JSON in this exact format:
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
|
"evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",
|
||||||
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
|
"memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",
|
||||||
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence."
|
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
|
||||||
"action":{"action_name": {// action-specific parameter}}
|
"action":{
|
||||||
|
"Action name": {// Action parameters}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</output>
|
</output>
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
* - switch_to_tab: Switch to an existing tab
|
* - switch_to_tab: Switch to an existing tab
|
||||||
* - close_tab: Close a tab (optionally switch to another)
|
* - close_tab: Close a tab (optionally switch to another)
|
||||||
*/
|
*/
|
||||||
import zod from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import type { TabsController } from './TabsController'
|
import type { TabsController } from './TabsController'
|
||||||
|
|
||||||
/** Tool definition compatible with PageAgentCore customTools */
|
/** Tool definition compatible with PageAgentCore customTools */
|
||||||
interface TabTool {
|
interface TabTool {
|
||||||
description: string
|
description: string
|
||||||
inputSchema: zod.ZodType
|
inputSchema: z.ZodType
|
||||||
execute: (input: unknown) => Promise<string>
|
execute: (input: unknown) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,37 +26,48 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
|||||||
open_new_tab: {
|
open_new_tab: {
|
||||||
description:
|
description:
|
||||||
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
|
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
url: zod.string().describe('The URL to open in the new tab'),
|
url: z.string().describe('The URL to open in the new tab'),
|
||||||
}),
|
}),
|
||||||
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)}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
switch_to_tab: {
|
switch_to_tab: {
|
||||||
description:
|
description:
|
||||||
'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',
|
'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
tab_id: zod.number().int().describe('The tab ID to switch to'),
|
tab_id: z.number().int().describe('The tab ID to switch to'),
|
||||||
}),
|
}),
|
||||||
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)}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
close_tab: {
|
close_tab: {
|
||||||
description:
|
description:
|
||||||
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
|
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
tab_id: zod.number().int().describe('The tab ID to close'),
|
tab_id: z.number().int().describe('The tab ID to close'),
|
||||||
}),
|
}),
|
||||||
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)}`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* React hook for using AgentController
|
* React hook for using AgentController
|
||||||
*/
|
*/
|
||||||
import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/core'
|
import type {
|
||||||
|
AgentActivity,
|
||||||
|
AgentStatus,
|
||||||
|
HistoricalEvent,
|
||||||
|
SupportedLanguage,
|
||||||
|
} from '@page-agent/core'
|
||||||
import type { LLMConfig } from '@page-agent/llms'
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { MultiPageAgent } from './MultiPageAgent'
|
import { MultiPageAgent } from './MultiPageAgent'
|
||||||
import { DEMO_CONFIG } from './constants'
|
import { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'
|
||||||
|
|
||||||
|
/** Language preference: undefined means follow system */
|
||||||
|
export type LanguagePreference = SupportedLanguage | undefined
|
||||||
|
|
||||||
|
export interface ExtConfig extends LLMConfig {
|
||||||
|
language?: LanguagePreference
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseAgentResult {
|
export interface UseAgentResult {
|
||||||
status: AgentStatus
|
status: AgentStatus
|
||||||
history: HistoricalEvent[]
|
history: HistoricalEvent[]
|
||||||
activity: AgentActivity | null
|
activity: AgentActivity | null
|
||||||
currentTask: string
|
currentTask: string
|
||||||
config: LLMConfig | null
|
config: ExtConfig | null
|
||||||
execute: (task: string) => Promise<void>
|
execute: (task: string) => Promise<void>
|
||||||
stop: () => void
|
stop: () => void
|
||||||
configure: (config: LLMConfig) => Promise<void>
|
configure: (config: ExtConfig) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAgent(): UseAgentResult {
|
export function useAgent(): UseAgentResult {
|
||||||
@@ -25,16 +37,23 @@ export function useAgent(): UseAgentResult {
|
|||||||
const [history, setHistory] = useState<HistoricalEvent[]>([])
|
const [history, setHistory] = useState<HistoricalEvent[]>([])
|
||||||
const [activity, setActivity] = useState<AgentActivity | null>(null)
|
const [activity, setActivity] = useState<AgentActivity | null>(null)
|
||||||
const [currentTask, setCurrentTask] = useState('')
|
const [currentTask, setCurrentTask] = useState('')
|
||||||
const [config, setConfig] = useState<LLMConfig | null>(null)
|
const [config, setConfig] = useState<ExtConfig | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get('llmConfig').then((result) => {
|
chrome.storage.local.get(['llmConfig', 'language']).then((result) => {
|
||||||
if (result.llmConfig) {
|
let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
|
||||||
setConfig(result.llmConfig as LLMConfig)
|
const language = (result.language as SupportedLanguage) || undefined
|
||||||
} else {
|
|
||||||
|
// Auto-migrate legacy testing endpoints
|
||||||
|
const migrated = migrateLegacyEndpoint(llmConfig)
|
||||||
|
if (migrated !== llmConfig) {
|
||||||
|
llmConfig = migrated
|
||||||
|
chrome.storage.local.set({ llmConfig: migrated })
|
||||||
|
} else if (!result.llmConfig) {
|
||||||
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
|
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
|
||||||
setConfig(DEMO_CONFIG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setConfig({ ...llmConfig, language })
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -75,7 +94,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,12 +103,17 @@ 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 ({ language, ...llmConfig }: ExtConfig) => {
|
||||||
await chrome.storage.local.set({ llmConfig: newConfig })
|
await chrome.storage.local.set({ llmConfig })
|
||||||
setConfig(newConfig)
|
if (language) {
|
||||||
|
await chrome.storage.local.set({ language })
|
||||||
|
} else {
|
||||||
|
await chrome.storage.local.remove('language')
|
||||||
|
}
|
||||||
|
setConfig({ ...llmConfig, language })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -111,6 +111,41 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%,
|
||||||
|
49% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-a {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-b {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||||
|
|
||||||
// Focus state.
|
// Focus state — soft multi-color glow matching ai-motion palette
|
||||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
|
||||||
|
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { MotionProps, motion, useInView } from 'motion/react'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children?: string
|
||||||
|
words?: string[]
|
||||||
|
className?: string
|
||||||
|
duration?: number
|
||||||
|
typeSpeed?: number
|
||||||
|
deleteSpeed?: number
|
||||||
|
delay?: number
|
||||||
|
pauseDelay?: number
|
||||||
|
loop?: boolean
|
||||||
|
as?: React.ElementType
|
||||||
|
startOnView?: boolean
|
||||||
|
showCursor?: boolean
|
||||||
|
blinkCursor?: boolean
|
||||||
|
cursorStyle?: 'line' | 'block' | 'underscore'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypingAnimation({
|
||||||
|
children,
|
||||||
|
words,
|
||||||
|
className,
|
||||||
|
duration = 100,
|
||||||
|
typeSpeed,
|
||||||
|
deleteSpeed,
|
||||||
|
delay = 0,
|
||||||
|
pauseDelay = 1000,
|
||||||
|
loop = false,
|
||||||
|
as: Component = 'span',
|
||||||
|
startOnView = true,
|
||||||
|
showCursor = true,
|
||||||
|
blinkCursor = true,
|
||||||
|
cursorStyle = 'line',
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) {
|
||||||
|
const MotionComponent = motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>('')
|
||||||
|
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||||
|
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||||
|
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null)
|
||||||
|
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
|
||||||
|
const hasMultipleWords = wordsToAnimate.length > 1
|
||||||
|
|
||||||
|
const typingSpeed = typeSpeed || duration
|
||||||
|
const deletingSpeed = deleteSpeed || typingSpeed / 2
|
||||||
|
|
||||||
|
const shouldStart = startOnView ? isInView : true
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldStart || wordsToAnimate.length === 0) return
|
||||||
|
|
||||||
|
const timeoutDelay =
|
||||||
|
delay > 0 && displayedText === ''
|
||||||
|
? delay
|
||||||
|
: phase === 'typing'
|
||||||
|
? typingSpeed
|
||||||
|
: phase === 'deleting'
|
||||||
|
? deletingSpeed
|
||||||
|
: pauseDelay
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const currentWord = wordsToAnimate[currentWordIndex] || ''
|
||||||
|
const graphemes = Array.from(currentWord)
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case 'typing':
|
||||||
|
if (currentCharIndex < graphemes.length) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex + 1)
|
||||||
|
} else {
|
||||||
|
if (hasMultipleWords || loop) {
|
||||||
|
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
|
||||||
|
if (!isLastWord || loop) {
|
||||||
|
setPhase('pause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
setPhase('deleting')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deleting':
|
||||||
|
if (currentCharIndex > 0) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex - 1)
|
||||||
|
} else {
|
||||||
|
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||||
|
setCurrentWordIndex(nextIndex)
|
||||||
|
setPhase('typing')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, timeoutDelay)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [
|
||||||
|
shouldStart,
|
||||||
|
phase,
|
||||||
|
currentCharIndex,
|
||||||
|
currentWordIndex,
|
||||||
|
displayedText,
|
||||||
|
wordsToAnimate,
|
||||||
|
hasMultipleWords,
|
||||||
|
loop,
|
||||||
|
typingSpeed,
|
||||||
|
deletingSpeed,
|
||||||
|
pauseDelay,
|
||||||
|
delay,
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
|
||||||
|
const isComplete =
|
||||||
|
!loop &&
|
||||||
|
currentWordIndex === wordsToAnimate.length - 1 &&
|
||||||
|
currentCharIndex >= currentWordGraphemes.length &&
|
||||||
|
phase !== 'deleting'
|
||||||
|
|
||||||
|
const shouldShowCursor =
|
||||||
|
showCursor &&
|
||||||
|
!isComplete &&
|
||||||
|
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
|
||||||
|
|
||||||
|
const getCursorChar = () => {
|
||||||
|
switch (cursorStyle) {
|
||||||
|
case 'block':
|
||||||
|
return '▌'
|
||||||
|
case 'underscore':
|
||||||
|
return '_'
|
||||||
|
case 'line':
|
||||||
|
default:
|
||||||
|
return '|'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
{shouldShowCursor && (
|
||||||
|
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
|
||||||
|
{getCursorChar()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MotionComponent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
|
import { 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}`)
|
||||||
@@ -69,11 +69,12 @@ async function exposeAgentToPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { task, llmConfig } = payload
|
const { task, config } = payload
|
||||||
|
|
||||||
// create when used
|
// Dispose old instance before creating new one
|
||||||
|
multiPageAgent?.dispose()
|
||||||
|
|
||||||
multiPageAgent = new MultiPageAgent(llmConfig)
|
multiPageAgent = new MultiPageAgent(config)
|
||||||
|
|
||||||
// events
|
// events
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
import type { LLMConfig } from '@page-agent/llms'
|
|
||||||
|
|
||||||
export interface ExecuteHooks {
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
|
|
||||||
|
export interface ExecuteConfig {
|
||||||
|
baseURL: string
|
||||||
|
apiKey: string
|
||||||
|
model: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to include the initial tab (that holds this main world script) in the task.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
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,
|
|
||||||
llmConfig: LLMConfig,
|
|
||||||
hooks?: ExecuteHooks
|
|
||||||
) => Promise<ExecutionResult>
|
|
||||||
|
|
||||||
export default defineUnlistedScript(() => {
|
export default defineUnlistedScript(() => {
|
||||||
let _lastId = 0
|
let _lastId = 0
|
||||||
function getId() {
|
function getId() {
|
||||||
@@ -21,13 +25,13 @@ export default defineUnlistedScript(() => {
|
|||||||
return _lastId
|
return _lastId
|
||||||
}
|
}
|
||||||
|
|
||||||
const execute: Execute = async (task, llmConfig, hooks) => {
|
const execute: Execute = async (task, config) => {
|
||||||
if (typeof task !== 'string') throw new Error('Task must be a string')
|
if (typeof task !== 'string') throw new Error('Task must be a string')
|
||||||
if (task.trim().length === 0) throw new Error('Task cannot be empty')
|
if (task.trim().length === 0) throw new Error('Task cannot be empty')
|
||||||
if (!llmConfig) throw new Error('LLM config is required')
|
if (!config) throw new Error('Config is required')
|
||||||
if (!llmConfig.baseURL) throw new Error('LLM config must have a baseURL')
|
if (!config.baseURL) throw new Error('Config must have a baseURL')
|
||||||
if (!llmConfig.apiKey) throw new Error('LLM config must have an apiKey')
|
if (!config.apiKey) throw new Error('Config must have an apiKey')
|
||||||
if (!llmConfig.model) throw new Error('LLM config must have a model')
|
if (!config.model) throw new Error('Config must have a model')
|
||||||
|
|
||||||
const id = getId()
|
const id = getId()
|
||||||
|
|
||||||
@@ -40,30 +44,25 @@ export default defineUnlistedScript(() => {
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
|
|
||||||
if (data.action === 'status_change_event' && hooks?.onStatusChange) {
|
if (data.action === 'status_change_event' && config.onStatusChange) {
|
||||||
hooks.onStatusChange(data.payload)
|
config.onStatusChange(data.payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action === 'activity_event' && hooks?.onActivity) {
|
if (data.action === 'activity_event' && config.onActivity) {
|
||||||
hooks.onActivity(data.payload)
|
config.onActivity(data.payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action === 'history_change_event' && hooks?.onHistoryUpdate) {
|
if (data.action === 'history_change_event' && config.onHistoryUpdate) {
|
||||||
hooks.onHistoryUpdate(data.payload)
|
config.onHistoryUpdate(data.payload)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.action === 'dispose_event' && hooks?.onDispose) {
|
|
||||||
hooks.onDispose()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// result
|
|
||||||
|
|
||||||
if (data.action !== 'execute_result') return
|
if (data.action !== 'execute_result') return
|
||||||
|
|
||||||
|
// execute_result
|
||||||
|
|
||||||
window.removeEventListener('message', handleMessage)
|
window.removeEventListener('message', handleMessage)
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -73,6 +72,7 @@ export default defineUnlistedScript(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @note will be removed on dispose or result
|
||||||
window.addEventListener('message', handleMessage)
|
window.addEventListener('message', handleMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,7 +81,15 @@ export default defineUnlistedScript(() => {
|
|||||||
channel: 'PAGE_AGENT_EXT_REQUEST',
|
channel: 'PAGE_AGENT_EXT_REQUEST',
|
||||||
id,
|
id,
|
||||||
action: 'execute',
|
action: 'execute',
|
||||||
payload: { task, llmConfig },
|
payload: {
|
||||||
|
task,
|
||||||
|
config: {
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
model: config.model,
|
||||||
|
includeInitialTab: config.includeInitialTab,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'*'
|
'*'
|
||||||
)
|
)
|
||||||
@@ -89,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',
|
||||||
},
|
},
|
||||||
'*'
|
'*'
|
||||||
)
|
)
|
||||||
@@ -106,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, MotionOverlay, 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,36 +83,62 @@ 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
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-background">
|
<div className="relative flex flex-col h-screen bg-background">
|
||||||
|
<MotionOverlay active={isRunning} />
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center justify-between border-b px-3 py-2">
|
<header className="flex items-center justify-between border-b px-3 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Logo className="size-5" />
|
<Logo className="size-5" />
|
||||||
<span className="text-sm font-medium">Page Agent Ext</span>
|
<span className="text-sm font-medium">Page Agent Ext</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-1">
|
||||||
<StatusDot status={status} />
|
<StatusDot status={status} />
|
||||||
<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" />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
|
||||||
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
|
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { siGithub } from 'simple-icons'
|
import { siGithub } from 'simple-icons'
|
||||||
|
|
||||||
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
|
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
|
||||||
|
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
config: LLMConfig | null
|
config: ExtConfig | null
|
||||||
onSave: (config: LLMConfig) => Promise<void>
|
onSave: (config: ExtConfig) => Promise<void>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
|
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
|
||||||
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
||||||
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
||||||
|
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [userAuthToken, setUserAuthToken] = useState<string>('')
|
const [userAuthToken, setUserAuthToken] = useState<string>('')
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
@@ -28,6 +29,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
setApiKey(config?.apiKey || DEMO_API_KEY)
|
setApiKey(config?.apiKey || DEMO_API_KEY)
|
||||||
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
||||||
setModel(config?.model || DEMO_MODEL)
|
setModel(config?.model || DEMO_MODEL)
|
||||||
|
setLanguage(config?.language)
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
// Poll for user auth token every second until found
|
// Poll for user auth token every second until found
|
||||||
@@ -65,7 +67,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await onSave({ apiKey, baseURL, model })
|
await onSave({ apiKey, baseURL, model, language })
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -85,6 +87,23 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Testing API notice */}
|
||||||
|
{isTestingEndpoint(baseURL) && (
|
||||||
|
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[10px] text-muted-foreground leading-relaxed">
|
||||||
|
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
|
||||||
|
You are using the free testing API. By using this service you agree to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-foreground"
|
||||||
|
>
|
||||||
|
Terms of Use & Privacy Policy
|
||||||
|
</a>
|
||||||
|
. No sensitive data. No guaranteed availability.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* User Auth Token Section */}
|
{/* User Auth Token Section */}
|
||||||
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
||||||
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
||||||
@@ -165,6 +184,19 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-muted-foreground">Language</label>
|
||||||
|
<select
|
||||||
|
value={language ?? ''}
|
||||||
|
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
|
||||||
|
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="">System</option>
|
||||||
|
<option value="en-US">English</option>
|
||||||
|
<option value="zh-CN">中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
||||||
Cancel
|
Cancel
|
||||||
@@ -204,7 +236,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md"
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 hover:text-foreground"
|
className="flex items-center gap-1 hover:text-foreground"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ArrowLeft } from 'lucide-react'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { type SessionRecord, getSession } from '@/lib/db'
|
||||||
|
|
||||||
|
import { EventCard } from './cards'
|
||||||
|
|
||||||
|
export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack: () => void }) {
|
||||||
|
const [session, setSession] = useState<SessionRecord | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSession(sessionId).then((s) => setSession(s ?? null))
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen text-xs text-muted-foreground">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||||
|
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
||||||
|
<ArrowLeft className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium truncate">History</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Task */}
|
||||||
|
<div className="border-b px-3 py-2 bg-muted/30">
|
||||||
|
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">Task</div>
|
||||||
|
<div className="text-xs font-medium" title={session.task}>
|
||||||
|
{session.task}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Events (read-only) */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
|
{session.history.map((event, index) => (
|
||||||
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
|
<EventCard key={index} event={event} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
|
||||||
|
|
||||||
|
function timeAgo(ts: number): string {
|
||||||
|
const seconds = Math.floor((Date.now() - ts) / 1000)
|
||||||
|
if (seconds < 60) return 'just now'
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
if (minutes < 60) return `${minutes}m ago`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
if (hours < 24) return `${hours}h ago`
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
return `${days}d ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryList({
|
||||||
|
onSelect,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
onSelect: (id: string) => void
|
||||||
|
onBack: () => void
|
||||||
|
}) {
|
||||||
|
const [sessions, setSessions] = useState<SessionRecord[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setSessions(await listSessions())
|
||||||
|
setLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
load()
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
await deleteSession(id)
|
||||||
|
setSessions((prev) => prev.filter((s) => s.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen bg-background">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||||
|
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
||||||
|
<ArrowLeft className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium flex-1">History</span>
|
||||||
|
{sessions.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
await clearSessions()
|
||||||
|
setSessions([])
|
||||||
|
}}
|
||||||
|
className="text-[10px] text-muted-foreground hover:text-destructive cursor-pointer h-6 px-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3 mr-1" />
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && sessions.length === 0 && (
|
||||||
|
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
||||||
|
No history yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => onSelect(session.id)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && onSelect(session.id)}
|
||||||
|
className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group"
|
||||||
|
>
|
||||||
|
{/* Status icon */}
|
||||||
|
{session.status === 'completed' ? (
|
||||||
|
<CheckCircle className="size-3.5 text-green-500 shrink-0 mt-0.5" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="size-3.5 text-destructive shrink-0 mt-0.5" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-xs font-medium truncate">{session.task}</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||||
|
{timeAgo(session.createdAt)} · {session.history.length} steps
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleDelete(e, session.id)}
|
||||||
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer shrink-0"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { AgentStatus } from '@page-agent/core'
|
import type { AgentStatus } from '@page-agent/core'
|
||||||
|
import { Motion } from 'ai-motion'
|
||||||
|
import { BookOpen, Globe } from 'lucide-react'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { siGithub } from 'simple-icons'
|
||||||
|
|
||||||
|
import { TypingAnimation } from '@/components/ui/typing-animation'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// Status dot indicator
|
// Status dot indicator
|
||||||
@@ -19,7 +24,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
|
|||||||
}[status]
|
}[status]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5 mr-2">
|
||||||
<span
|
<span
|
||||||
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
||||||
/>
|
/>
|
||||||
@@ -32,14 +37,111 @@ export function Logo({ className }: { className?: string }) {
|
|||||||
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
|
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty state with logo
|
// Full-screen ai-motion glow overlay, shown only while running
|
||||||
|
export function MotionOverlay({ active }: { active: boolean }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const motionRef = useRef<Motion | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
|
const motion = new Motion({
|
||||||
|
mode,
|
||||||
|
borderWidth: 4,
|
||||||
|
borderRadius: 14,
|
||||||
|
glowWidth: mode === 'dark' ? 120 : 60,
|
||||||
|
styles: { position: 'absolute', inset: '0' },
|
||||||
|
})
|
||||||
|
motionRef.current = motion
|
||||||
|
containerRef.current!.appendChild(motion.element)
|
||||||
|
motion.autoResize(containerRef.current!)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
motion.dispose()
|
||||||
|
motionRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const motion = motionRef.current
|
||||||
|
if (!motion) return
|
||||||
|
|
||||||
|
let disposed = false
|
||||||
|
if (active) {
|
||||||
|
motion.start()
|
||||||
|
motion.fadeIn()
|
||||||
|
} else {
|
||||||
|
motion.fadeOut().then(() => !disposed && motion.pause())
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
disposed = true
|
||||||
|
}
|
||||||
|
}, [active])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="pointer-events-none absolute inset-0 z-10 opacity-60"
|
||||||
|
style={{ display: active ? undefined : 'none' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state with logo and breathing glow
|
||||||
export function EmptyState() {
|
export function EmptyState() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-6">
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
|
||||||
<Logo className="size-20 opacity-80" />
|
<div className="relative select-none pointer-events-none">
|
||||||
|
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
|
||||||
|
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
|
||||||
|
<Logo className="relative size-20 opacity-80" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-medium text-foreground">Page Agent Ext</h2>
|
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
|
||||||
<p className="text-xs text-muted-foreground mt-1">Enter a task to automate this page</p>
|
<TypingAnimation
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
words={[
|
||||||
|
'Enter a task to automate this page',
|
||||||
|
'Execute multi-page tasks',
|
||||||
|
'Call this extension from your web page',
|
||||||
|
'Use this extension in your own agents',
|
||||||
|
]}
|
||||||
|
cursorStyle="underscore"
|
||||||
|
loop
|
||||||
|
typeSpeed={20}
|
||||||
|
deleteSpeed={10}
|
||||||
|
pauseDelay={3000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="GitHub"
|
||||||
|
>
|
||||||
|
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
|
||||||
|
<path d={siGithub.path} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://alibaba.github.io/page-agent/docs/features/chrome-extension"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="Documentation"
|
||||||
|
>
|
||||||
|
<BookOpen className="size-4" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://alibaba.github.io/page-agent"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="Website"
|
||||||
|
>
|
||||||
|
<Globe className="size-4" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { HistoricalEvent } from '@page-agent/core'
|
||||||
|
import { type DBSchema, type IDBPDatabase, openDB } from 'idb'
|
||||||
|
|
||||||
|
const DB_NAME = 'page-agent-ext'
|
||||||
|
const DB_VERSION = 1
|
||||||
|
|
||||||
|
export interface SessionRecord {
|
||||||
|
id: string
|
||||||
|
task: string
|
||||||
|
history: HistoricalEvent[]
|
||||||
|
status: 'completed' | 'error'
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageAgentDB extends DBSchema {
|
||||||
|
sessions: {
|
||||||
|
key: string
|
||||||
|
value: SessionRecord
|
||||||
|
indexes: { 'by-created': number }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbPromise: Promise<IDBPDatabase<PageAgentDB>> | null = null
|
||||||
|
|
||||||
|
function getDB() {
|
||||||
|
if (!dbPromise) {
|
||||||
|
dbPromise = openDB<PageAgentDB>(DB_NAME, DB_VERSION, {
|
||||||
|
upgrade(db) {
|
||||||
|
const store = db.createObjectStore('sessions', { keyPath: 'id' })
|
||||||
|
store.createIndex('by-created', 'createdAt')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dbPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveSession(
|
||||||
|
session: Omit<SessionRecord, 'id' | 'createdAt'>
|
||||||
|
): Promise<SessionRecord> {
|
||||||
|
const db = await getDB()
|
||||||
|
const record: SessionRecord = {
|
||||||
|
...session,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}
|
||||||
|
await db.put('sessions', record)
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List sessions, newest first */
|
||||||
|
export async function listSessions(): Promise<SessionRecord[]> {
|
||||||
|
const db = await getDB()
|
||||||
|
const all = await db.getAllFromIndex('sessions', 'by-created')
|
||||||
|
return all.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSession(id: string): Promise<SessionRecord | undefined> {
|
||||||
|
const db = await getDB()
|
||||||
|
return db.get('sessions', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSession(id: string): Promise<void> {
|
||||||
|
const db = await getDB()
|
||||||
|
await db.delete('sessions', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearSessions(): Promise<void> {
|
||||||
|
const db = await getDB()
|
||||||
|
await db.clear('sessions')
|
||||||
|
}
|
||||||
@@ -38,9 +38,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Page Agent Ext',
|
default_locale: 'en',
|
||||||
description:
|
name: '__MSG_extName__',
|
||||||
'AI-powered browser automation assistant. Control web pages with natural language.',
|
description: '__MSG_extDescription__',
|
||||||
homepage_url: 'https://alibaba.github.io/page-agent/',
|
homepage_url: 'https://alibaba.github.io/page-agent/',
|
||||||
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
||||||
host_permissions: ['<all_urls>'],
|
host_permissions: ['<all_urls>'],
|
||||||
@@ -48,7 +48,7 @@ export default defineConfig({
|
|||||||
64: 'assets/page-agent-64.png',
|
64: 'assets/page-agent-64.png',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
default_title: 'Open Page Agent',
|
default_title: '__MSG_extActionTitle__',
|
||||||
},
|
},
|
||||||
web_accessible_resources: [
|
web_accessible_resources: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* OpenAI Client implementation
|
* OpenAI Client implementation
|
||||||
*/
|
*/
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
import { modelPatch, zodToOpenAITool } from './utils'
|
import { modelPatch, zodToOpenAITool } from './utils'
|
||||||
@@ -54,8 +56,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
|
||||||
@@ -180,7 +184,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
// Validate with schema
|
// Validate with schema
|
||||||
const validation = tool.inputSchema.safeParse(parsedArgs)
|
const validation = tool.inputSchema.safeParse(parsedArgs)
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
console.error(validation.error)
|
console.error(z.prettifyError(validation.error))
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
'Tool arguments validation failed',
|
'Tool arguments validation failed',
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
||||||
import { InvokeError } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
|
|
||||||
export type { InvokeError, InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export { InvokeError, InvokeErrorType }
|
||||||
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||||
// Runtime validation as defensive programming (types already guarantee these)
|
// Runtime validation as defensive programming (types already guarantee these)
|
||||||
if (!config.baseURL || !config.apiKey || !config.model) {
|
if (!config.baseURL || !config.apiKey || !config.model) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
|
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
|
||||||
'See: https://alibaba.github.io/page-agent/#/docs/features/models'
|
'See: https://alibaba.github.io/page-agent/docs/features/models'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,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 @@
|
|||||||
/**
|
/**
|
||||||
* Core types for LLM integration
|
* Core types for LLM integration
|
||||||
*/
|
*/
|
||||||
import type { z } from 'zod'
|
import type * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message format - OpenAI standard (industry standard)
|
* Message format - OpenAI standard (industry standard)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Utility functions for LLM integration
|
* Utility functions for LLM integration
|
||||||
*/
|
*/
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import type { Tool } from './types'
|
import type { Tool } from './types'
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
if (modelName.startsWith('qwen')) {
|
if (modelName.startsWith('qwen')) {
|
||||||
debug('Applying Qwen patch: use higher temperature for auto fixing')
|
debug('Applying Qwen patch: use higher temperature for auto fixing')
|
||||||
body.temperature = Math.max(body.temperature || 0, 1.0)
|
body.temperature = Math.max(body.temperature || 0, 1.0)
|
||||||
|
body.enable_thinking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('claude')) {
|
if (modelName.startsWith('claude')) {
|
||||||
@@ -74,6 +75,10 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
} else if (modelName.startsWith('gpt-51')) {
|
} else if (modelName.startsWith('gpt-51')) {
|
||||||
debug('Applying GPT-51 patch: disable reasoning')
|
debug('Applying GPT-51 patch: disable reasoning')
|
||||||
body.reasoning_effort = 'none'
|
body.reasoning_effort = 'none'
|
||||||
|
} else if (modelName.startsWith('gpt-5-mini')) {
|
||||||
|
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
||||||
|
body.reasoning_effort = 'low'
|
||||||
|
body.temperature = 1
|
||||||
} else if (modelName.startsWith('gpt-5')) {
|
} else if (modelName.startsWith('gpt-5')) {
|
||||||
debug('Applying GPT-5 patch: set reasoning effort to low')
|
debug('Applying GPT-5 patch: set reasoning effort to low')
|
||||||
body.reasoning_effort = 'low'
|
body.reasoning_effort = 'low'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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,11 +44,11 @@
|
|||||||
"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.4.2",
|
||||||
|
"@page-agent/llms": "1.4.2",
|
||||||
|
"@page-agent/page-controller": "1.4.2",
|
||||||
|
"@page-agent/ui": "1.4.2",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5"
|
||||||
"@page-agent/llms": "1.1.0",
|
|
||||||
"@page-agent/page-controller": "1.1.0",
|
|
||||||
"@page-agent/core": "1.1.0",
|
|
||||||
"@page-agent/ui": "1.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ window.PageAgent = PageAgent
|
|||||||
|
|
||||||
console.log('🚀 page-agent.js loaded!')
|
console.log('🚀 page-agent.js loaded!')
|
||||||
|
|
||||||
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_MODEL = 'qwen3.5-plus'
|
||||||
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'NA'
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { config as dotenvConfig } from 'dotenv'
|
|||||||
import { dirname, resolve } from 'path'
|
import { dirname, resolve } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
// import { analyzer } from 'vite-bundle-analyzer'
|
||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
@@ -14,8 +15,11 @@ dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
|||||||
// - alias all local packages so that they can be build in
|
// - alias all local packages so that they can be build in
|
||||||
// - no external
|
// - no external
|
||||||
// - no d.ts. dts does not work with monorepo aliasing
|
// - no d.ts. dts does not work with monorepo aliasing
|
||||||
export default defineConfig(({ mode }) => ({
|
export default defineConfig(() => ({
|
||||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
plugins: [
|
||||||
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
|
// analyzer()
|
||||||
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
esbuild: {
|
||||||
keepNames: true,
|
keepNames: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export async function clickElement(element: HTMLElement) {
|
|||||||
// dispatch a click event
|
// dispatch a click event
|
||||||
// element.click()
|
// element.click()
|
||||||
|
|
||||||
await waitFor(0.1) // Wait to ensure click event processing completes
|
await waitFor(0.2) // Wait to ensure click event processing completes
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
@@ -181,11 +181,6 @@ export async function scrollVertically(
|
|||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Starting direct container scroll for element:',
|
|
||||||
targetElement.tagName
|
|
||||||
)
|
|
||||||
|
|
||||||
let currentElement = targetElement as HTMLElement | null
|
let currentElement = targetElement as HTMLElement | null
|
||||||
let scrollSuccess = false
|
let scrollSuccess = false
|
||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
@@ -198,19 +193,6 @@ export async function scrollVertically(
|
|||||||
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
||||||
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Checking element:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'hasScrollableY:',
|
|
||||||
hasScrollableY,
|
|
||||||
'canScrollVertically:',
|
|
||||||
canScrollVertically,
|
|
||||||
'scrollHeight:',
|
|
||||||
currentElement.scrollHeight,
|
|
||||||
'clientHeight:',
|
|
||||||
currentElement.clientHeight
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasScrollableY && canScrollVertically) {
|
if (hasScrollableY && canScrollVertically) {
|
||||||
const beforeScroll = currentElement.scrollTop
|
const beforeScroll = currentElement.scrollTop
|
||||||
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
|
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
|
||||||
@@ -228,27 +210,10 @@ export async function scrollVertically(
|
|||||||
const afterScroll = currentElement.scrollTop
|
const afterScroll = currentElement.scrollTop
|
||||||
const actualScrollDelta = afterScroll - beforeScroll
|
const actualScrollDelta = afterScroll - beforeScroll
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Scroll attempt:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'before:',
|
|
||||||
beforeScroll,
|
|
||||||
'after:',
|
|
||||||
afterScroll,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Math.abs(actualScrollDelta) > 0.5) {
|
if (Math.abs(actualScrollDelta) > 0.5) {
|
||||||
scrollSuccess = true
|
scrollSuccess = true
|
||||||
scrolledElement = currentElement
|
scrolledElement = currentElement
|
||||||
scrollDelta = actualScrollDelta
|
scrollDelta = actualScrollDelta
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Successfully scrolled container:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,11 +309,6 @@ export async function scrollHorizontally(
|
|||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Starting direct container scroll for element:',
|
|
||||||
targetElement.tagName
|
|
||||||
)
|
|
||||||
|
|
||||||
let currentElement = targetElement as HTMLElement | null
|
let currentElement = targetElement as HTMLElement | null
|
||||||
let scrollSuccess = false
|
let scrollSuccess = false
|
||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
@@ -361,19 +321,6 @@ export async function scrollHorizontally(
|
|||||||
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
||||||
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Checking element:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'hasScrollableX:',
|
|
||||||
hasScrollableX,
|
|
||||||
'canScrollHorizontally:',
|
|
||||||
canScrollHorizontally,
|
|
||||||
'scrollWidth:',
|
|
||||||
currentElement.scrollWidth,
|
|
||||||
'clientWidth:',
|
|
||||||
currentElement.clientWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasScrollableX && canScrollHorizontally) {
|
if (hasScrollableX && canScrollHorizontally) {
|
||||||
const beforeScroll = currentElement.scrollLeft
|
const beforeScroll = currentElement.scrollLeft
|
||||||
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
|
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
|
||||||
@@ -391,27 +338,10 @@ export async function scrollHorizontally(
|
|||||||
const afterScroll = currentElement.scrollLeft
|
const afterScroll = currentElement.scrollLeft
|
||||||
const actualScrollDelta = afterScroll - beforeScroll
|
const actualScrollDelta = afterScroll - beforeScroll
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Scroll attempt:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'before:',
|
|
||||||
beforeScroll,
|
|
||||||
'after:',
|
|
||||||
afterScroll,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Math.abs(actualScrollDelta) > 0.5) {
|
if (Math.abs(actualScrollDelta) > 0.5) {
|
||||||
scrollSuccess = true
|
scrollSuccess = true
|
||||||
scrolledElement = currentElement
|
scrolledElement = currentElement
|
||||||
scrollDelta = actualScrollDelta
|
scrollDelta = actualScrollDelta
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Successfully scrolled container:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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,9 +12,10 @@ 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}}',
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
clicking: 'Clicking element [{{index}}]...',
|
clicking: 'Clicking element [{{index}}]...',
|
||||||
@@ -59,9 +60,10 @@ const zhCN = {
|
|||||||
question: '询问: {{question}}',
|
question: '询问: {{question}}',
|
||||||
waitingPlaceholder: '等待任务开始...',
|
waitingPlaceholder: '等待任务开始...',
|
||||||
stop: '终止',
|
stop: '终止',
|
||||||
|
close: '关闭',
|
||||||
expand: '展开历史',
|
expand: '展开历史',
|
||||||
collapse: '收起历史',
|
collapse: '收起历史',
|
||||||
step: '步骤 {{number}} · {{time}}{{duration}}',
|
step: '步骤 {{number}}',
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
clicking: '正在点击元素 [{{index}}]...',
|
clicking: '正在点击元素 [{{index}}]...',
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { I18n, type SupportedLanguage } from '../i18n'
|
import { I18n, type SupportedLanguage } from '../i18n'
|
||||||
import { truncate } from '../utils'
|
import { truncate } from '../utils'
|
||||||
import { createCard, createReflectionLines, formatTime } from './cards'
|
import { createCard, createReflectionLines } from './cards'
|
||||||
import type { AgentActivity, PanelAgentAdapter } from './types'
|
import type { AgentActivity, PanelAgentAdapter } from './types'
|
||||||
|
|
||||||
import styles from './Panel.module.css'
|
import styles from './Panel.module.css'
|
||||||
@@ -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()
|
||||||
@@ -178,7 +187,6 @@ export class Panel {
|
|||||||
tempCard.innerHTML = createCard({
|
tempCard.innerHTML = createCard({
|
||||||
icon: '❓',
|
icon: '❓',
|
||||||
content: `Question: ${question}`,
|
content: `Question: ${question}`,
|
||||||
meta: formatTime(this.#config.language ?? 'en-US'),
|
|
||||||
type: 'question',
|
type: 'question',
|
||||||
})
|
})
|
||||||
const cardElement = tempCard.firstElementChild as HTMLElement
|
const cardElement = tempCard.firstElementChild as HTMLElement
|
||||||
@@ -266,10 +274,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 +395,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 +432,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
|
||||||
@@ -578,15 +590,12 @@ export class Panel {
|
|||||||
/** Create cards for a history event */
|
/** Create cards for a history event */
|
||||||
#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {
|
#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {
|
||||||
const cards: string[] = []
|
const cards: string[] = []
|
||||||
const time = formatTime(this.#config.language ?? 'en-US')
|
|
||||||
const meta =
|
const meta =
|
||||||
event.type === 'step' && event.stepIndex !== undefined
|
event.type === 'step' && event.stepIndex !== undefined
|
||||||
? this.#i18n.t('ui.panel.step', {
|
? this.#i18n.t('ui.panel.step', {
|
||||||
number: (event.stepIndex + 1).toString(),
|
number: (event.stepIndex + 1).toString(),
|
||||||
time,
|
|
||||||
duration: '',
|
|
||||||
})
|
})
|
||||||
: time
|
: undefined
|
||||||
|
|
||||||
if (event.type === 'step') {
|
if (event.type === 'step') {
|
||||||
// Reflection card
|
// Reflection card
|
||||||
@@ -623,7 +632,7 @@ export class Panel {
|
|||||||
/** Create cards for an action */
|
/** Create cards for an action */
|
||||||
#createActionCards(
|
#createActionCards(
|
||||||
action: { name: string; input: unknown; output: string },
|
action: { name: string; input: unknown; output: string },
|
||||||
meta: string
|
meta?: string
|
||||||
): string[] {
|
): string[] {
|
||||||
const cards: string[] = []
|
const cards: string[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ export function createCard({ icon, content, meta, type }: CardOptions): string {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format timestamp for cards */
|
|
||||||
export function formatTime(locale: string = 'en-US'): string {
|
|
||||||
return new Date().toLocaleTimeString(locale, {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create reflection lines from reflection object */
|
/** Create reflection lines from reflection object */
|
||||||
export function createReflectionLines(reflection: {
|
export function createReflectionLines(reflection: {
|
||||||
evaluation_previous_goal?: string
|
evaluation_previous_goal?: string
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-24
@@ -5,9 +5,9 @@
|
|||||||
- **React** with TypeScript
|
- **React** with TypeScript
|
||||||
- **Vite** for dev server and build
|
- **Vite** for dev server and build
|
||||||
- **Tailwind CSS** for styling
|
- **Tailwind CSS** for styling
|
||||||
- **shadcn/ui** (new-york style) for UI components
|
- **shadcn/ui** (new-york style) for UI components — **do NOT hand-edit `src/components/ui/` files**
|
||||||
- **Magic UI** for animations and effects
|
- **Magic UI** for animations and effects
|
||||||
- **wouter** with hash routing for static hosting
|
- **wouter** with browser routing (`base: "/page-agent"`)
|
||||||
- **lucide-react** for icons
|
- **lucide-react** for icons
|
||||||
|
|
||||||
## Component Guidelines
|
## Component Guidelines
|
||||||
@@ -53,58 +53,74 @@ Located in `src/components/ui/`:
|
|||||||
### Styling Rules
|
### Styling Rules
|
||||||
|
|
||||||
1. **Prefer Tailwind classes** over custom CSS
|
1. **Prefer Tailwind classes** over custom CSS
|
||||||
2. Use CSS modules only for complex component-specific styles
|
2. Support dark mode via `dark:` classes
|
||||||
3. Support dark mode via `dark:` classes
|
3. Use CSS variables from `src/index.css` for theme colors
|
||||||
4. Use CSS variables from `src/index.css` for theme colors
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── pages/
|
├── pages/
|
||||||
│ ├── Home.tsx # Homepage
|
│ ├── home/
|
||||||
|
│ │ ├── HomeContent.tsx # Homepage sections
|
||||||
|
│ │ └── ...Section.tsx
|
||||||
│ └── docs/
|
│ └── docs/
|
||||||
│ ├── Layout.tsx # Documentation sidebar
|
│ ├── index.tsx # Docs route switch
|
||||||
|
│ ├── Layout.tsx # Sidebar navigation
|
||||||
│ └── [section]/[topic]/page.tsx
|
│ └── [section]/[topic]/page.tsx
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── ui/ # shadcn/ui + Magic UI components
|
│ ├── ui/ # shadcn/ui + Magic UI (DO NOT hand-edit)
|
||||||
|
│ ├── Heading.tsx # Anchor heading for doc pages
|
||||||
│ ├── Header.tsx # Site header
|
│ ├── Header.tsx # Site header
|
||||||
│ └── Footer.tsx # Site footer
|
│ └── Footer.tsx # Site footer
|
||||||
├── i18n/ # Internationalization
|
├── i18n/ # Internationalization
|
||||||
├── router.tsx # Central routing
|
├── router.tsx # Root layout + routing
|
||||||
└── main.tsx # App entry
|
└── main.tsx # App entry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
Uses wouter browser routing with base path for GitHub Pages deployment at `https://alibaba.github.io/page-agent/`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// main.tsx
|
||||||
|
<Router base="/page-agent">
|
||||||
|
<PagesRouter />
|
||||||
|
</Router>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
|
||||||
|
- Header and Footer live in `router.tsx` **outside** `<Switch>`, so they always see the root router context (`base="/page-agent"`)
|
||||||
|
- Docs pages are nested via `<Route path="/docs" nest>`, which creates a child context (`base="/page-agent/docs"`)
|
||||||
|
- Inside the docs nest, Link hrefs are relative to `/docs` (e.g. `href="/features/models"`, NOT `href="/docs/features/models"`)
|
||||||
|
- **Never use `~` prefix** in Link hrefs — it bypasses the base path entirely
|
||||||
|
- Doc page headings use `<Heading id="slug" level={2}>` for anchor links
|
||||||
|
|
||||||
|
### SPA on GitHub Pages
|
||||||
|
|
||||||
|
Instead of `404.html` redirects, the build copies `index.html` into every route directory. Add new routes to the `SPA_ROUTES` array in `vite.config.js`.
|
||||||
|
|
||||||
## Adding New Pages
|
## Adding New Pages
|
||||||
|
|
||||||
### Documentation Page
|
### Documentation Page
|
||||||
|
|
||||||
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
|
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
|
||||||
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
2. Add route in `src/pages/docs/index.tsx`
|
||||||
3. Add navigation item to `pages/docs/Layout.tsx`
|
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
||||||
|
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
||||||
## Routing
|
|
||||||
|
|
||||||
Uses hash-based routing for static hosting:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Router } from 'wouter'
|
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
|
||||||
|
|
||||||
;<Router hook={useHashLocation}>{/* routes */}</Router>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
| ----------------- | ----------------------- |
|
| ----------------- | ----------------------- |
|
||||||
| `components.json` | shadcn/ui configuration |
|
| `components.json` | shadcn/ui configuration |
|
||||||
| `vite.config.js` | Vite build settings |
|
| `vite.config.js` | Vite build + SPA routes |
|
||||||
| `tsconfig.json` | TypeScript config |
|
| `tsconfig.json` | TypeScript config |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # Dev server (from root)
|
npm start # Dev server (from root)
|
||||||
npm run build:website # Build website (from root)
|
npm run build:website # Build website (from root)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -45,13 +45,24 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<div id="sk">
|
||||||
|
<p class="sk-text" id="sk-text">Loading...</p>
|
||||||
|
<style>
|
||||||
|
#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}
|
||||||
|
@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}
|
||||||
|
.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}
|
||||||
|
@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script type="module" src="./src/main.tsx"></script>
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dynamically update html lang attribute based on i18n detection
|
|
||||||
const updateHtmlLang = () => {
|
const updateHtmlLang = () => {
|
||||||
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
||||||
document.documentElement.lang = lang
|
document.documentElement.lang = lang
|
||||||
|
const el = document.getElementById('sk-text')
|
||||||
|
if (el) el.textContent = lang.startsWith('zh') ? '加载中...' : 'Loading...'
|
||||||
}
|
}
|
||||||
updateHtmlLang()
|
updateHtmlLang()
|
||||||
window.addEventListener('storage', updateHtmlLang)
|
window.addEventListener('storage', updateHtmlLang)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.0",
|
"version": "1.4.2",
|
||||||
"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.14",
|
||||||
|
"@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.575.0",
|
||||||
"motion": "^12.30.0",
|
"motion": "^12.34.3",
|
||||||
"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.9.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"wouter": "^3.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export default function Footer() {
|
|||||||
© 2026 page-agent. All rights reserved.
|
© 2026 page-agent. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 text-sm"
|
||||||
|
>
|
||||||
|
{isZh ? '使用条款与隐私' : 'Terms & Privacy'}
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/alibaba/page-agent"
|
href="https://github.com/alibaba/page-agent"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Header() {
|
|||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
href="~/"
|
href="/"
|
||||||
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
||||||
aria-label={isZh ? 'page-agent 首页' : 'page-agent home'}
|
aria-label={isZh ? 'page-agent 首页' : 'page-agent home'}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
@@ -44,7 +44,7 @@ export default function Header() {
|
|||||||
animateOnHover={true}
|
animateOnHover={true}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
GUI Agent in your webpage
|
AI Agent In Your Webpage
|
||||||
</HyperText>
|
</HyperText>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -56,7 +56,7 @@ export default function Header() {
|
|||||||
aria-label="Mobile navigation"
|
aria-label="Mobile navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||||
aria-label={isZh ? '文档' : 'Docs'}
|
aria-label={isZh ? '文档' : 'Docs'}
|
||||||
>
|
>
|
||||||
@@ -90,7 +90,7 @@ export default function Header() {
|
|||||||
{import.meta.env.VERSION}
|
{import.meta.env.VERSION}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<BookOpen className="w-4 h-4" />
|
<BookOpen className="w-4 h-4" />
|
||||||
@@ -138,7 +138,7 @@ export default function Header() {
|
|||||||
role="navigation"
|
role="navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type Level = 2 | 3
|
||||||
|
|
||||||
|
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
||||||
|
id: string
|
||||||
|
level?: Level
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelStyles = {
|
||||||
|
2: { tag: 'h2', className: 'text-2xl font-semibold mb-4' },
|
||||||
|
3: { tag: 'h3', className: 'text-xl font-semibold mb-3' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
||||||
|
const ref = useRef<HTMLHeadingElement>(null)
|
||||||
|
const { tag: Tag, className: defaultClassName } = levelStyles[level]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.location.hash === `#${id}`) {
|
||||||
|
ref.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
className={cn('group relative scroll-mt-20', defaultClassName, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`#${id}`}
|
||||||
|
className="absolute -left-5 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-blue-500 transition-opacity no-underline"
|
||||||
|
aria-label={`Link to ${id}`}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</a>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ export function MagicCard({
|
|||||||
`,
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="bg-background absolute inset-px rounded-[inherit]" />
|
<div className="absolute inset-px rounded-[inherit] bg-white dark:bg-neutral-900" />
|
||||||
<motion.div
|
<motion.div
|
||||||
className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { ComponentPropsWithoutRef } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface MarqueeProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
/**
|
||||||
|
* Optional CSS class name to apply custom styles
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
/**
|
||||||
|
* Whether to reverse the animation direction
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
reverse?: boolean
|
||||||
|
/**
|
||||||
|
* Whether to pause the animation on hover
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
pauseOnHover?: boolean
|
||||||
|
/**
|
||||||
|
* Content to be displayed in the marquee
|
||||||
|
*/
|
||||||
|
children: React.ReactNode
|
||||||
|
/**
|
||||||
|
* Whether to animate vertically instead of horizontally
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
vertical?: boolean
|
||||||
|
/**
|
||||||
|
* Number of times to repeat the content
|
||||||
|
* @default 4
|
||||||
|
*/
|
||||||
|
repeat?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Marquee({
|
||||||
|
className,
|
||||||
|
reverse = false,
|
||||||
|
pauseOnHover = false,
|
||||||
|
children,
|
||||||
|
vertical = false,
|
||||||
|
repeat = 4,
|
||||||
|
...props
|
||||||
|
}: MarqueeProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]',
|
||||||
|
{
|
||||||
|
'flex-row': !vertical,
|
||||||
|
'flex-col': vertical,
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Array(repeat)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn('flex shrink-0 justify-around [gap:var(--gap)]', {
|
||||||
|
'animate-marquee flex-row': !vertical,
|
||||||
|
'animate-marquee-vertical flex-col': vertical,
|
||||||
|
'group-hover:[animation-play-state:paused]': pauseOnHover,
|
||||||
|
'[animation-direction:reverse]': reverse,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
// 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.0/dist/iife/page-agent.demo.js'
|
'https://cdn.jsdelivr.net/npm/page-agent@1.4.2/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.0/files/dist/iife/page-agent.demo.js'
|
'https://registry.npmmirror.com/page-agent/1.4.2/files/dist/iife/page-agent.demo.js'
|
||||||
|
|
||||||
// Demo LLM for website testing
|
// Demo LLM for website testing (homepage quick trial uses flash)
|
||||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
export const DEMO_MODEL = 'qwen3.5-flash'
|
||||||
export const DEMO_BASE_URL =
|
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
export const DEMO_API_KEY = 'NA'
|
||||||
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
|
||||||
|
|||||||
@@ -377,6 +377,24 @@ td {
|
|||||||
background-position: bottom center;
|
background-position: bottom center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
--animate-marquee: marquee var(--duration) infinite linear;
|
||||||
|
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
|
||||||
|
@keyframes marquee {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(-100% - var(--gap)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes marquee-vertical {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(calc(-100% - var(--gap)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* shadcn dark mode */
|
/* shadcn dark mode */
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Router } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
|
||||||
|
|
||||||
import { LanguageProvider } from './i18n/context'
|
import { LanguageProvider } from './i18n/context'
|
||||||
import { default as PagesRouter } from './router'
|
import { default as PagesRouter } from './router'
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
// Redirect legacy hash routes (e.g. /#/docs/foo) to clean paths
|
||||||
|
const { hash } = window.location
|
||||||
|
if (hash.length > 1 && hash.includes('/')) {
|
||||||
|
const path = hash.replace(/^#\/?/, '/')
|
||||||
|
history.replaceState(null, '', '/page-agent' + path)
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<Router hook={useHashLocation}>
|
<Router base="/page-agent">
|
||||||
<PagesRouter />
|
<PagesRouter />
|
||||||
</Router>
|
</Router>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
|
|||||||
@@ -1,586 +1,27 @@
|
|||||||
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
|
import { Suspense, lazy } from 'react'
|
||||||
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { Link, useSearchParams } from 'wouter'
|
|
||||||
|
|
||||||
import Footer from '../components/Footer'
|
import HeroSection from './home/HeroSection'
|
||||||
import Header from '../components/Header'
|
|
||||||
import { AnimatedGradientText } from '../components/ui/animated-gradient-text'
|
|
||||||
import { Highlighter } from '../components/ui/highlighter'
|
|
||||||
import { NeonGradientCard } from '../components/ui/neon-gradient-card'
|
|
||||||
import { Particles } from '../components/ui/particles'
|
|
||||||
import { SparklesText } from '../components/ui/sparkles-text'
|
|
||||||
import {
|
|
||||||
CDN_DEMO_CN_URL,
|
|
||||||
CDN_DEMO_URL,
|
|
||||||
DEMO_API_KEY,
|
|
||||||
DEMO_BASE_URL,
|
|
||||||
DEMO_MODEL,
|
|
||||||
} from '../constants'
|
|
||||||
import { useLanguage } from '../i18n/context'
|
|
||||||
|
|
||||||
function getInjection(useCN?: boolean) {
|
const FeaturesSection = lazy(() => import('./home/FeaturesSection'))
|
||||||
const cdn = useCN ? CDN_DEMO_CN_URL : CDN_DEMO_URL
|
const ScenariosSection = lazy(() => import('./home/ScenariosSection'))
|
||||||
|
const OneMoreThingSection = lazy(() => import('./home/OneMoreThingSection'))
|
||||||
const injection = encodeURI(
|
|
||||||
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
|
|
||||||
)
|
|
||||||
|
|
||||||
return `
|
|
||||||
<a
|
|
||||||
href=${injection}
|
|
||||||
class="inline-flex items-center text-xs px-3 py-2 bg-blue-500 text-white font-medium rounded-lg hover:shadow-md transform hover:scale-105 transition-all duration-200 cursor-move border-2 border-dashed border-green-300"
|
|
||||||
draggable="true"
|
|
||||||
onclick="return false;"
|
|
||||||
title="Drag me to your bookmarks bar!"
|
|
||||||
>
|
|
||||||
✨PageAgent
|
|
||||||
</a>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { language, isZh } = useLanguage()
|
|
||||||
|
|
||||||
const defaultTask = isZh
|
|
||||||
? '从导航栏中进入文档页,打开"快速开始"相关的文档,帮我总结成 markdown'
|
|
||||||
: 'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown'
|
|
||||||
|
|
||||||
const [task, setTask] = useState(() => defaultTask)
|
|
||||||
|
|
||||||
// Update task when language changes
|
|
||||||
useEffect(() => {
|
|
||||||
setTask(defaultTask)
|
|
||||||
}, [defaultTask])
|
|
||||||
|
|
||||||
const [params, setParams] = useSearchParams()
|
|
||||||
const isOther = params.has('try_other')
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'try' | 'other'>(isOther ? 'other' : 'try')
|
|
||||||
const [cdnSource, setCdnSource] = useState<'international' | 'china'>('international')
|
|
||||||
|
|
||||||
const handleExecute = async () => {
|
|
||||||
if (!task.trim()) return
|
|
||||||
|
|
||||||
const win = window as any
|
|
||||||
|
|
||||||
// Lazy load PageAgent only when user clicks execute
|
|
||||||
if (!win.pageAgent || win.pageAgent.disposed) {
|
|
||||||
const { PageAgent } = await import('page-agent')
|
|
||||||
win.pageAgent = new PageAgent({
|
|
||||||
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
|
|
||||||
interactiveBlacklist: [document.getElementById('root')!],
|
|
||||||
language: language,
|
|
||||||
|
|
||||||
instructions: {
|
|
||||||
system: 'You are a helpful assistant on PageAgent website.',
|
|
||||||
getPageInstructions: (url) => {
|
|
||||||
const hint = url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined
|
|
||||||
console.log('[instructions] getPageInstructions:', url, '->', hint)
|
|
||||||
return hint
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
model:
|
|
||||||
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
|
|
||||||
? import.meta.env.LLM_MODEL_NAME
|
|
||||||
: DEMO_MODEL,
|
|
||||||
baseURL:
|
|
||||||
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
|
|
||||||
? import.meta.env.LLM_BASE_URL
|
|
||||||
: DEMO_BASE_URL,
|
|
||||||
apiKey:
|
|
||||||
import.meta.env.DEV && import.meta.env.LLM_API_KEY
|
|
||||||
? import.meta.env.LLM_API_KEY
|
|
||||||
: DEMO_API_KEY,
|
|
||||||
|
|
||||||
// enableAskUser: false,
|
|
||||||
// promptForNextTask: false,
|
|
||||||
// enablePanel: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await win.pageAgent.execute(task)
|
|
||||||
console.log(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-900 dark:to-gray-800">
|
<>
|
||||||
<Header />
|
<HeroSection />
|
||||||
|
<Suspense
|
||||||
{/* Hero Section */}
|
fallback={
|
||||||
<main id="main-content">
|
<div className="flex items-center justify-center gap-3 py-20 text-gray-400">
|
||||||
<section
|
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
className="relative px-6 py-22 pb-18 lg:py-28 overflow-hidden"
|
Loading...
|
||||||
aria-labelledby="hero-heading"
|
|
||||||
>
|
|
||||||
<div className="max-w-7xl mx-auto text-center">
|
|
||||||
{/* Background Pattern + Particles */}
|
|
||||||
<div className="absolute inset-0 opacity-30" aria-hidden="true">
|
|
||||||
<div className="absolute inset-0 bg-linear-to-r from-blue-400/20 to-purple-400/20 rounded-3xl transform rotate-1"></div>
|
|
||||||
<div className="absolute inset-0 bg-linear-to-l from-purple-400/20 to-blue-400/20 rounded-3xl transform -rotate-1"></div>
|
|
||||||
</div>
|
|
||||||
<Particles
|
|
||||||
className="absolute inset-0"
|
|
||||||
quantity={80}
|
|
||||||
staticity={30}
|
|
||||||
ease={80}
|
|
||||||
color="#6366f1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative z-10">
|
|
||||||
<div className="inline-flex items-center px-4 py-2 mb-8 text-sm font-medium bg-white/90 dark:bg-gray-800/90 rounded-full shadow-lg border border-gray-200 dark:border-gray-700">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 bg-blue-500 rounded-full mr-2 animate-pulse"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
<AnimatedGradientText colorFrom="#3b82f6" colorTo="#8b5cf6">
|
|
||||||
GUI Agent in your webpage
|
|
||||||
</AnimatedGradientText>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1
|
|
||||||
id="hero-heading"
|
|
||||||
className="text-5xl lg:text-7xl font-bold mb-8 bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent pb-1"
|
|
||||||
>
|
|
||||||
{isZh ? '让你的 Web 应用' : 'The AI Operator'}
|
|
||||||
<br />
|
|
||||||
{isZh ? '拥有 AI 操作员' : 'Living in Your Web App'}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
|
|
||||||
<Highlighter action="underline" color="#8b5cf6" strokeWidth={2}>
|
|
||||||
<span className="bg-linear-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent font-bold">
|
|
||||||
{isZh ? '🪄一行代码' : '🪄One line of code'}
|
|
||||||
</span>
|
|
||||||
</Highlighter>
|
|
||||||
{isZh
|
|
||||||
? ',为你的网站添加 GUI Agent。'
|
|
||||||
: ' adds intelligent GUI Agents to your website.'}
|
|
||||||
<br />
|
|
||||||
{isZh
|
|
||||||
? '用户/答疑机器人给出文字指示,AI 帮你操作页面。'
|
|
||||||
: 'Users give natural language commands, AI handles the rest.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Try It Now Section - Tab Card */}
|
|
||||||
<div className="mb-12">
|
|
||||||
<div className="max-w-3xl mx-auto">
|
|
||||||
<NeonGradientCard
|
|
||||||
borderSize={2}
|
|
||||||
borderRadius={20}
|
|
||||||
neonColors={{ firstColor: '#ff00aa', secondColor: '#00FFF1' }}
|
|
||||||
>
|
|
||||||
{/* Tab Headers */}
|
|
||||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('try')}
|
|
||||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tl-2xl ${
|
|
||||||
activeTab === 'try'
|
|
||||||
? 'bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/30 dark:to-purple-900/30 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isZh ? '🚀 立即尝试' : '🚀 Try It Now'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('other')}
|
|
||||||
className={`flex-1 px-4 py-4 text-lg font-medium transition-colors duration-200 rounded-tr-2xl ${
|
|
||||||
activeTab === 'other'
|
|
||||||
? 'bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/30 dark:to-blue-900/30 text-green-700 dark:text-green-300 border-b-2 border-green-500'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isZh ? '🌐 其他网页尝试' : '🌐 Try on Other Sites'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
<div className="p-4">
|
|
||||||
{activeTab === 'try' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
value={task}
|
|
||||||
onChange={(e) => setTask(e.target.value)}
|
|
||||||
placeholder={
|
|
||||||
isZh
|
|
||||||
? '输入您想要 AI 执行的任务...'
|
|
||||||
: 'Describe what you want AI to do...'
|
|
||||||
}
|
|
||||||
className="w-full px-4 py-3 pr-20 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm mb-0"
|
|
||||||
data-page-agent-not-interactive
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleExecute}
|
|
||||||
// disabled
|
|
||||||
// disabled={!task.trim()}
|
|
||||||
className="absolute right-2 top-2 px-5 py-1.5 bg-linear-to-r from-blue-600 to-purple-600 text-white font-medium rounded-md hover:shadow-md transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none text-sm"
|
|
||||||
data-page-agent-not-interactive
|
|
||||||
>
|
|
||||||
{isZh ? '执行' : 'Run'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'other' && (
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
|
||||||
{/* 左侧:操作步骤 */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Keyboard Shortcut Hint */}
|
|
||||||
<div className="bg-blue-50 dark:bg-gray-700 p-4 rounded-lg">
|
|
||||||
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
|
|
||||||
<span className="font-semibold">
|
|
||||||
{isZh ? '步骤 1:' : 'Step 1:'}
|
|
||||||
</span>{' '}
|
|
||||||
{isZh ? '显示收藏夹栏' : 'Show your bookmarks bar'}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-center gap-2">
|
|
||||||
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
|
|
||||||
Ctrl + Shift + B
|
|
||||||
</kbd>
|
|
||||||
<span className="text-gray-500 dark:text-gray-400">
|
|
||||||
{isZh ? '或' : 'or'}
|
|
||||||
</span>
|
|
||||||
<kbd className="px-2 py-1 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded text-xs font-mono">
|
|
||||||
⌘ + Shift + B
|
|
||||||
</kbd>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Draggable Bookmarklet */}
|
|
||||||
<div className="bg-green-50 dark:bg-gray-700 p-4 rounded-lg">
|
|
||||||
<p className="text-gray-700 dark:text-gray-300 text-sm mb-3">
|
|
||||||
<span className="font-semibold">
|
|
||||||
{isZh ? '步骤 2:' : 'Step 2:'}
|
|
||||||
</span>{' '}
|
|
||||||
{isZh
|
|
||||||
? '拖拽下面按钮到收藏夹栏'
|
|
||||||
: 'Drag this button to your bookmarks'}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-center gap-3">
|
|
||||||
<select
|
|
||||||
value={cdnSource}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCdnSource(e.target.value as 'international' | 'china')
|
|
||||||
}
|
|
||||||
className="px-2 py-1.5 text-xs border border-gray-300 dark:border-gray-500 rounded bg-white dark:bg-gray-600 text-gray-700 dark:text-gray-200"
|
|
||||||
>
|
|
||||||
<option value="international">
|
|
||||||
{isZh ? '国际' : 'International'}
|
|
||||||
</option>
|
|
||||||
<option value="china">
|
|
||||||
{isZh ? '国内镜像' : 'China Mirror'}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: getInjection(cdnSource === 'china'),
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Usage Instructions */}
|
|
||||||
<div className="bg-purple-50 dark:bg-gray-700 p-4 rounded-lg">
|
|
||||||
<p className="text-gray-700 dark:text-gray-300 text-sm">
|
|
||||||
<span className="font-semibold">
|
|
||||||
{isZh ? '步骤 3:' : 'Step 3:'}
|
|
||||||
</span>{' '}
|
|
||||||
{isZh
|
|
||||||
? '在其他网站点击收藏夹中的按钮即可使用'
|
|
||||||
: 'Click the bookmark on any site to activate'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧:注意事项 */}
|
|
||||||
<div className="bg-yellow-50 dark:bg-gray-700 p-4 rounded-lg">
|
|
||||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-3 text-sm">
|
|
||||||
{isZh ? '⚠️ 注意' : '⚠️ Heads Up'}
|
|
||||||
</h4>
|
|
||||||
<ul className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh
|
|
||||||
? '仅做技术评估,链接定期失效'
|
|
||||||
: 'Demo only—link may expire without notice'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh
|
|
||||||
? '使用 DeepSeek 模型,参考 DeepSeek 用户协议和隐私政策'
|
|
||||||
: 'This free demo uses DeepSeek API (see their terms and privacy policy)'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh
|
|
||||||
? '部分网站屏蔽了链接嵌入,将无反应'
|
|
||||||
: 'Some sites block script injection (CSP policies)'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh
|
|
||||||
? '仅支持单页应用,页面跳转后需要重新注入'
|
|
||||||
: 'Works on single-page apps only—reload required after navigation'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh
|
|
||||||
? '仅识别文本,不识别图像,不支持拖拽等复杂交互'
|
|
||||||
: 'Text-only understanding—no image recognition or drag-and-drop'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start text-left">
|
|
||||||
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full mt-2 mr-2 shrink-0 "></span>
|
|
||||||
{isZh ? '详细使用限制参照' : 'Full limitations in'}{' '}
|
|
||||||
<Link
|
|
||||||
href="/docs/introduction/limitations"
|
|
||||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
|
||||||
>
|
|
||||||
{isZh ? '《文档》' : 'Docs'}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</NeonGradientCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
className="flex flex-wrap justify-center gap-6 text-sm text-gray-500 dark:text-gray-400"
|
|
||||||
role="list"
|
|
||||||
>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 bg-green-500 rounded-full mr-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
{isZh ? '纯前端方案' : 'Pure Front-end Solution'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 bg-green-500 rounded-full mr-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
{isZh ? '支持私有模型' : 'Your Own Models'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 bg-green-500 rounded-full mr-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
{isZh ? '无痛脱敏' : 'Built-in Privacy'}
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center">
|
|
||||||
<span
|
|
||||||
className="w-2 h-2 bg-green-500 rounded-full mr-2"
|
|
||||||
aria-hidden="true"
|
|
||||||
></span>
|
|
||||||
{isZh ? 'MIT 开源' : 'MIT Open Source'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
}
|
||||||
|
>
|
||||||
{/* Features Section */}
|
<FeaturesSection />
|
||||||
<section
|
<ScenariosSection />
|
||||||
className="px-6 py-20 bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm"
|
<OneMoreThingSection />
|
||||||
aria-labelledby="features-heading"
|
</Suspense>
|
||||||
>
|
</>
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8" role="list">
|
|
||||||
{/* Feature 1 */}
|
|
||||||
<article
|
|
||||||
className="group p-8 bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-14 h-14 bg-linear-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Box className="w-7 h-7 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '纯页面内方案' : 'In-page Solution'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
{isZh
|
|
||||||
? '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。'
|
|
||||||
: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.'}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Feature 2 */}
|
|
||||||
<article
|
|
||||||
className="group p-8 bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-14 h-14 bg-linear-to-br from-green-500 to-green-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Zap className="w-7 h-7 text-white fill-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '零后端部署' : 'Zero Backend Setup'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
{isZh
|
|
||||||
? '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。'
|
|
||||||
: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.'}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Feature 3 */}
|
|
||||||
<article
|
|
||||||
className="group p-8 bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-14 h-14 bg-linear-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-7 h-7 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '普惠智能交互' : 'Natural Language UI'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
{isZh
|
|
||||||
? '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。'
|
|
||||||
: 'Transform complex admin panels into chat interfaces. Make powerful tools accessible to everyone, not just experts.'}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Feature 4 */}
|
|
||||||
<article
|
|
||||||
className="group p-8 bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 border border-gray-100 dark:border-gray-700"
|
|
||||||
role="listitem"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="w-14 h-14 bg-linear-to-br from-orange-500 to-orange-600 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 group-hover:rotate-3 transition-all duration-300 shadow-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Shield className="w-7 h-7 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '安全可控集成' : 'Secure by Design'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
{isZh
|
|
||||||
? '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。'
|
|
||||||
: 'Control what AI can access with allowlists, data masking, and custom knowledge injection. Your rules, your data.'}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Use Cases Section */}
|
|
||||||
<section className="px-6 py-20" aria-labelledby="use-cases-heading">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<div className="text-center mb-16">
|
|
||||||
<SparklesText
|
|
||||||
className="text-4xl lg:text-5xl mb-6"
|
|
||||||
colors={{ first: '#3b82f6', second: '#8b5cf6' }}
|
|
||||||
>
|
|
||||||
{isZh ? '应用场景' : 'Where It Shines'}
|
|
||||||
</SparklesText>
|
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
|
||||||
{isZh
|
|
||||||
? '从简单的表单填写到复杂的业务流程,AI 都能理解并执行'
|
|
||||||
: 'From simple forms to complex workflows, AI understands and executes'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-12" role="list">
|
|
||||||
{/* Use Case 1 */}
|
|
||||||
<div className="group bg-linear-to-br from-blue-100 to-purple-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="w-12 h-12 bg-linear-to-br from-blue-500 to-purple-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
|
||||||
<Bot className="w-6 h-6 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '对接答疑机器人' : 'Supercharge Support Bots'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
{isZh
|
|
||||||
? '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。'
|
|
||||||
: 'Stop telling users where to click—let AI do it for them. Turn your chatbot from a guide into an operator that actually completes tasks.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Use Case 2 */}
|
|
||||||
<div className="group bg-linear-to-br from-green-100 to-blue-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="w-12 h-12 bg-linear-to-br from-green-500 to-blue-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
|
||||||
<Sparkles className="w-6 h-6 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '交互升级/智能化改造' : 'Modernize Legacy Apps'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
{isZh
|
|
||||||
? '一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。'
|
|
||||||
: 'Add AI superpowers to old software without rebuilding. One script tag transforms complex enterprise tools into chat-driven interfaces.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Use Case 3 */}
|
|
||||||
<div className="group bg-linear-to-br from-purple-100 to-pink-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="w-12 h-12 bg-linear-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
|
||||||
<PlayCircle className="w-6 h-6 text-white fill-white/30" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '产品教学' : 'Interactive Walkthroughs'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
{isZh
|
|
||||||
? '向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。'
|
|
||||||
: "Show, don't tell. Let AI demonstrate workflows in real-time—perfect for onboarding or training new users on complex systems."}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Use Case 4 */}
|
|
||||||
<div className="group bg-linear-to-br from-orange-100 to-red-100 dark:from-gray-700 dark:to-gray-800 p-8 rounded-2xl hover:shadow-xl transition-all duration-300">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="w-12 h-12 bg-linear-to-br from-orange-500 to-red-500 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform duration-300 shadow-md">
|
|
||||||
<Users className="w-6 h-6 text-white" strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
|
|
||||||
{isZh ? '无障碍支持' : 'Accessibility First'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
|
||||||
{isZh
|
|
||||||
? '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。'
|
|
||||||
: 'Make web apps accessible through natural language. Perfect for screen readers, voice control, or users who find traditional interfaces challenging.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
|||||||
{ title: isZh ? '概览' : 'Overview', path: '/introduction/overview' },
|
{ title: isZh ? '概览' : 'Overview', path: '/introduction/overview' },
|
||||||
{ title: isZh ? '快速开始' : 'Quick Start', path: '/introduction/quick-start' },
|
{ title: isZh ? '快速开始' : 'Quick Start', path: '/introduction/quick-start' },
|
||||||
{ title: isZh ? '使用限制' : 'Limitations', path: '/introduction/limitations' },
|
{ title: isZh ? '使用限制' : 'Limitations', path: '/introduction/limitations' },
|
||||||
|
{
|
||||||
|
title: isZh ? '故障排查' : 'Troubleshooting',
|
||||||
|
path: '/introduction/troubleshooting',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '🚧 ' + (isZh ? '最佳实践' : 'Best Practices'),
|
title: '🚧 ' + (isZh ? '最佳实践' : 'Best Practices'),
|
||||||
path: '/integration/best-practices',
|
path: '/integration/best-practices',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { APIDivider, APIReference } from '@/components/ui/api-reference'
|
import { APIDivider, APIReference } from '@/components/ui/api-reference'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* Architecture */}
|
{/* Architecture */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '架构' : 'Architecture'}</h2>
|
<Heading id="architecture">{isZh ? '架构' : 'Architecture'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? 'PageAgent 由三个独立模块组成,可自由组合:'
|
? 'PageAgent 由三个独立模块组成,可自由组合:'
|
||||||
@@ -45,7 +46,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* Two Event Streams */}
|
{/* Two Event Streams */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '两个事件流' : 'Two Event Streams'}</h2>
|
<Heading id="two-event-streams">{isZh ? '两个事件流' : 'Two Event Streams'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? 'PageAgentCore 提供两种不同性质的事件流,方便 UI 渲染:'
|
? 'PageAgentCore 提供两种不同性质的事件流,方便 UI 渲染:'
|
||||||
@@ -120,7 +121,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* All Events */}
|
{/* All Events */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '所有事件' : 'All Events'}</h2>
|
<Heading id="all-events">{isZh ? '所有事件' : 'All Events'}</Heading>
|
||||||
<APIReference
|
<APIReference
|
||||||
properties={[
|
properties={[
|
||||||
{
|
{
|
||||||
@@ -155,7 +156,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* HistoricalEvent Types */}
|
{/* HistoricalEvent Types */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">HistoricalEvent</h2>
|
<Heading id="historicalevent">HistoricalEvent</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh ? 'agent.history 数组中的事件类型:' : 'Event types in agent.history array:'}
|
{isZh ? 'agent.history 数组中的事件类型:' : 'Event types in agent.history array:'}
|
||||||
</p>
|
</p>
|
||||||
@@ -172,7 +173,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* AgentActivity Types */}
|
{/* AgentActivity Types */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">AgentActivity</h2>
|
<Heading id="agentactivity">AgentActivity</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh ? 'activity 事件的 detail 类型:' : 'The detail type of activity events:'}
|
{isZh ? 'activity 事件的 detail 类型:' : 'The detail type of activity events:'}
|
||||||
</p>
|
</p>
|
||||||
@@ -191,9 +192,7 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* React Hooks Example */}
|
{/* React Hooks Example */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<Heading id="using-react-hooks">{isZh ? '使用 React Hooks' : 'Using React Hooks'}</Heading>
|
||||||
{isZh ? '使用 React Hooks' : 'Using React Hooks'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh ? '监听事件并更新 React 状态:' : 'Listen to events and update React state:'}
|
{isZh ? '监听事件并更新 React 状态:' : 'Listen to events and update React state:'}
|
||||||
</p>
|
</p>
|
||||||
@@ -229,9 +228,9 @@ export default function CustomUIDocs() {
|
|||||||
|
|
||||||
{/* Assembly Example */}
|
{/* Assembly Example */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<Heading id="assembling-core-controller-custom-ui">
|
||||||
{isZh ? '组装 Core + Controller + 自定义 UI' : 'Assembling Core + Controller + Custom UI'}
|
{isZh ? '组装 Core + Controller + 自定义 UI' : 'Assembling Core + Controller + Custom UI'}
|
||||||
</h2>
|
</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '参考内置 PageAgent 的实现方式,用自定义 UI 替换 Panel:'
|
? '参考内置 PageAgent 的实现方式,用自定义 UI 替换 Panel:'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { APIDivider, APIReference, TypeRef } from '@/components/ui/api-reference'
|
import { APIDivider, APIReference, TypeRef } from '@/components/ui/api-reference'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
@@ -17,9 +18,9 @@ export default function PageAgentCoreDocs() {
|
|||||||
|
|
||||||
{/* When to use */}
|
{/* When to use */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<Heading id="when-to-use-pageagentcore">
|
||||||
{isZh ? '何时使用 PageAgentCore' : 'When to Use PageAgentCore'}
|
{isZh ? '何时使用 PageAgentCore' : 'When to Use PageAgentCore'}
|
||||||
</h2>
|
</Heading>
|
||||||
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2">
|
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
<li>{isZh ? '需要自定义 UI 界面' : 'Need a custom UI interface'}</li>
|
<li>{isZh ? '需要自定义 UI 界面' : 'Need a custom UI interface'}</li>
|
||||||
<li>{isZh ? '在自动化测试中无头运行' : 'Running headless in automated tests'}</li>
|
<li>{isZh ? '在自动化测试中无头运行' : 'Running headless in automated tests'}</li>
|
||||||
@@ -38,7 +39,7 @@ export default function PageAgentCoreDocs() {
|
|||||||
|
|
||||||
{/* Basic Usage */}
|
{/* Basic Usage */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '基本用法' : 'Basic Usage'}</h2>
|
<Heading id="basic-usage">{isZh ? '基本用法' : 'Basic Usage'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="typescript"
|
language="typescript"
|
||||||
code={`import { PageAgentCore } from '@page-agent/core'
|
code={`import { PageAgentCore } from '@page-agent/core'
|
||||||
@@ -76,7 +77,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* LLM Configuration */}
|
{/* LLM Configuration */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">LLMConfig</h2>
|
<Heading id="llmconfig">LLMConfig</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '配置与大语言模型的连接参数。支持 OpenAI 兼容的 API。'
|
? '配置与大语言模型的连接参数。支持 OpenAI 兼容的 API。'
|
||||||
@@ -133,7 +134,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Agent Configuration */}
|
{/* Agent Configuration */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">AgentConfig</h2>
|
<Heading id="agentconfig">AgentConfig</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
|
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
|
||||||
@@ -210,7 +211,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Lifecycle Hooks */}
|
{/* Lifecycle Hooks */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '生命周期钩子' : 'Lifecycle Hooks'}</h2>
|
<Heading id="lifecycle-hooks">{isZh ? '生命周期钩子' : 'Lifecycle Hooks'}</Heading>
|
||||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
|
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
|
||||||
<p className="text-amber-800 dark:text-amber-200 text-sm">
|
<p className="text-amber-800 dark:text-amber-200 text-sm">
|
||||||
<strong>⚠️ {isZh ? '警告' : 'Warning'}:</strong>{' '}
|
<strong>⚠️ {isZh ? '警告' : 'Warning'}:</strong>{' '}
|
||||||
@@ -262,7 +263,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* PageController Configuration */}
|
{/* PageController Configuration */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">PageControllerConfig</h2>
|
<Heading id="pagecontrollerconfig">PageControllerConfig</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '配置 DOM 提取、元素交互和视觉反馈。'
|
? '配置 DOM 提取、元素交互和视觉反馈。'
|
||||||
@@ -321,7 +322,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Properties */}
|
{/* Properties */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '属性' : 'Properties'}</h2>
|
<Heading id="properties">{isZh ? '属性' : 'Properties'}</Heading>
|
||||||
<APIReference
|
<APIReference
|
||||||
properties={[
|
properties={[
|
||||||
{
|
{
|
||||||
@@ -366,7 +367,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Methods */}
|
{/* Methods */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '方法' : 'Methods'}</h2>
|
<Heading id="methods">{isZh ? '方法' : 'Methods'}</Heading>
|
||||||
<APIReference
|
<APIReference
|
||||||
properties={[
|
properties={[
|
||||||
{
|
{
|
||||||
@@ -376,13 +377,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',
|
||||||
@@ -396,7 +390,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Events */}
|
{/* Events */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '事件' : 'Events'}</h2>
|
<Heading id="events">{isZh ? '事件' : 'Events'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh ? (
|
{isZh ? (
|
||||||
<>
|
<>
|
||||||
@@ -445,7 +439,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* ExecutionResult */}
|
{/* ExecutionResult */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">ExecutionResult</h2>
|
<Heading id="executionresult">ExecutionResult</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="typescript"
|
language="typescript"
|
||||||
code={`interface ExecutionResult {
|
code={`interface ExecutionResult {
|
||||||
@@ -461,7 +455,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* AgentActivity */}
|
{/* AgentActivity */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">AgentActivity</h2>
|
<Heading id="agentactivity">AgentActivity</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="typescript"
|
language="typescript"
|
||||||
code={`type AgentActivity =
|
code={`type AgentActivity =
|
||||||
@@ -477,7 +471,7 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
|
|
||||||
{/* Headless Usage */}
|
{/* Headless Usage */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '无头模式' : 'Headless Mode'}</h2>
|
<Heading id="headless-mode">{isZh ? '无头模式' : 'Headless Mode'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '在非 DOM 环境中,你必须实现自定义的 PageController(例如远程操作页面或 Puppeteer)。'
|
? '在非 DOM 环境中,你必须实现自定义的 PageController(例如远程操作页面或 Puppeteer)。'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Link } from 'wouter'
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { APIReference, TypeRef } from '@/components/ui/api-reference'
|
import { APIReference, TypeRef } from '@/components/ui/api-reference'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
@@ -19,9 +20,9 @@ export default function PageAgentDocs() {
|
|||||||
|
|
||||||
{/* When to use */}
|
{/* When to use */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<Heading id="when-to-use-pageagent">
|
||||||
{isZh ? '何时使用 PageAgent' : 'When to Use PageAgent'}
|
{isZh ? '何时使用 PageAgent' : 'When to Use PageAgent'}
|
||||||
</h2>
|
</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '在大多数场景下,你应该使用 PageAgent。它提供了开箱即用的完整体验:'
|
? '在大多数场景下,你应该使用 PageAgent。它提供了开箱即用的完整体验:'
|
||||||
@@ -43,7 +44,7 @@ export default function PageAgentDocs() {
|
|||||||
|
|
||||||
{/* Basic Usage */}
|
{/* Basic Usage */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '基本用法' : 'Basic Usage'}</h2>
|
<Heading id="basic-usage">{isZh ? '基本用法' : 'Basic Usage'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="typescript"
|
language="typescript"
|
||||||
code={`import { PageAgent } from 'page-agent'
|
code={`import { PageAgent } from 'page-agent'
|
||||||
@@ -69,7 +70,7 @@ console.log(result.history) // Full execution history`}
|
|||||||
|
|
||||||
{/* Class Definition */}
|
{/* Class Definition */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '类定义' : 'Class Definition'}</h2>
|
<Heading id="class-definition">{isZh ? '类定义' : 'Class Definition'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="typescript"
|
language="typescript"
|
||||||
code={`class PageAgent extends PageAgentCore {
|
code={`class PageAgent extends PageAgentCore {
|
||||||
@@ -109,7 +110,7 @@ console.log(result.history) // Full execution history`}
|
|||||||
|
|
||||||
{/* Configuration */}
|
{/* Configuration */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '配置' : 'Configuration'}</h2>
|
<Heading id="configuration">{isZh ? '配置' : 'Configuration'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? 'PageAgent 使用与 PageAgentCore 相同的配置接口。'
|
? 'PageAgent 使用与 PageAgentCore 相同的配置接口。'
|
||||||
@@ -144,7 +145,7 @@ console.log(result.history) // Full execution history`}
|
|||||||
|
|
||||||
{/* Panel Property */}
|
{/* Panel Property */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? 'Panel 属性' : 'Panel Property'}</h2>
|
<Heading id="panel-property">{isZh ? 'Panel 属性' : 'Panel Property'}</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性访问它来控制 UI:'
|
? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性访问它来控制 UI:'
|
||||||
@@ -185,9 +186,9 @@ agent.panel.dispose()`}
|
|||||||
|
|
||||||
{/* Comparison with PageAgentCore */}
|
{/* Comparison with PageAgentCore */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">
|
<Heading id="pageagent-vs-pageagentcore">
|
||||||
{isZh ? 'PageAgent vs PageAgentCore' : 'PageAgent vs PageAgentCore'}
|
{isZh ? 'PageAgent vs PageAgentCore' : 'PageAgent vs PageAgentCore'}
|
||||||
</h2>
|
</Heading>
|
||||||
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
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 { Heading } from '@/components/Heading'
|
||||||
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,105 +16,132 @@ 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>
|
<Heading id="key-features" className="text-2xl font-bold mb-4">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
{isZh ? '核心特性' : 'Key Features'}
|
||||||
|
</Heading>
|
||||||
|
<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>
|
<Heading id="get-the-extension" className="text-2xl font-bold mb-4">
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
{isZh ? '获取扩展' : 'Get the Extension'}
|
||||||
{isZh
|
</Heading>
|
||||||
? '扩展目前处于 Beta 阶段,请从 GitHub Releases 下载最新版本。'
|
<div className="flex flex-wrap gap-3">
|
||||||
: 'The extension is currently in beta. Download the latest version from GitHub Releases.'}
|
<a
|
||||||
</p>
|
href={chromeWebStoreUrl}
|
||||||
<a
|
target="_blank"
|
||||||
href="https://github.com/alibaba/page-agent/releases"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
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"
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
>
|
<path d={siChromewebstore.path} />
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
</svg>
|
||||||
<path d={siGithub.path} />
|
{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}
|
||||||
</svg>
|
</a>
|
||||||
{isZh ? '前往 GitHub Releases 下载' : 'Download from GitHub Releases'}
|
<a
|
||||||
</a>
|
href={githubReleasesUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d={siGithub.path} />
|
||||||
|
</svg>
|
||||||
|
{isZh ? 'GitHub Releases(更新版本)' : 'GitHub Releases (faster updates)'}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Relationship with PageAgent.js */}
|
||||||
|
<section>
|
||||||
|
<Heading id="how-it-relates-to-page-agent-js" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '与 PageAgent.js 的关系' : 'How It Relates to PageAgent.js'}
|
||||||
|
</Heading>
|
||||||
|
<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 */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<Heading id="third-party-integration" className="text-2xl font-bold mb-4">
|
||||||
{isZh ? '第三方接入' : 'Third-Party Integration'}
|
{isZh ? '第三方接入' : 'Third-Party Integration'}
|
||||||
</h2>
|
</Heading>
|
||||||
<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"
|
||||||
@@ -120,7 +150,9 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
|
|||||||
|
|
||||||
{/* API Reference */}
|
{/* API Reference */}
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? 'API 参考' : 'API Reference'}</h2>
|
<Heading id="api-reference" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? 'API 参考' : 'API Reference'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
{/* AI Assistant Instructions */}
|
{/* AI Assistant Instructions */}
|
||||||
<section className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
<section className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||||
@@ -139,61 +171,119 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
|
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
|
||||||
>
|
>
|
||||||
📄 {isZh ? '英文版 API 文档' : 'API Documentation (English)'}
|
📄 {isZh ? 'API 文档' : 'API Documentation'}
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/docs/extension_api_zh.md"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="block text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 hover:underline"
|
|
||||||
>
|
|
||||||
📄 {isZh ? '中文版 API 文档' : 'API Documentation (Chinese)'}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h3 className="text-xl font-semibold my-3">
|
{/* TypeScript Declaration */}
|
||||||
PAGE_AGENT_EXT.execute(task, llmConfig, hooks?)
|
<Heading id="typescript-declaration" className="text-2xl font-bold mb-4">
|
||||||
</h3>
|
{isZh ? 'TypeScript 类型声明' : 'TypeScript Declaration'}
|
||||||
|
</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '使用 LLM 配置执行任务。返回一个 Promise,在任务完成时 resolve。可选的 hooks 参数用于监听任务执行过程中的事件。'
|
? '推荐把 `execute` 的类型声明加入你的项目,获得完整类型提示。'
|
||||||
: 'Execute a task with LLM configuration. Returns a Promise that resolves when the task completes. Optional hooks parameter for listening to events during task execution.'}
|
: 'Add this `execute` declaration to your project for full type support.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={
|
code={
|
||||||
isZh
|
isZh
|
||||||
? `// 使用 LLM 配置和 hooks 执行任务
|
? `import type {
|
||||||
|
AgentActivity,
|
||||||
|
AgentStatus,
|
||||||
|
ExecutionResult,
|
||||||
|
HistoricalEvent
|
||||||
|
} from '@page-agent/core'
|
||||||
|
|
||||||
|
interface ExecuteConfig {
|
||||||
|
baseURL: string // LLM API 端点
|
||||||
|
apiKey: string // API 密钥
|
||||||
|
model: string // 模型名称
|
||||||
|
|
||||||
|
includeInitialTab?: boolean
|
||||||
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
onActivity?: (activity: AgentActivity) => void
|
||||||
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
|
PAGE_AGENT_EXT?: {
|
||||||
|
version: string
|
||||||
|
execute: Execute
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
: `import type {
|
||||||
|
AgentActivity,
|
||||||
|
AgentStatus,
|
||||||
|
ExecutionResult,
|
||||||
|
HistoricalEvent
|
||||||
|
} from '@page-agent/core'
|
||||||
|
|
||||||
|
interface ExecuteConfig {
|
||||||
|
baseURL: string // LLM API endpoint
|
||||||
|
apiKey: string // API key
|
||||||
|
model: string // Model name
|
||||||
|
|
||||||
|
includeInitialTab?: boolean
|
||||||
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
onActivity?: (activity: AgentActivity) => void
|
||||||
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
|
PAGE_AGENT_EXT?: {
|
||||||
|
version: string
|
||||||
|
execute: Execute
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
language="typescript"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 className="text-xl font-semibold mt-6 mb-3">PAGE_AGENT_EXT.execute(task, config)</h3>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
code={
|
||||||
|
isZh
|
||||||
|
? `// 使用配置执行任务
|
||||||
const result = await window.PAGE_AGENT_EXT.execute(
|
const result = await window.PAGE_AGENT_EXT.execute(
|
||||||
'在 GitHub 上搜索 "page-agent" 并打开第一个结果',
|
'在 GitHub 上搜索 "page-agent" 并打开第一个结果',
|
||||||
{
|
{
|
||||||
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 排除初始标签页
|
||||||
{
|
|
||||||
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('已停止')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(result) // 任务执行结果`
|
console.log(result) // 任务执行结果`
|
||||||
: `// Execute a task with LLM configuration and hooks
|
: `// Execute a task with configuration
|
||||||
const result = await window.PAGE_AGENT_EXT.execute(
|
const result = await window.PAGE_AGENT_EXT.execute(
|
||||||
'Search for "page-agent" on GitHub and open the first result',
|
'Search for "page-agent" on GitHub and open the first result',
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -202,138 +292,50 @@ 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>
|
||||||
|
|
||||||
{/* LLM Config */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? 'LLM 配置' : 'LLM Configuration'}</h2>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
code={
|
|
||||||
isZh
|
|
||||||
? `interface LLMConfig {
|
|
||||||
baseURL: string // LLM API 端点
|
|
||||||
apiKey: string // API 密钥
|
|
||||||
model: string // 模型名称
|
|
||||||
}`
|
|
||||||
: `interface LLMConfig {
|
|
||||||
baseURL: string // LLM API endpoint
|
|
||||||
apiKey: string // API key
|
|
||||||
model: string // Model name
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
language="typescript"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Execute Hooks */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? 'Execute Hooks' : 'Execute Hooks'}</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '通过 hooks 参数,你可以监听任务执行过程中的各种事件,实现实时更新 UI、日志记录等功能。'
|
|
||||||
: 'With hooks parameter, you can listen to various events during task execution for real-time UI updates, logging, and more.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CodeEditor
|
|
||||||
code={
|
|
||||||
isZh
|
|
||||||
? `interface ExecuteHooks {
|
|
||||||
// Agent 状态变化时调用(idle, running, error, completed 等)
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
|
|
||||||
// Agent 执行活动时调用(如点击、输入、导航等操作)
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
|
|
||||||
// 历史记录更新时调用(包含完整的事件历史)
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
|
|
||||||
// Agent 被停止时调用
|
|
||||||
onDispose?: () => void
|
|
||||||
}`
|
|
||||||
: `interface ExecuteHooks {
|
|
||||||
// Called when agent status changes (idle, running, error, completed, etc.)
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
|
|
||||||
// Called when agent performs an activity (click, input, navigation, etc.)
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
|
|
||||||
// Called when history is updated (contains full event history)
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
|
|
||||||
// Called when agent is disposed
|
|
||||||
onDispose?: () => void
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
language="typescript"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Security Notice */}
|
|
||||||
<section className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
|
||||||
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
|
|
||||||
⚠️ {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">
|
<Heading
|
||||||
{isZh
|
id="integrate-multipageagent-into-your-extension"
|
||||||
? '将 MultiPageAgent 融入你自己的插件'
|
className="text-2xl font-bold mb-4"
|
||||||
: 'Integrate MultiPageAgent into Your Extension'}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '你可以将 MultiPageAgent 集成到自己的浏览器扩展中,实现跨页面的 AI 自动化能力。'
|
|
||||||
: 'You can integrate MultiPageAgent into your own browser extension for cross-page AI automation capabilities.'}
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">TODO</p>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
|
||||||
{isZh ? '参考源码实现:' : 'Reference implementation:'}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
{isZh
|
||||||
<path d={siGithub.path} />
|
? '将 MultiPageAgent 集成你自己的插件'
|
||||||
</svg>
|
: 'Integrate MultiPageAgent into Your Extension'}
|
||||||
packages/extension/src/entrypoints/background.ts
|
</Heading>
|
||||||
</a>
|
<p>@TODO</p>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
|
||||||
|
: 'Start with the extension API docs, then use the background entry implementation as a reference.'}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d={siGithub.path} />
|
||||||
|
</svg>
|
||||||
|
packages/extension/src/entrypoints/background.ts
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function Instructions() {
|
export default function Instructions() {
|
||||||
@@ -16,9 +17,9 @@ export default function Instructions() {
|
|||||||
|
|
||||||
{/* System Instructions */}
|
{/* System Instructions */}
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6">
|
<Heading id="system-instructions" className="text-3xl font-bold mb-6">
|
||||||
{isZh ? '系统级指导 (System Instructions)' : 'System Instructions'}
|
{isZh ? '系统级指导 (System Instructions)' : 'System Instructions'}
|
||||||
</h2>
|
</Heading>
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
{isZh
|
{isZh
|
||||||
@@ -46,9 +47,9 @@ Guidelines:
|
|||||||
|
|
||||||
{/* Page Instructions */}
|
{/* Page Instructions */}
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6">
|
<Heading id="page-instructions" className="text-3xl font-bold mb-6">
|
||||||
{isZh ? '页面级指导 (Page Instructions)' : 'Page Instructions'}
|
{isZh ? '页面级指导 (Page Instructions)' : 'Page Instructions'}
|
||||||
</h2>
|
</Heading>
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
{isZh
|
{isZh
|
||||||
@@ -90,7 +91,9 @@ This is the product listing page.
|
|||||||
|
|
||||||
{/* How It Works */}
|
{/* How It Works */}
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6">{isZh ? '工作原理' : 'How It Works'}</h2>
|
<Heading id="how-it-works" className="text-3xl font-bold mb-6">
|
||||||
|
{isZh ? '工作原理' : 'How It Works'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function CustomTools() {
|
export default function CustomTools() {
|
||||||
@@ -17,7 +18,9 @@ export default function CustomTools() {
|
|||||||
|
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '工具注册' : 'Tool Registration'}</h2>
|
<Heading id="tool-registration" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '工具注册' : 'Tool Registration'}
|
||||||
|
</Heading>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。'
|
? '每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。'
|
||||||
@@ -25,7 +28,7 @@ export default function CustomTools() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`import zod from 'zod'
|
code={`import * as zod from 'zod'
|
||||||
import { PageAgent, tool } from 'page-agent'
|
import { PageAgent, tool } from 'page-agent'
|
||||||
|
|
||||||
// override internal tool
|
// override internal tool
|
||||||
@@ -55,7 +58,9 @@ const pageAgent = new PageAgent({customTools})
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '页面过滤器' : 'Page Filter'}</h2>
|
<Heading id="page-filter" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '页面过滤器' : 'Page Filter'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<BetaNotice />
|
<BetaNotice />
|
||||||
|
|
||||||
@@ -89,7 +94,9 @@ const pageAgent = new PageAgent({customTools})
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '最佳实践' : 'Best Practices'}</h2>
|
<Heading id="best-practices" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '最佳实践' : 'Best Practices'}
|
||||||
|
</Heading>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
<div 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">
|
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function DataMasking() {
|
export default function DataMasking() {
|
||||||
@@ -15,7 +16,9 @@ export default function DataMasking() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6">{isZh ? '接口定义' : 'API Definition'}</h2>
|
<Heading id="api-definition" className="text-3xl font-bold mb-6">
|
||||||
|
{isZh ? '接口定义' : 'API Definition'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
@@ -30,9 +33,9 @@ export default function DataMasking() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
<h2 className="text-3xl font-bold mb-6">
|
<Heading id="common-masking-patterns" className="text-3xl font-bold mb-6">
|
||||||
{isZh ? '常用脱敏规则' : 'Common Masking Patterns'}
|
{isZh ? '常用脱敏规则' : 'Common Masking Patterns'}
|
||||||
</h2>
|
</Heading>
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||||
{isZh
|
{isZh
|
||||||
|
|||||||
@@ -1,34 +1,57 @@
|
|||||||
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
// Recommended models: lightweight with excellent tool call capabilities
|
const BASELINE = new Set([
|
||||||
const MODELS = {
|
'gpt-5.1',
|
||||||
recommended: [
|
'claude-haiku-4.5',
|
||||||
'gpt-4.1-mini',
|
'gemini-3-flash',
|
||||||
'claude-haiku-4.5',
|
'deepseek-3.2',
|
||||||
'gemini-3-flash',
|
'qwen3.5-plus',
|
||||||
'deepseek-3.2',
|
])
|
||||||
'gpt-5.2',
|
|
||||||
|
// Models grouped by brand, newest first
|
||||||
|
const MODEL_GROUPS: Record<string, string[]> = {
|
||||||
|
OpenAI: ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
||||||
|
Qwen: [
|
||||||
|
'qwen3.5-plus',
|
||||||
|
'qwen3.5-flash',
|
||||||
|
'qwen3-coder-next',
|
||||||
'qwen-3-max',
|
'qwen-3-max',
|
||||||
],
|
|
||||||
verified: [
|
|
||||||
'qwen-3-plus',
|
'qwen-3-plus',
|
||||||
'gpt-4.1',
|
'qwen3:14b (ollama)',
|
||||||
'gpt-5',
|
|
||||||
'gpt-5-mini',
|
|
||||||
'grok-4',
|
|
||||||
'grok-code-fast',
|
|
||||||
'claude-sonnet-3.5',
|
|
||||||
'claude-sonnet-4.5',
|
|
||||||
'claude-opus-4.5',
|
|
||||||
'gemini-2.5',
|
|
||||||
'gemini-3-pro',
|
|
||||||
],
|
],
|
||||||
|
DeepSeek: ['deepseek-3.2'],
|
||||||
|
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
|
||||||
|
Anthropic: [
|
||||||
|
'claude-opus-4.6',
|
||||||
|
'claude-opus-4.5',
|
||||||
|
'claude-sonnet-4.5',
|
||||||
|
'claude-haiku-4.5',
|
||||||
|
'claude-sonnet-3.5',
|
||||||
|
],
|
||||||
|
xAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'],
|
||||||
|
MoonshotAI: ['kimi-k2.5'],
|
||||||
|
'Z.AI': ['glm-5', 'glm-4.7'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModelBadge = ({ model, baseline }: { model: string; baseline?: boolean }) => (
|
||||||
|
<div
|
||||||
|
className={`px-3 py-1.5 rounded-md text-xs font-medium font-mono transition-colors ${
|
||||||
|
baseline
|
||||||
|
? 'bg-emerald-500 text-white shadow-sm'
|
||||||
|
: 'bg-white/80 dark:bg-gray-800/80 text-gray-800 dark:text-gray-200 border border-gray-300 dark:border-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{model}
|
||||||
|
{baseline && <span className="ml-1">⭐</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
export default function Models() {
|
export default function Models() {
|
||||||
const { isZh } = useLanguage()
|
const { isZh } = useLanguage()
|
||||||
const allModels = [...MODELS.recommended, ...MODELS.verified]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl">
|
<div className="max-w-4xl">
|
||||||
@@ -41,32 +64,29 @@ export default function Models() {
|
|||||||
|
|
||||||
{/* Models Section */}
|
{/* Models Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-3">{isZh ? '已测试模型' : 'Tested Models'}</h2>
|
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
||||||
|
{isZh ? '已测试模型' : 'Tested Models'}
|
||||||
|
</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '推荐使用 ToolCall 能力强的轻量级模型。'
|
? '推荐使用 ToolCall 能力强的轻量级模型。'
|
||||||
: 'Recommended: Lightweight models with strong ToolCall capabilities.'}
|
: 'Recommended: Fast, lightweight models with strong ToolCall capabilities.'}
|
||||||
</p>
|
</p>
|
||||||
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
|
||||||
{allModels.map((model) => {
|
{Object.entries(MODEL_GROUPS).map(([brand, models]) => (
|
||||||
const isRecommended = MODELS.recommended.includes(model)
|
<Fragment key={brand}>
|
||||||
return (
|
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 pt-2">
|
||||||
<div
|
{brand}
|
||||||
key={model}
|
</span>
|
||||||
className={`px-3 py-1.5 rounded-md text-sm font-medium font-mono transition-colors ${
|
<div className="flex flex-wrap gap-2">
|
||||||
isRecommended
|
{models.map((model) => (
|
||||||
? 'bg-emerald-500 text-white shadow-sm'
|
<ModelBadge key={model} model={model} baseline={BASELINE.has(model)} />
|
||||||
: 'bg-white/80 dark:bg-gray-800/80 text-gray-800 dark:text-gray-200 border border-gray-300 dark:border-gray-600'
|
))}
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{model}
|
|
||||||
{isRecommended && <span className="ml-1">⭐</span>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</Fragment>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-5">⭐ baseline models</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -124,13 +144,13 @@ export default function Models() {
|
|||||||
|
|
||||||
{/* Configuration Section */}
|
{/* Configuration Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '配置方式' : 'Configuration'}</h2>
|
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
|
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
|
||||||
const pageAgent = new PageAgent({
|
const pageAgent = new PageAgent({
|
||||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'qwen-plus'
|
model: 'qwen3.5-plus'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Self-hosted models (e.g., Ollama)
|
// Self-hosted models (e.g., Ollama)
|
||||||
@@ -140,19 +160,63 @@ const pageAgent = new PageAgent({
|
|||||||
model: 'qwen3:14b'
|
model: 'qwen3:14b'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Free testing endpoint
|
|
||||||
// Note: Rate-limited, content-filtered, subject to change. Replace with your own.
|
|
||||||
// Note: Uses official DeepSeek-chat (3.2). See DeepSeek website for terms & privacy.
|
|
||||||
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
|
||||||
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
|
||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Free Testing API Section */}
|
||||||
|
<section className="mb-10">
|
||||||
|
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估使用。有速率限制,可能随时变更。请勿用于生产环境。'
|
||||||
|
: 'The following free testing endpoints are provided for technical evaluation of PageAgent.js and PageAgent Extension only. Rate-limited, subject to change. Not for production use.'}
|
||||||
|
</p>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
Qwen (Alibaba Cloud China)
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '通过阿里云函数计算(中国大陆)转发至百炼 Qwen 模型'
|
||||||
|
: 'Proxied via Alibaba Cloud FC (Mainland China) to BaiLian Qwen models'}
|
||||||
|
{' · '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{isZh ? '使用条款' : 'Terms of Use'}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`# qwen3.5-plus (default for demos) or qwen3.5-flash (lighter)
|
||||||
|
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
|
||||||
|
LLM_MODEL_NAME="qwen3.5-plus"
|
||||||
|
LLM_API_KEY="NA"`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{isZh
|
||||||
|
? '⚠️ 仅供技术评估和研发用途,禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'
|
||||||
|
: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
{isZh ? '使用条款' : 'Terms of Use'}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Ollama Section */}
|
{/* Ollama Section */}
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">Ollama</h2>
|
<Heading id="ollama">Ollama</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { Route, Switch } from 'wouter'
|
import { Route, Switch } from 'wouter'
|
||||||
|
|
||||||
import Header from '../../components/Header'
|
|
||||||
import DocsLayout from './Layout'
|
import DocsLayout from './Layout'
|
||||||
import CustomUIDocs from './advanced/custom-ui/page'
|
import CustomUIDocs from './advanced/custom-ui/page'
|
||||||
import PageAgentCoreDocs from './advanced/page-agent-core/page'
|
import PageAgentCoreDocs from './advanced/page-agent-core/page'
|
||||||
@@ -20,15 +19,13 @@ import Limitations from './introduction/limitations/page'
|
|||||||
// Introduction
|
// Introduction
|
||||||
import Overview from './introduction/overview/page'
|
import Overview from './introduction/overview/page'
|
||||||
import QuickStart from './introduction/quick-start/page'
|
import QuickStart from './introduction/quick-start/page'
|
||||||
|
import Troubleshooting from './introduction/troubleshooting/page'
|
||||||
|
|
||||||
function DocsPage({ children }: { children: React.ReactNode }) {
|
function DocsPage({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white dark:bg-gray-900">
|
<DocsLayout>
|
||||||
<Header />
|
<Suspense>{children}</Suspense>
|
||||||
<DocsLayout>
|
</DocsLayout>
|
||||||
<Suspense>{children}</Suspense>
|
|
||||||
</DocsLayout>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +48,11 @@ export default function DocsRouter() {
|
|||||||
<Limitations />
|
<Limitations />
|
||||||
</DocsPage>
|
</DocsPage>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/introduction/troubleshooting">
|
||||||
|
<DocsPage>
|
||||||
|
<Troubleshooting />
|
||||||
|
</DocsPage>
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
<Route path="/features/custom-tools">
|
<Route path="/features/custom-tools">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import BetaNotice from '@/components/BetaNotice'
|
import BetaNotice from '@/components/BetaNotice'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function SecurityPermissions() {
|
export default function SecurityPermissions() {
|
||||||
@@ -18,9 +19,9 @@ export default function SecurityPermissions() {
|
|||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
<Heading id="element-interaction-allowlist-blocklist" className="text-2xl font-bold mb-3">
|
||||||
{isZh ? '元素操作黑白名单' : 'Element Interaction Allowlist/Blocklist'}
|
{isZh ? '元素操作黑白名单' : 'Element Interaction Allowlist/Blocklist'}
|
||||||
</h2>
|
</Heading>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
||||||
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
|
<h3 className="text-lg font-semibold text-red-900 dark:text-red-300">
|
||||||
@@ -46,9 +47,9 @@ export default function SecurityPermissions() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
<Heading id="instruction-safety-constraints" className="text-2xl font-bold mb-3">
|
||||||
{isZh ? 'Instruction 安全约束' : 'Instruction Safety Constraints'}
|
{isZh ? 'Instruction 安全约束' : 'Instruction Safety Constraints'}
|
||||||
</h2>
|
</Heading>
|
||||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
||||||
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300">
|
||||||
⚠️ {isZh ? '高危操作控制' : 'High-Risk Operation Control'}
|
⚠️ {isZh ? '高危操作控制' : 'High-Risk Operation Control'}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function ThirdPartyAgentPage() {
|
export default function ThirdPartyAgentPage() {
|
||||||
@@ -26,7 +27,9 @@ export default function ThirdPartyAgentPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '集成方式' : 'Integration Method'}</h2>
|
<Heading id="integration-method" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '集成方式' : 'Integration Method'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
@@ -57,7 +60,9 @@ const pageAgentTool = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '应用场景' : 'Use Cases'}</h2>
|
<Heading id="use-cases" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '应用场景' : 'Use Cases'}
|
||||||
|
</Heading>
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||||
<div className="bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
<div className="bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||||
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
|
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function LimitationsPage() {
|
export default function LimitationsPage() {
|
||||||
@@ -11,184 +14,146 @@ export default function LimitationsPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-gray-600 dark:text-gray-300">
|
<p className="text-xl text-gray-600 dark:text-gray-300">
|
||||||
{isZh
|
{isZh
|
||||||
? '了解 page-agent 当前的功能边界和技术限制'
|
? 'Page Agent 基于 DOM 理解网页并执行操作。这决定了它的能力边界。'
|
||||||
: "Understand page-agent's current capabilities and technical constraints"}
|
: 'Page Agent understands web pages via DOM and performs actions accordingly. This defines its capability boundary.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
<div className="prose prose-lg dark:prose-invert max-w-none">
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
{/* PageAgent.js vs PageAgentExt */}
|
||||||
{isZh ? '页面支持限制' : 'Page Support Limitations'}
|
<Heading id="pageagent-js-vs-pageagentext" className="text-2xl font-bold mb-3">
|
||||||
</h2>
|
{isZh ? 'PageAgent.js vs PageAgentExt' : 'PageAgent.js vs PageAgentExt'}
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
|
</Heading>
|
||||||
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh ? '单页应用限制' : 'Single Page Application Limits'}
|
{isZh
|
||||||
</h3>
|
? 'PageAgent.js 是核心库,运行在页面内。PageAgentExt 是可选的浏览器扩展,提供额外的浏览器级控制能力。'
|
||||||
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
|
: 'PageAgent.js is the core library running inside a page. PageAgentExt is an optional browser extension that adds browser-level control.'}
|
||||||
<li>
|
</p>
|
||||||
{isZh
|
<div className="overflow-x-auto mb-6">
|
||||||
? '• 仅支持单页应用(SPA):目前只能在单个页面内进行操作'
|
<table className="w-full text-sm border-collapse">
|
||||||
: '• SPA only: Currently operates within a single page'}
|
<thead>
|
||||||
</li>
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<li>
|
<th className="text-left py-3 pr-4"></th>
|
||||||
{isZh
|
<th className="text-left py-3 px-4 font-semibold">PageAgent.js</th>
|
||||||
? '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务'
|
<th className="text-left py-3 pl-4 font-semibold">
|
||||||
: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet'}
|
PageAgentExt{' '}
|
||||||
</li>
|
<Link
|
||||||
<li>
|
href="/features/chrome-extension"
|
||||||
{isZh
|
className="text-xs font-normal text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
? '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent'
|
>
|
||||||
: '• Requires integration: Cannot operate on sites without page-agent'}
|
{isZh ? '了解更多' : 'learn more'}
|
||||||
</li>
|
</Link>
|
||||||
</ul>
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="text-gray-600 dark:text-gray-300">
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||||
|
{isZh ? '接入方式' : 'Integration'}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{isZh ? '网站开发者主动集成' : 'Site developer integrates the library'}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 pl-4">
|
||||||
|
{isZh ? '用户安装浏览器扩展' : 'User installs a browser extension'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-gray-100 dark:border-gray-800">
|
||||||
|
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||||
|
{isZh ? '可操作范围' : 'Scope'}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
{isZh ? '当前页面(为 SPA 设计)' : 'Current page (designed for SPAs)'}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 pl-4">
|
||||||
|
{isZh ? '任意网页、多标签页' : 'Any web page, multi-tab'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
|
||||||
|
{isZh ? '额外能力' : 'Extra capabilities'}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">—</td>
|
||||||
|
<td className="py-3 pl-4">
|
||||||
|
{isZh ? '新建/切换/关闭标签页' : 'Open / switch / close tabs'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
{/* Interaction Limitations */}
|
||||||
{isZh ? '交互行为限制' : 'Interaction Limitations'}
|
<Heading id="interaction-capabilities" className="text-2xl font-bold mb-3">
|
||||||
</h2>
|
{isZh ? '交互能力' : 'Interaction Capabilities'}
|
||||||
|
</Heading>
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
|
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
|
||||||
<h3 className="font-semibold mb-4">{isZh ? '支持的操作' : 'Supported Operations'}</h3>
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '点击操作' : 'Click'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '文本输入' : 'Text input'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '页面滚动' : 'Scroll'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '表单提交' : 'Form submit'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '选择操作' : 'Select'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-green-600 dark:text-green-400">
|
|
||||||
<span className="mr-2">✅</span>
|
|
||||||
<span>{isZh ? '焦点切换' : 'Focus'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="font-semibold mb-4">{isZh ? '不支持的操作' : 'Unsupported Operations'}</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '鼠标悬停(hover)' : 'Mouse hover'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '拖拽操作' : 'Drag & drop'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '右键菜单' : 'Right-click menu'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '图形绘制' : 'Drawing'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '键盘快捷键' : 'Keyboard shortcuts'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-red-600 dark:text-red-400">
|
|
||||||
<span className="mr-2">❌</span>
|
|
||||||
<span>{isZh ? '基于点击区域或鼠标位置的控制' : 'Position-based control'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
|
||||||
{isZh ? '网页理解限制' : 'Understanding Limitations'}
|
|
||||||
</h2>
|
|
||||||
<div className="bg-red-50 dark:bg-red-900/20 border-l-4 border-red-400 p-4 mb-6">
|
|
||||||
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
|
|
||||||
{isZh ? '无视觉能力' : 'No Visual Recognition'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-red-700 dark:text-red-300 mb-3">
|
|
||||||
{isZh
|
|
||||||
? 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:'
|
|
||||||
: 'page-agent operates based on DOM structure with no visual recognition. Cannot understand:'}
|
|
||||||
</p>
|
|
||||||
<ul className="text-red-700 dark:text-red-300 space-y-1">
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '• 图片内容:无法识别图片中的文字、图标或视觉元素'
|
|
||||||
: '• Image content: Cannot recognize text, icons, or visual elements in images'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容'
|
|
||||||
: '• Canvas: Cannot understand graphics drawn on Canvas'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '• WebGL 3D 内容:无法操作 3D 场景中的元素'
|
|
||||||
: '• WebGL 3D: Cannot operate elements in 3D scenes'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '• SVG 图形:无法理解 SVG 中的视觉内容和路径'
|
|
||||||
: '• SVG graphics: Cannot understand visual content and paths in SVG'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">
|
|
||||||
{isZh ? '被操作网站要求' : 'Website Requirements'}
|
|
||||||
</h2>
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold mb-2">
|
<h3 className="font-semibold mb-3 text-green-700 dark:text-green-400">
|
||||||
{isZh ? '语义化和易用性' : 'Semantic & Usability'}
|
{isZh ? '支持' : 'Supported'}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
<ul className="space-y-1.5 text-sm">
|
||||||
{isZh
|
{[
|
||||||
? '所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。'
|
isZh ? '点击、文本输入、选择' : 'Click, text input, select',
|
||||||
: 'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.'}
|
isZh ? '页面滚动(垂直 / 水平)' : 'Scroll (vertical / horizontal)',
|
||||||
</p>
|
isZh ? '表单提交、焦点切换' : 'Form submit, focus',
|
||||||
|
isZh ? '执行 JavaScript(可选)' : 'Execute JavaScript (opt-in)',
|
||||||
|
].map((text) => (
|
||||||
|
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="mr-2 text-green-600 dark:text-green-400">✓</span>
|
||||||
|
{text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold mb-2">{isZh ? 'UI/UX' : 'UI/UX'}</h3>
|
<h3 className="font-semibold mb-3 text-red-700 dark:text-red-400">
|
||||||
<p className="text-gray-600 dark:text-gray-300">
|
{isZh ? '不支持' : 'Not supported'}
|
||||||
{isZh
|
</h3>
|
||||||
? '反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。'
|
<ul className="space-y-1.5 text-sm">
|
||||||
: 'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.'}
|
{[
|
||||||
</p>
|
isZh ? '悬停、拖拽、右键菜单' : 'Hover, drag & drop, right-click',
|
||||||
</div>
|
isZh ? '键盘快捷键' : 'Keyboard shortcuts',
|
||||||
<div>
|
isZh ? '坐标定位操作' : 'Position-based control',
|
||||||
<h3 className="font-semibold mb-2">{isZh ? '环境要求' : 'Environment'}</h3>
|
isZh ? '绘图操作' : 'Drawing',
|
||||||
<p className="text-gray-600 dark:text-gray-300">modern browser</p>
|
].map((text) => (
|
||||||
|
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="mr-2 text-red-600 dark:text-red-400">✗</span>
|
||||||
|
{text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{isZh ? '未来规划' : 'Future Plans'}</h2>
|
{/* Understanding Limitations */}
|
||||||
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
|
<Heading id="understanding" className="text-2xl font-bold mb-3">
|
||||||
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
|
{isZh ? '理解能力' : 'Understanding'}
|
||||||
{isZh ? '即将支持' : 'Coming Soon'}
|
</Heading>
|
||||||
</h3>
|
<div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-400 p-4 mb-6">
|
||||||
<ul className="text-green-700 dark:text-green-300 space-y-1">
|
<p className="text-amber-800 dark:text-amber-200 mb-2 font-medium">
|
||||||
<li>{isZh ? '• 多页面接力操作能力' : '• Multi-page relay capabilities'}</li>
|
{isZh
|
||||||
<li>{isZh ? '• 更丰富的鼠标交互支持' : '• Richer mouse interaction support'}</li>
|
? 'Page Agent 不使用多模态模型,不截图,没有视觉能力。仅通过 DOM 结构理解页面。'
|
||||||
<li>{isZh ? '• 基础的视觉理解能力' : '• Basic visual understanding'}</li>
|
: 'Page Agent does not use multimodal models, does not take screenshots, and has no visual capability. It reads pages through DOM structure only.'}
|
||||||
<li>{isZh ? '• 更智能的错误恢复机制' : '• Smarter error recovery'}</li>
|
</p>
|
||||||
</ul>
|
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
||||||
|
{isZh
|
||||||
|
? '图片、Canvas、WebGL、SVG 等视觉内容无法被识别。页面的语义化程度和可访问性直接影响 AI 的理解准确性。'
|
||||||
|
: 'Images, Canvas, WebGL, SVG and other visual content cannot be recognized. Page semantic quality and accessibility directly affect AI accuracy.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Website Quality */}
|
||||||
|
<Heading id="page-quality-matters" className="text-2xl font-bold mb-3">
|
||||||
|
{isZh ? '网页质量影响' : 'Page Quality Matters'}
|
||||||
|
</Heading>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
|
{isZh
|
||||||
|
? '反常识的交互逻辑、纯视觉的操作提示、快速出现消失的元素等都会降低自动化成功率。语义化的 HTML 和良好的可访问性会显著提升效果。'
|
||||||
|
: 'Counter-intuitive interactions, visual-only cues, and rapidly appearing/disappearing elements reduce automation success. Semantic HTML and good accessibility significantly improve results.'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
export default function Overview() {
|
export default function Overview() {
|
||||||
@@ -48,9 +49,9 @@ export default function Overview() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<Heading id="what-is-page-agent" className="text-2xl font-bold mb-4">
|
||||||
{isZh ? '什么是 page-agent?' : 'What is page-agent?'}
|
{isZh ? '什么是 page-agent?' : 'What is page-agent?'}
|
||||||
</h2>
|
</Heading>
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed ">
|
<p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed ">
|
||||||
{isZh
|
{isZh
|
||||||
@@ -60,7 +61,9 @@ export default function Overview() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-2xl font-bold mb-3">{isZh ? '核心特性' : 'Core Features'}</h2>
|
<Heading id="core-features" className="text-2xl font-bold mb-3">
|
||||||
|
{isZh ? '核心特性' : 'Core Features'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-8" role="list">
|
<div className="grid md:grid-cols-2 gap-4 mb-8" role="list">
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
@@ -108,9 +111,9 @@ export default function Overview() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-4">
|
<Heading id="vs-browser-use" className="text-2xl font-bold mb-4">
|
||||||
{isZh ? '与 browser-use 的区别' : 'vs. browser-use'}
|
{isZh ? '与 browser-use 的区别' : 'vs. browser-use'}
|
||||||
</h2>
|
</Heading>
|
||||||
|
|
||||||
<div className="overflow-x-auto mb-8">
|
<div className="overflow-x-auto mb-8">
|
||||||
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
|
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
|
||||||
@@ -174,7 +177,9 @@ export default function Overview() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-4">{isZh ? '应用场景' : 'Use Cases'}</h2>
|
<Heading id="use-cases" className="text-2xl font-bold mb-4">
|
||||||
|
{isZh ? '应用场景' : 'Use Cases'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<ul className="space-y-4 mb-8">
|
<ul className="space-y-4 mb-8">
|
||||||
<li className="flex items-start space-x-3">
|
<li className="flex items-start space-x-3">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
|
import { Heading } from '@/components/Heading'
|
||||||
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
|
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ export default function QuickStart() {
|
|||||||
{isZh ? '几分钟内完成 page-agent 的集成。' : 'Integrate page-agent in minutes.'}
|
{isZh ? '几分钟内完成 page-agent 的集成。' : 'Integrate page-agent in minutes.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 className="text-2xl font-bold mb-3">{isZh ? '安装步骤' : 'Installation Steps'}</h2>
|
<Heading id="installation-steps" className="text-2xl font-bold mb-3">
|
||||||
|
{isZh ? '安装步骤' : 'Installation Steps'}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
{/* Demo CDN - One Line */}
|
{/* Demo CDN - One Line */}
|
||||||
@@ -23,7 +26,32 @@ export default function QuickStart() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-2 rounded mb-3 text-sm">
|
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-2 rounded mb-3 text-sm">
|
||||||
<span className="text-yellow-800 dark:text-yellow-200">
|
<span className="text-yellow-800 dark:text-yellow-200">
|
||||||
⚠️ {isZh ? '仅用于技术评估' : 'For evaluation only'}
|
⚠️{' '}
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
该 Demo CDN 使用了免费的测试 LLM API,使用即表示您同意其
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
使用条款
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
This demo CDN uses our free testing LLM API. By using it you agree to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
Terms of Use
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
@@ -71,8 +99,8 @@ import { PageAgent } from 'page-agent'`}
|
|||||||
</h3>
|
</h3>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`const agent = new PageAgent({
|
code={`const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'qwen3.5-plus',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: '${isZh ? 'zh-CN' : 'en-US'}'
|
language: '${isZh ? 'zh-CN' : 'en-US'}'
|
||||||
})`}
|
})`}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user