Compare commits

...

65 Commits

Author SHA1 Message Date
Simon 6abe75f199 docs(ext): clarify data handling and privacy in PRIVACY.md 2026-02-14 17:03:41 +08:00
Simon e27ebda960 chore(ext): docs and locales 2026-02-14 16:51:37 +08:00
Simon 9b603d90ba docs: update limitations page 2026-02-14 16:23:34 +08:00
Simon b89228693d feat(ext): add lang setting 2026-02-14 16:10:46 +08:00
Simon 66cc9e0a3c style: pretty up; fix extension shadcn 2026-02-14 15:12:45 +08:00
Simon 53144a8021 chore(ext): rm zh doc 2026-02-14 15:12:03 +08:00
Simon 43b7c1b136 style(ext): make over 2026-02-13 19:09:46 +08:00
Simon 3e7851849f chore(version): bump version to 1.3.0 2026-02-13 18:50:32 +08:00
Simon dffcb53db9 feat!: Refine lifecycle hooks; fix abortSignal
- add `stop` method. agent can be reused after stopped
- agent can not be reused after disposed
- extension DO NOT exposes `dispose` anymore. only `stop`.
- update panel for new `stop` method
- fix MultiPageAgent dispose event
- better handling abortSignal
2026-02-13 17:57:12 +08:00
Simon 4dc332a32c docs: update changelog 2026-02-12 17:23:35 +08:00
Simon f19b3cc2cc docs: update extension related docs 2026-02-12 17:19:14 +08:00
Simon 11d66f42c4 chore(ext): version 0.1.5 2026-02-11 20:12:57 +08:00
Simon 20e947a1c1 chore(version): bump version to 1.2.0 2026-02-11 20:09:23 +08:00
Simon 7c87c90258 fix(ext): fix multi-thread logic; extensive logging and error handling 2026-02-11 19:51:19 +08:00
Simon fcb9ec4e57 chore: clean up 2026-02-11 19:27:14 +08:00
Simon 1fcf931685 feat(core): add a observe phase in a step; improve abortSignal 2026-02-10 22:20:42 +08:00
Simon c71ffcec1c refactor(ext): tidy up background code 2026-02-10 21:59:58 +08:00
Simon 430be466fc feat(ext): add chat history 2026-02-10 18:43:54 +08:00
Simon 3f284cf2e6 chore(controller): remove link hint 2026-02-10 17:19:02 +08:00
Simon 6f0655478a fix(core): remove trimLines to fix indentation lost 2026-02-10 16:30:03 +08:00
Simon 7d5449967b feat(core): improve observation logic; mark states as private 2026-02-10 16:28:25 +08:00
Simon fd47888d1b chore(core): tidy up PageAgentCore members 2026-02-10 15:45:04 +08:00
Simon 10f470ce7d chore: improve package.json 2026-02-10 15:28:50 +08:00
Simon cb29db1cc4 Merge pull request #138 from alibaba/dependabot/npm_and_yarn/production-dependencies-58bd30f460
chore(deps): bump the production-dependencies group with 2 updates
2026-02-10 14:21:11 +08:00
Simon 8c08f639dd Merge pull request #139 from alibaba/dependabot/npm_and_yarn/development-dependencies-dee6027aab
chore(deps-dev): bump the development-dependencies group with 8 updates
2026-02-10 14:18:18 +08:00
dependabot[bot] 488f080e68 chore(deps-dev): bump the development-dependencies group with 8 updates
Bumps the development-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.56.0` | `7.56.3` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.0` | `25.2.2` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.2.3` | `17.2.4` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.9.3` | `2.12.2` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.9.3` | `2.12.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.54.0` | `8.55.0` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.10` | `19.2.13` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.13` | `0.20.14` |


Updates `@microsoft/api-extractor` from 7.56.0 to 7.56.3
- [Changelog](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md)
- [Commits](https://github.com/microsoft/rushstack/commits/@microsoft/api-extractor_v7.56.3/apps/api-extractor)

Updates `@types/node` from 25.2.0 to 25.2.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `dotenv` from 17.2.3 to 17.2.4
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/commits)

Updates `eslint-plugin-react-dom` from 2.9.3 to 2.12.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.12.2/packages/plugins/eslint-plugin-react-dom)

Updates `eslint-plugin-react-x` from 2.9.3 to 2.12.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v2.12.2/packages/plugins/eslint-plugin-react-x)

Updates `typescript-eslint` from 8.54.0 to 8.55.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.55.0/packages/typescript-eslint)

Updates `@types/react` from 19.2.10 to 19.2.13
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `wxt` from 0.20.13 to 0.20.14
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.56.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: dotenv
  dependency-version: 17.2.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.12.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.12.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.55.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 22:25:35 +00:00
dependabot[bot] 6e07af422b chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.30.0 to 12.34.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.30.0...v12.34.0)

Updates `simple-icons` from 16.7.0 to 16.8.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.7.0...16.8.0)

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 22:23:28 +00:00
Simon e9d721f6b9 chore(ext): bump extension version to 0.1.4 2026-02-09 21:12:11 +08:00
Simon 840162df8c feat: change observation timing 2026-02-09 21:05:29 +08:00
Simon 1517bc4868 feat: prune prompts 2026-02-09 21:04:39 +08:00
Simon 67f6a56a87 chore(version): bump version to 1.1.2 2026-02-09 17:50:34 +08:00
Simon 6f0ff1fd33 refactor: zod tree-shaking; better error handling in agent steps 2026-02-09 17:49:10 +08:00
Simon 6fdb8d03f2 docs: trouble shooting 2026-02-09 16:32:58 +08:00
Simon d00a8dcc21 feat: improve prompt; update model list 2026-02-09 15:50:20 +08:00
Simon 851e9ef158 docs: model list 2026-02-06 18:49:06 +08:00
Simon 8ea8ce9acd fix(llms): gpt-5-mini temperature 2026-02-06 17:19:06 +08:00
Simon 9bd4a47d35 feat(ext): add includeInitialTab option; change main world API 2026-02-04 19:22:06 +08:00
Simon 71ca554108 feat(ext): use PAGE_AGENT_EXT namespace; add viber instructions 2026-02-03 19:09:37 +08:00
Simon 648a0c1bda feat(ext): expose ext meta data to main world 2026-02-03 16:39:24 +08:00
Simon 1a0422f99d Merge pull request #135 from alibaba/dependabot/npm_and_yarn/development-dependencies-ad640cb911
chore(deps-dev): bump eslint-plugin-react-refresh from 0.4.26 to 0.5.0 in the development-dependencies group across 1 directory
2026-02-03 14:20:04 +08:00
dependabot[bot] 2f72c677a0 chore(deps-dev): bump eslint-plugin-react-refresh
Bumps the development-dependencies group with 1 update in the / directory: [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh).


Updates `eslint-plugin-react-refresh` from 0.4.26 to 0.5.0
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.4.26...v0.5.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 06:00:00 +00:00
Simon 18019d390a Merge pull request #132 from alibaba/dependabot/npm_and_yarn/production-dependencies-8604a495b2
chore(deps): bump the production-dependencies group with 2 updates
2026-02-03 13:48:12 +08:00
Simon bb3605b3df chore: clean up dep 2026-02-03 13:39:47 +08:00
Simon 2b3bb8ab9e chore(ext): rm React.FormEvent 2026-02-03 13:18:03 +08:00
dependabot[bot] 7a09bc559d chore(deps): bump the production-dependencies group with 2 updates
Bumps the production-dependencies group with 2 updates: [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `motion` from 12.29.2 to 12.30.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.29.2...v12.30.0)

Updates `simple-icons` from 16.6.1 to 16.7.0
- [Release notes](https://github.com/simple-icons/simple-icons/releases)
- [Commits](https://github.com/simple-icons/simple-icons/compare/16.6.1...16.7.0)

---
updated-dependencies:
- dependency-name: motion
  dependency-version: 12.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 01:15:58 +00:00
Simon a834d37f28 fix(ext): inject versions 2026-02-02 21:11:23 +08:00
Simon 0c3be817c9 style(ext): attribution; dark mode 2026-02-02 21:07:31 +08:00
Simon b8999fdb7c style(ext): improve config panel 2026-02-02 19:51:48 +08:00
Simon 9ba95e96b6 feat(ext): detect user lang 2026-02-02 19:29:30 +08:00
Simon 4459026297 feat: set default lang to english; fix ext lang setting 2026-02-02 19:17:15 +08:00
Simon 98d3016395 style(ext): remove raw section from ResultCard 2026-02-02 19:16:07 +08:00
Simon 15ea488719 fix(ext): save config to storage 2026-02-02 18:48:16 +08:00
Simon 5b2a11c0b2 chore(version): bump version to 1.1.0 2026-02-02 17:28:40 +08:00
Simon c5c6340eb6 chore: ext version 2026-02-02 17:27:58 +08:00
Simon 74aea50875 feat: rename include_attributes to includeAttributes 2026-02-02 17:24:08 +08:00
Simon f86bb03745 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-02-02 16:53:45 +08:00
Simon f6b4447ef7 docs: tips about Ollama 2026-02-02 16:51:51 +08:00
Simon d8ec4cf1ab feat: better action key in prompt example 2026-02-02 13:39:04 +08:00
Simon da440f63c3 chore(version): bump version to 1.0.1 2026-01-30 16:59:11 +08:00
Simon 622fbff790 docs: simplify layout; rm useless CDN-setup 2026-01-30 16:54:31 +08:00
Simon 68c944c3a4 docs: remove i18n dep 2026-01-30 16:47:23 +08:00
Simon 6cd3e3726c docs: typography 2026-01-30 16:34:27 +08:00
Simon d11a2593b3 docs: pretty chrome extension entry 2026-01-30 15:19:54 +08:00
Simon 5e936c35d7 docs: simplify i18n 2026-01-30 15:13:31 +08:00
Simon 97cdb1e1cd docs: Add Custom UI doc 2026-01-30 14:58:33 +08:00
102 changed files with 4453 additions and 3125 deletions
+3 -2
View File
@@ -18,7 +18,7 @@
"wouter"
],
"files.exclude": {
"packages/*/node_modules": true,
"packages/*/node_modules": true
},
"markdownlint.config": {
// "comment": "Relaxed rules",
@@ -32,6 +32,7 @@
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false
"ol-prefix": false,
"no-duplicate-heading": false
}
}
+2 -1
View File
@@ -22,6 +22,7 @@ npm start # Start website dev server
npm run build # Build all packages
npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules
npm run zip -w @page-agent/ext # Zip the extension package
```
## Architecture
@@ -36,7 +37,7 @@ packages/
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms
├── extension/ # 🚧 WIP: Browser extension (WXT + React)
├── extension/ # Browser extension (WXT + React)
├── page-controller/ # @page-agent/page-controller
└── ui/ # @page-agent/ui
```
+59
View File
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.3.0] - 2026-02-13
### Breaking Changes
- **Lifecycle: `stop()` vs `dispose()`** - New `stop()` method to cancel the current task while keeping the agent reusable. `dispose()` is now terminal — a disposed agent cannot be reused. This affects both `PageAgentCore` and `PanelAgentAdapter`.
### Features
- **Panel action button** - The panel button now morphs between Stop (■) and Close (X) based on agent status
- **Error history** - Errors and max-step failures are now recorded in `history` as `AgentErrorEvent`, making post-task analysis more complete
### Bug Fixes
- **AbortError handling** - `AbortError` is no longer retried by the LLM client, and shows a clean "Task stopped" message instead of a raw error stack
---
## [1.2.0] - 2026-02-11
### Features
- **Observe Phase** - Agent now observes the page before each action, improving decision accuracy on dynamic pages
- **Better Abort Handling** - Improved `abortSignal` support for cleaner task cancellation
### Improvements
- Pruned system prompts for lower token usage and faster responses
- Improved error handling during agent steps with better error messages
- Zod tree-shaking for smaller bundle size
### Bug Fixes
- Fixed indentation lost in DOM extraction caused by `trimLines`
- Fixed `gpt-5-mini` temperature configuration
---
## [1.1.0] - 2026-02-02
### Features
- **Custom System Prompt** - New `systemPrompt` config option to customize or extend the default system prompt
- **Chrome Extension** - Extension with multi-tab control, main-world API with token auth, and tab lifecycle management
### Improvements
- Renamed `include_attributes` to `includeAttributes` in PageController config (camelCase consistency)
- Lazy-loaded mask module for faster initialization
- Better date formatting and error messages from LLM client
- Added `rawRequest` to step history for easier debugging
### Bug Fixes
- Fixed CSP errors by using local SVGs for cursor mask instead of inline styles
- Fixed `AbortError` being incorrectly retried and shown to users
- Fixed mask not working correctly when starting a new task after stopping a previous one
---
## [1.0.0] - 2026-01-19
### 🎉 First Stable Release
+22 -9
View File
@@ -20,10 +20,11 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
### Project Structure
This is a **monorepo** with npm workspaces containing **3 main packages**:
This is a **monorepo** with npm workspaces containing **4 main packages**:
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
@@ -126,6 +127,16 @@ If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add
LLM_BASE_URL=https://api.your-llm-provider.com/v1
```
- **Ollama example** (tested on 0.15 + qwen3:14b, RTX3090 24GB):
```env
LLM_BASE_URL="http://localhost:11434/v1"
LLM_API_KEY="NA"
LLM_MODEL_NAME="qwen3:14b"
```
> ⚠️ Add `*` to `OLLAMA_ORIGINS` (403). Models < 10B unlikely strong enough. Requires tool_call support. Set context length > 15k (default 4k will NOT work): `$env:OLLAMA_CONTEXT_LENGTH=64000; ollama serve`
- Restart the dev server to load new env vars
- If not provided, the demo will the free testing proxy by default
@@ -135,6 +146,16 @@ If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add
npm start
```
### Extension Development
```bash
npm run dev -w @page-agent/ext
npm run zip -w @page-agent/ext
```
- Load extension in Chrome via `chrome://extensions` -> **Load unpacked**
- Use `packages/extension/docs/extension_api.md` (EN) or `packages/extension/docs/extension_api_zh.md` (ZH) for API integration details
### Testing on Other Websites
- Start and serve a local `iife` script
@@ -183,14 +204,6 @@ By contributing to this project, you agree that your contributions will be licen
> You may need to sign a github CLA before you create a PR.
### Browser-Use Attribution
Parts of this project are derived from the [browser-use](https://github.com/browser-use/browser-use) project (MIT License). When contributing to DOM-related functionality:
- Maintain existing attribution comments
- Follow similar patterns for consistency
- Credit browser-use for derived concepts
## 💬 Questions?
- Open a GitHub issue for technical questions
+23 -28
View File
@@ -1,4 +1,4 @@
# PageAgent 🤖🪄
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
@@ -19,18 +19,16 @@
## ✨ Features
- **🎯 轻松集成**
- 无需 Python,无需无头浏览器,无需浏览器插件。纯页面内脚本
- **🔐 端侧运行**
- **🧠 HTML 脱水**
- **💬 自然语言接口**
- **🎨 HITL 交互界面**
以及 😉
- **🧪 实验性的 Chrome 扩展,支持跨页面控制** - `packages/extension`
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
- **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器`
- 纯页面内 JavaScript,一切都在你的网页中完成。
- The best tool for your agent to control web pages.
- **📖 基于文本的 DOM 操作**
- 无需截图,无需 OCR 或多模态模型。
- 无需特殊权限。
- **🧠 用你自己的 LLM**
- **🎨 精美 UI,支持人机协同**
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension),支持跨页面任务。**
## 🚀 快速开始
@@ -39,19 +37,15 @@
通过我们免费的 Demo LLM 快速体验 PageAgent
```html
<script
src="https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js"
crossorigin="true"
></script>
<script src="{URL}" crossorigin="true"></script>
```
> - **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,可能随时变更。
> - **🌷 建议使用自己的 LLM API。**
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js |
> **⚠️ 仅用于技术评估。** Demo LLM 有速率和使用限制,速度较慢,可能随时变更。
### NPM 安装
@@ -72,7 +66,7 @@ const agent = new PageAgent({
await agent.execute('点击登录按钮')
```
适用于无法使用 NPM 的环境,我们也提供了 IIFE 构建的 CDN 方式。[@see CDN Usage](https://alibaba.github.io/page-agent/#/docs/integration/cdn-setup)
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/#/docs/introduction/overview)
## 🏗️ 架构设计
@@ -80,12 +74,13 @@ PageAgent adopts a simplified monorepo structure:
```
packages/
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
├── page-agent/ # Exported agent and demo(npm: page-agent)
├── core/ # ** Core agent logic (npm: @page-agent/core) **
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
├── ui/ # 面板 & i18n (npm: @page-agent/ui)
── website/ # 文档站点
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
├── ui/ # 面板 UI (npm: @page-agent/ui)
── page-agent/ # 入口类 & iife 包 (npm: page-agent)
├── extension/ # Chrome 扩展,支持跨页面任务
└── website/ # 网站 & 文档站点
```
## 🤝 贡献
+24 -27
View File
@@ -1,4 +1,4 @@
# PageAgent 🤖🪄
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
@@ -19,18 +19,16 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
## ✨ Features
- **🎯 Easy Integration**
- No python. No headless browser. No browser extension. Just in-page scripts.
- **🔐 Client-Side Processing**
- **🧠 DOM Extraction**
- **💬 Natural Language Interface**
- **🎨 UI with Human in the loop**
And 😉
- **🧪 `cross-page` control with an experimental chrome extension** - `packages/extension`
👉 [**🗺️ Roadmap**](https://github.com/alibaba/page-agent/issues/96)
- **🎯 Easy integration**
- No need for `browser extension` / `python` / `headless browser`.
- Just in-page javascript. Everything happens in your web page.
- The best tool for your agent to control web pages.
- **📖 Text-based DOM manipulation**
- No screenshots. No OCR or multi-modal LLMs needed.
- No special permissions required.
- **🧠 Bring your own LLMs**
- **🎨 Pretty UI with human-in-the-loop**
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension) for multi-page tasks.**
## 🚀 Quick Start
@@ -39,19 +37,15 @@ And 😉
Fastest way to try PageAgent with our free Demo LLM:
```html
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js"
crossorigin="true"
></script>
<script src="{URL}" crossorigin="true"></script>
```
> - **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. May change without notice.
> - **🌷 Bring your own LLM API.**
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js |
> **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. Slow. May change without notice.
### NPM Installation
@@ -72,18 +66,21 @@ const agent = new PageAgent({
await agent.execute('Click the login button')
```
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/#/docs/introduction/overview).
## 🏗️ Structure
PageAgent adopts a simplified monorepo structure:
```
packages/
├── core/ # ** Core agent logic without UI(npm: @page-agent/core) **
├── page-agent/ # Exported agent and demo(npm: page-agent)
├── core/ # ** Core agent logic (npm: @page-agent/core) **
├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
├── ui/ # Panel & i18n (npm: @page-agent/ui)
── website/ # Demo & Documentation site
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
├── ui/ # Panel UI (npm: @page-agent/ui)
── page-agent/ # Entry class and iife builds(npm: page-agent)
├── extension/ # Chrome extension for multi-page tasks
└── website/ # Website & Documentation site
```
## 🤝 Contributing
+811 -1079
View File
File diff suppressed because it is too large Load Diff
+14 -11
View File
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"workspaces": [
"packages/page-controller",
@@ -21,8 +21,7 @@
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"scripts": {
"cleanup": "rm -rf packages/*/dist",
@@ -39,26 +38,30 @@
"@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2",
"@microsoft/api-extractor": "^7.56.3",
"@tailwindcss/vite": "^4.1.18",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.9",
"@types/node": "^25.2.2",
"@vitejs/plugin-react-swc": "^4.1.0",
"chalk": "^5.6.2",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"dotenv": "^17.2.4",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.7.2",
"eslint-plugin-react-dom": "^2.12.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.7.2",
"eslint-plugin-react-refresh": "^0.5.0",
"eslint-plugin-react-x": "^2.12.2",
"globals": "^17.0.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.4",
"prettier": "^3.8.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"typescript-eslint": "^8.55.0",
"unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2"
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-bundle-analyzer": "^1.3.6"
},
"overrides": {
"typescript": "^5.9.3"
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
@@ -45,7 +45,7 @@
"dependencies": {
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "1.0.0",
"@page-agent/page-controller": "1.0.0"
"@page-agent/llms": "1.3.0",
"@page-agent/page-controller": "1.3.0"
}
}
+167 -128
View File
@@ -3,15 +3,15 @@
* All rights reserved.
*/
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
import type { PageController } from '@page-agent/page-controller'
import type { BrowserState, PageController } from '@page-agent/page-controller'
import chalk from 'chalk'
import zod from 'zod'
import * as zod from 'zod'
import { type PageAgentConfig } from './config'
import { type PageAgentConfig, type SupportedLanguage } from './config'
import { DEFAULT_MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools'
import {
import type {
AgentActivity,
AgentReflection,
AgentStatus,
@@ -21,17 +21,26 @@ import {
MacroToolInput,
MacroToolResult,
} from './types'
import { normalizeResponse, trimLines, uid, waitFor } from './utils'
import { assert } from './utils/assert'
import { assert, normalizeResponse, uid, waitFor } from './utils'
export { type PageAgentConfig }
export type { SupportedLanguage }
export { tool, type PageAgentTool } from './tools'
export type * from './types'
/**
* AI agent for browser DOM automation.
* AI agent for browser automation.
*
* @remarks
* ## Re-act Agent Loop
* - step
* - observe (gather information about current environment and context)
* - think (LLM calling)
* - reflection (evaluate history, generate memory, short-term planning)
* - action (give the action to approach the next goal)
* - act (execute the action)
* - loop
*
* ## Event System
* - `statuschange` - Agent status transitions (idle → running → completed/error)
* - `historychange` - History events updated (persistent, part of agent memory)
@@ -50,14 +59,18 @@ export type * from './types'
* - Types: thinking, executing, executed, retrying, error
*/
export class PageAgentCore extends EventTarget {
config: PageAgentConfig & { maxSteps: number }
id = uid()
tools: typeof tools
readonly id = uid()
readonly config: PageAgentConfig & { maxSteps: number }
readonly tools: typeof tools
/** PageController for DOM operations */
readonly pageController: PageController
task = ''
taskId = ''
/** Agent execution status */
#status: AgentStatus = 'idle'
/** History events */
history: HistoricalEvent[] = []
/** Whether this agent has been disposed */
disposed = false
/**
* Callback for when agent needs user input (ask_user tool)
@@ -66,23 +79,21 @@ export class PageAgentCore extends EventTarget {
*/
onAskUser?: (question: string) => Promise<string>
#status: AgentStatus = 'idle'
#llm: LLM
#abortController = new AbortController()
#observations: string[] = []
/** PageController for DOM operations */
pageController: PageController
/** Runtime states for tracking across steps */
states = {
/** Accumulated wait time in seconds, used by wait tool */
/** internal states during a single task execution */
#states = {
/** Accumulated wait time in seconds */
totalWaitTime: 0,
/** Last known URL for detecting navigation */
/** For detecting navigation */
lastURL: '',
/** Browser state */
browserState: null as BrowserState | null,
}
/** History events */
history: HistoricalEvent[] = []
constructor(config: PageAgentConfig & { pageController: PageController }) {
super()
@@ -166,15 +177,24 @@ export class PageAgentCore extends EventTarget {
}
/**
* Push a persistent observation to the history event stream.
* This will be visible in <agent_history> and remain in memory across steps.
* Push a observation message to the history event stream.
* This will be visible in <agent_history> and remain persistent in memory across steps.
* @experimental @internal
* @note history change will be emitted before next step starts
*/
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
this.#emitHistoryChange()
this.#observations.push(content)
}
/** Stop the current task. Agent remains reusable. */
stop() {
this.pageController.cleanUpHighlights()
this.pageController.hideMask()
this.#abortController.abort()
}
async execute(task: string): Promise<ExecutionResult> {
if (this.disposed) throw new Error('PageAgent has been disposed. Create a new instance.')
if (!task) throw new Error('Task is required')
this.task = task
this.taskId = uid()
@@ -203,30 +223,25 @@ export class PageAgentCore extends EventTarget {
this.#setStatus('running')
this.#emitHistoryChange()
// Reset states
this.states = {
totalWaitTime: 0,
lastURL: '',
}
// Reset internal states
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
try {
let step = 0
let step = 0
while (true) {
await this.#generateObservations(step)
while (true) {
try {
console.group(`step: ${step}`)
await onBeforeStep?.(this, step)
console.group(`step: ${step}`)
// observe
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('👀 Observing...'))
// Thinking
console.log(chalk.blue('Thinking...'))
this.#emitActivity({ type: 'thinking' })
this.#states.browserState = await this.pageController.getBrowserState()
await this.#handleObservations(step)
// invoke LLM
// assemble prompts
const messages = [
{ role: 'system' as const, content: this.#getSystemPrompt() },
@@ -235,12 +250,17 @@ export class PageAgentCore extends EventTarget {
const tools = { AgentOutput: this.#packMacroTool() }
// invoke LLM
console.log(chalk.blue.bold('🧠 Thinking...'))
this.#emitActivity({ type: 'thinking' })
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
toolChoiceName: 'AgentOutput',
normalizeResponse,
})
// assemble history event
// assemble history
const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input
@@ -270,22 +290,12 @@ export class PageAgentCore extends EventTarget {
//
console.log(chalk.green('Step finished:'), actionName)
console.groupEnd()
await onAfterStep?.(this, this.history)
step++
if (step > this.config.maxSteps) {
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: 'Step count exceeded maximum limit',
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
console.groupEnd()
// finish task if done
if (actionName === 'done') {
const success = action.input?.success ?? false
const text = action.input?.text || 'no text provided'
@@ -299,19 +309,39 @@ export class PageAgentCore extends EventTarget {
await onAfterTask?.(this, result)
return result
}
} catch (error: unknown) {
console.groupEnd() // to prevent nested groups
const isAbortError = (error as any)?.rawError?.name === 'AbortError'
console.error('Task failed', error)
const errorMessage = isAbortError ? 'Task stopped' : String(error)
this.#emitActivity({ type: 'error', message: errorMessage })
this.history.push({ type: 'error', message: errorMessage, rawResponse: error })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
} catch (error: unknown) {
console.error('Task failed', error)
const errorMessage = String(error)
this.#emitActivity({ type: 'error', message: errorMessage })
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
step++
if (step > this.config.maxSteps) {
const errorMessage = 'Step count exceeded maximum limit'
this.history.push({ type: 'error', message: errorMessage })
this.#emitHistoryChange()
this.#onDone(false)
const result: ExecutionResult = {
success: false,
data: errorMessage,
history: this.history,
}
await onAfterTask?.(this, result)
return result
}
await onAfterTask?.(this, result)
return result
}
}
@@ -344,13 +374,13 @@ export class PageAgentCore extends EventTarget {
})
return {
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
description: 'You MUST call this tool every step!',
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort
if (this.#abortController.signal.aborted) throw new Error('AbortError')
console.log(chalk.blue.bold('MacroTool execute'), input)
console.log(chalk.blue.bold('MacroTool input'), input)
const action = input.action
const toolName = Object.keys(action)[0]
@@ -371,7 +401,7 @@ export class PageAgentCore extends EventTarget {
// Find the corresponding tool
const tool = tools.get(toolName)
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
assert(tool, `Tool ${toolName} not found`)
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
@@ -395,9 +425,11 @@ export class PageAgentCore extends EventTarget {
duration,
})
// Reset wait time for non-wait tools
if (toolName !== 'wait') {
this.states.totalWaitTime = 0
// counting wait time
if (toolName === 'wait') {
this.#states.totalWaitTime += toolInput?.seconds || 0
} else {
this.#states.totalWaitTime = 0
}
// Return structured result
@@ -417,10 +449,8 @@ export class PageAgentCore extends EventTarget {
return this.config.customSystemPrompt
}
let systemPrompt = SYSTEM_PROMPT
const targetLanguage = this.config.language === 'zh-CN' ? '中文' : 'English'
systemPrompt = systemPrompt.replace(
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`Default working language: **${targetLanguage}**`
)
@@ -429,17 +459,17 @@ export class PageAgentCore extends EventTarget {
}
/**
* Get instructions from config and format as XML block
* Get instructions from config
*/
async #getInstructions(): Promise<string> {
const { instructions } = this.config
if (!instructions) return ''
const systemInstructions = instructions.system?.trim()
const url = await this.pageController.getCurrentUrl()
let pageInstructions: string | undefined
if (instructions.getPageInstructions) {
const url = this.#states.browserState?.url || ''
if (instructions.getPageInstructions && url) {
try {
pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) {
@@ -467,23 +497,28 @@ export class PageAgentCore extends EventTarget {
}
/**
* Generate observations before each step
* - URL change detection
* - Too many steps warning
* Generate system observations before each step
* @todo loop detection
* @todo console error
*/
async #generateObservations(stepCount: number): Promise<void> {
async #handleObservations(step: number): Promise<void> {
// Accumulated wait time warning
if (this.#states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
)
}
// Detect URL change
const currentURL = await this.pageController.getCurrentUrl()
if (currentURL !== this.states.lastURL) {
const currentURL = this.#states.browserState?.url || ''
if (currentURL !== this.#states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.states.lastURL = currentURL
this.#states.lastURL = currentURL
await waitFor(0.5) // wait for page to stabilize
}
// Warn about remaining steps
const remaining = this.config.maxSteps - stepCount
// Remaining steps warning
const remaining = this.config.maxSteps - step
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
@@ -493,12 +528,25 @@ export class PageAgentCore extends EventTarget {
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
// Push observations to history and emit
if (this.#observations.length > 0) {
for (const content of this.#observations) {
this.history.push({ type: 'observation', content })
console.log(chalk.cyan('Observation:'), content)
}
this.#observations = []
this.#emitHistoryChange()
}
}
async #assembleUserPrompt(): Promise<string> {
const browserState = this.#states.browserState!
let prompt = ''
// <instructions> (optional)
prompt += await this.#getInstructions()
// <agent_state>
@@ -508,38 +556,36 @@ export class PageAgentCore extends EventTarget {
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += `<agent_state>
<user_request>
${this.task}
</user_request>
<step_info>
Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
Current date and time: ${new Date().toLocaleString()}
</step_info>
</agent_state>
`
prompt += '<agent_state>\n'
prompt += '<user_request>\n'
prompt += `${this.task}\n`
prompt += '</user_request>\n'
prompt += '<step_info>\n'
prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps\n`
prompt += `Current time: ${new Date().toLocaleString()}\n`
prompt += '</step_info>\n'
prompt += '</agent_state>\n\n'
// <agent_history>
// - <step_N> for steps
// - <sys> for observations and system messages
prompt += '\n<agent_history>\n'
prompt += '<agent_history>\n'
let stepIndex = 0
for (const event of this.history) {
if (event.type === 'step') {
stepIndex++
prompt += `<step_${stepIndex}>
Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
Memory: ${event.reflection.memory}
Next Goal: ${event.reflection.next_goal}
Action Results: ${event.action.output}
</step_${stepIndex}>
`
prompt += `<step_${stepIndex}>\n`
prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}\n`
prompt += `Memory: ${event.reflection.memory}\n`
prompt += `Next Goal: ${event.reflection.next_goal}\n`
prompt += `Action Results: ${event.action.output}\n`
prompt += `</step_${stepIndex}>\n`
} else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
prompt += `<sys>User took over control and made changes to the page</sys>\n`
} else if (event.type === 'error') {
// Error events are mainly for panel rendering, not included in LLM context
// to avoid polluting the agent's reasoning with transient errors
@@ -550,9 +596,18 @@ export class PageAgentCore extends EventTarget {
// <browser_state>
prompt += await this.#getBrowserState()
let pageContent = browserState.content
if (this.config.transformPageContent) {
pageContent = await this.config.transformPageContent(pageContent)
}
return trimLines(prompt)
prompt += '<browser_state>\n'
prompt += browserState.header + '\n'
prompt += pageContent + '\n'
prompt += browserState.footer + '\n\n'
prompt += '</browser_state>\n\n'
return prompt
}
#onDone(success = true) {
@@ -562,25 +617,9 @@ export class PageAgentCore extends EventTarget {
this.#abortController.abort()
}
async #getBrowserState(): Promise<string> {
const state = await this.pageController.getBrowserState()
let content = state.content
if (this.config.transformPageContent) {
content = await this.config.transformPageContent(content)
}
return trimLines(`<browser_state>
${state.header}
${content}
${state.footer}
</browser_state>
`)
}
dispose() {
console.log('Disposing PageAgent...')
this.disposed = true
this.pageController.dispose()
// this.history = []
this.#abortController.abort()
+5 -7
View File
@@ -10,7 +10,7 @@ You excel at following tasks:
</intro>
<language_settings>
- Default working language: **中文**
- Default working language: **English**
- Use the language that user is using. Return in user's language.
</language_settings>
@@ -128,7 +128,6 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples>
- Positive Examples:
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples>
@@ -139,17 +138,16 @@ Here are examples of good output patterns. Use them as reference but never copy
<next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
"next_goal": "Extract details from the first item on the page."
</next_goal_examples>
</examples>
<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.",
"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."
"action":{"one_action_name": {// action-specific parameter}}
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{
"Action name": {// Action parameters}
}
}
</output>
+11 -20
View File
@@ -2,7 +2,7 @@
* Internal tools for PageAgent.
* @note Adapted from browser-use
*/
import zod, { type z } from 'zod'
import * as zod from 'zod'
import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils'
@@ -13,7 +13,7 @@ import { waitFor } from '../utils'
export interface PageAgentTool<TParams = any> {
// name: string
description: string
inputSchema: z.ZodType<TParams>
inputSchema: zod.ZodType<TParams>
execute: (this: PageAgentCore, args: TParams) => Promise<string>
}
@@ -31,14 +31,13 @@ tools.set(
'done',
tool({
description:
'Complete task - provide a summary of results for the user. Set success=True if task completed successfully, false otherwise. Text should be your response to the user summarizing results.',
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
inputSchema: zod.object({
text: zod.string(),
success: zod.boolean().default(true),
}),
execute: async function (this: PageAgentCore, input) {
// @note main loop will handle this one
// this.onDone(input.text, input.success)
return Promise.resolve('Task completed')
},
})
@@ -47,25 +46,17 @@ tools.set(
tools.set(
'wait',
tool({
description:
'Wait for x seconds. default 1s (max 10 seconds, min 1 second). This can be used to wait until the page or data is fully loaded.',
description: 'Wait for x seconds. Can be used to wait until the page or data is fully loaded.',
inputSchema: zod.object({
seconds: zod.number().min(1).max(10).default(1),
}),
execute: async function (this: PageAgentCore, input) {
// try to subtract LLM calling time from the actual wait time
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime)
this.states.totalWaitTime += input.seconds
if (this.states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
)
}
return `✅ Waited for ${input.seconds} seconds.`
},
})
@@ -106,7 +97,7 @@ tools.set(
tools.set(
'input_text',
tool({
description: 'Click and input text into a input interactive element',
description: 'Click and type text into an interactive input element',
inputSchema: zod.object({
index: zod.int().min(0),
text: zod.string(),
@@ -140,8 +131,7 @@ tools.set(
tools.set(
'scroll',
tool({
description:
'Scroll the page by specified number of pages (set down=True to scroll down, down=False to scroll up, num_pages=number of pages to scroll like 0.5 for half page, 1.0 for one page, etc.). Optional index parameter to scroll within a specific element or its scroll container (works well for dropdowns and custom UI components). Optional pixels parameter to scroll by a specific number of pixels instead of pages.',
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
inputSchema: zod.object({
down: zod.boolean().default(true),
num_pages: zod.number().min(0).max(10).optional().default(0.1),
@@ -158,11 +148,14 @@ tools.set(
})
)
/**
* @todo Tables need a dedicated parser to extract structured data. This tool is useless.
*/
tools.set(
'scroll_horizontally',
tool({
description:
'Scroll the page or element horizontally (set right=True to scroll right, right=False to scroll left, pixels=number of pixels to scroll). Optional index parameter to scroll within a specific element or its scroll container (works well for wide tables).',
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
inputSchema: zod.object({
right: zod.boolean().default(true),
pixels: zod.number().int().min(0),
@@ -190,8 +183,6 @@ tools.set(
})
)
// @todo get_dropdown_options
// @todo select_dropdown_option
// @todo send_keys
// @todo upload_file
// @todo go_back
-17
View File
@@ -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)
}
}
+18 -31
View File
@@ -1,27 +1,7 @@
import chalk from 'chalk'
export { normalizeResponse } from './autoFixer'
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
* @param timeout Timeout in milliseconds, default 0 means no timeout, throws error on timeout
*/
export async function waitUntil(check: () => boolean, timeout = 60 * 60_1000): Promise<boolean> {
if (check()) return true
return new Promise((resolve, reject) => {
const start = Date.now()
const interval = setInterval(() => {
if (check()) {
clearInterval(interval)
resolve(true)
} else if (Date.now() - start > timeout) {
clearInterval(interval)
reject(new Error('Timeout waiting for condition to become true'))
}
}, 100)
})
}
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
@@ -37,15 +17,6 @@ export function truncate(text: string, maxLength: number): string {
//
export function trimLines(text: string): string {
return text
.split('\n')
.map((line) => line.trim())
.join('\n')
}
//
export function randomID(existingIDs?: string[]): string {
let id = Math.random().toString(36).substring(2, 11)
@@ -85,3 +56,19 @@ export function uid() {
ids.push(id)
return id
}
/**
* Simple assertion function that throws an error if the condition is falsy
* @param condition - The condition to assert
* @param message - Optional error message
* @throws Error if condition is falsy
*/
export function assert(condition: unknown, message?: string, silent?: boolean): asserts condition {
if (!condition) {
const errorMessage = message ?? 'Assertion failed'
if (!silent) console.error(chalk.red(`❌ assert: ${errorMessage}`))
throw new Error(errorMessage)
}
}
+7 -4
View File
@@ -1,6 +1,6 @@
# Privacy Policy for Page Agent Extension
**Last updated:** January 2026
**Last updated:** February 2026
## Overview
@@ -10,24 +10,27 @@ Page Agent Extension is a browser automation tool that uses AI to help you inter
### Local Processing
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension itself.
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
### Data Transmission
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands)
- Sanitized page structure (simplified DOM, excluding sensitive form values)
- Simplified page structure (cleaned DOM) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**.
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
**If you use our testing endpoint**, your requests are proxied to [DeepSeek](https://deepseek.com) for AI processing. Regarding this test endpoint:
- This endpoint is provided for evaluation purposes only and is not recommended for production or daily use
- The free model and their service providers may change at any time without prior notice
- We do **not** store your task content, page content, or visited URLs
- Minimal logging (timestamps, request metadata, IP addresses) may occur for abuse prevention and service stability
- Minimal logging (timestamps, request metadata, IP addresses) may be collected for abuse prevention and service stability
- DeepSeek's [Privacy Policy](https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html) applies to their processing of your requests
## Data Storage
+2 -2
View File
@@ -5,7 +5,7 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"css": "src/assets/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
@@ -16,7 +16,7 @@
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"hooks": "@/lib/hooks"
},
"registries": {
"@magicui": "https://magicui.design/r/{name}.json"
+30
View File
@@ -0,0 +1,30 @@
AI-powered browser automation. Control web pages with natural language.
Page Agent Ext — AI-Powered Browser Automation
🌟 What is Page Agent Ext?
Page Agent Ext brings AI-powered automation to your browser. Built on the open-source Page Agent framework, it lets you control web pages across multiple tabs using natural language — no scripting required.
🌟 Key Features:
- Natural Language Control — Command your browser in plain language, no code needed
- Cross-Tab Automation — Seamlessly operate across multiple tabs and pages
- Smart HTML Cleaning — Intelligently extracts and simplifies page structure for accurate AI understanding
- Bring Your Own LLM — Use OpenAI, Anthropic, or any compatible API with full data control
- Privacy-First — Zero data collection; all data flows directly to your chosen LLM provider
- Open Source — MIT licensed, built on the Page Agent framework with full transparency
🌟 How It Works & Privacy:
Page Agent Ext performs DOM analysis locally in your browser. When you initiate a task, sanitized page structure is sent to the LLM API you configure. Your data is never collected or stored by us.
- Your API Key — Configure your own LLM API (OpenAI, Anthropic, etc.). Data goes directly to your provider
- Test API — A free test endpoint is available for evaluation; we recommend your own key for regular use
Privacy policy: https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
🌟 Open Source:
This project is MIT licensed. Review the code, verify privacy claims, or extend it for your needs:
https://github.com/alibaba/page-agent
@@ -0,0 +1,30 @@
AI 驱动的浏览器自动化助手,用自然语言控制网页。
Page Agent Ext — AI 驱动的浏览器自动化助手
🌟 什么是 Page Agent Ext
Page Agent Ext 为浏览器带来 AI 自动化能力。基于开源的 Page Agent 框架,你可以用自然语言跨标签页控制网页,无需编写任何脚本。
🌟 核心特性:
- 自然语言控制 — 用日常语言指挥浏览器,无需编程
- 跨标签页操作 — 在多个标签页之间无缝切换和操控
- HTML 智能清洗 — 智能提取和精简页面结构,让 AI 准确理解网页内容
- 使用你自己的模型 — 支持 OpenAI、Anthropic 或任何兼容 API,数据完全自主可控
- 隐私优先 — 零数据收集,所有数据直接发送到你配置的 LLM 服务商
- 开源透明 — MIT 协议,基于 Page Agent 开源框架,代码完全公开
🌟 工作原理与隐私:
Page Agent Ext 在浏览器本地进行 DOM 分析。当你发起任务时,经清洗的页面结构会发送到你配置的 LLM API。我们不会收集或存储你的任何数据。
- 你的 API Key — 配置你自己的 LLM APIOpenAI、Anthropic 等),数据直接发送到你的服务商
- 测试 API — 提供免费测试端点供体验,日常使用建议配置自己的 Key
隐私政策:https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md
🌟 开源项目:
MIT 协议开源。查看源码、验证隐私承诺,或按需扩展:
https://github.com/alibaba/page-agent
+226
View File
@@ -0,0 +1,226 @@
# Page Agent Extension API
Integrate the Page Agent extension into your web app and trigger multi-page browser tasks from page JavaScript.
## Installation
### 1. Install the browser extension
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)
```bash
npm install @page-agent/core --save-dev
```
### 3. Authorization (Token)
The token allows your page JS to call the extension API (`window.PAGE_AGENT_EXT`) and execute multi-page tasks.
Why token-based access is required:
- The extension has broad browser permissions (page access, navigation, multi-tab control).
- If abused, it can harm user privacy and security.
- Users must explicitly provide the token only to applications they trust.
Setup:
1. Open the extension side panel and copy your auth token.
2. Set the token in your page:
```typescript
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
```
## Quick Start
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
// Wait for extension injection (up to 1 second)
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
}
// Usage
if (await waitForExtension()) {
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
onStatusChange: (status) => console.log('Status:', status),
onActivity: (activity) => console.log('Activity:', activity),
})
console.log('Result:', result)
}
```
## Global API
After token match, the extension injects APIs into `window`.
### `window.PAGE_AGENT_EXT_VERSION`
Extension version string (for capability checks before using the main API).
### `window.PAGE_AGENT_EXT`
Main namespace object.
#### `PAGE_AGENT_EXT.execute(task, config)`
Execute one agent task.
Parameters:
| Name | Type | Required | Description |
| ---- | ---- | -------- | ----------- |
| `task` | `string` | Yes | Task description |
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
Returns: `Promise<ExecutionResult>`
#### `PAGE_AGENT_EXT.stop()`
Stop the current task.
## Types
Install `@page-agent/core` for complete types:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
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
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
```
`AgentStatus`
```typescript
type AgentStatus = '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 }
```
`HistoricalEvent`
```typescript
type HistoricalEvent =
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
| { type: 'observation'; content: string }
| { type: 'user_takeover' }
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
| { type: 'error'; message: string; rawResponse?: unknown }
```
`ExecutionResult`
```typescript
interface ExecutionResult {
success: boolean
data: string
history: HistoricalEvent[]
}
```
## Usage Examples
### Basic Execution
```typescript
const result = await window.PAGE_AGENT_EXT!.execute(
'Fill in the email field with test@example.com and click Submit',
{
baseURL: 'https://api.openai.com/v1',
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-5.2',
includeInitialTab: false, // Optional: exclude current tab
onStatusChange: (status) => console.log(status),
onActivity: (activity) => console.log(activity),
}
)
```
### Stop the Current Task
```typescript
window.PAGE_AGENT_EXT!.stop()
```
## Window Type Declaration
If you are not importing `@page-agent/core`, add:
```typescript
import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent,
} from '@page-agent/core'
interface ExecuteConfig {
baseURL: string
apiKey: string
model: string
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
declare global {
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}
```
+23 -25
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "0.1.0-b4",
"version": "0.1.7",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -10,27 +10,6 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/chrome": "^0.1.34",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"@wxt-dev/module-react": "^1.1.5",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.2",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0",
"wxt": "^0.20.13"
},
"dependencies": {
"@page-agent/core": "1.0.0",
"@page-agent/llms": "1.0.0",
"@page-agent/page-controller": "1.0.0",
"@page-agent/ui": "1.0.0",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
@@ -38,14 +17,33 @@
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/chrome": "^0.1.34",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.1",
"@wxt-dev/module-react": "^1.1.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"idb": "^8.0.3",
"lucide-react": "^0.563.0",
"motion": "^12.29.2",
"motion": "^12.34.0",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rough-notation": "^0.5.1",
"simple-icons": "^16.5.0",
"simple-icons": "^16.8.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.14"
},
"dependencies": {
"@page-agent/core": "1.3.0",
"@page-agent/llms": "1.3.0",
"@page-agent/page-controller": "1.3.0",
"@page-agent/ui": "1.3.0",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2",
"zod": "^4.3.5"
}
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI-powered browser automation assistant. Control web pages with natural language."
},
"extActionTitle": {
"message": "Open Page Agent"
}
}
@@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Agent Ext"
},
"extDescription": {
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
},
"extActionTitle": {
"message": "打开 Page Agent"
}
}
+27 -5
View File
@@ -5,17 +5,35 @@ import { TabsController } from './TabsController'
import SYSTEM_PROMPT from './system_prompt.md?raw'
import { createTabTools } from './tabTools'
/** Detect user language from browser settings */
function detectLanguage(): 'en-US' | 'zh-CN' {
const lang = navigator.language || navigator.languages?.[0] || 'en-US'
return lang.startsWith('zh') ? 'zh-CN' : 'en-US'
}
/**
* MultiPageAgent
* - use with extension
* - can be used from a side panel or a content script
*/
export class MultiPageAgent extends PageAgentCore {
constructor(config: Omit<PageAgentConfig, 'pageController'>) {
constructor(config: Omit<PageAgentConfig, 'pageController'> & { includeInitialTab?: boolean }) {
// multi page controller
const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController)
const customTools = createTabTools(tabsController)
// system prompt - auto-detect language if not specified
const language = config.language ?? detectLanguage()
const targetLanguage = language === 'zh-CN' ? '中文' : 'English'
const systemPrompt = SYSTEM_PROMPT.replace(
/Default working language: \*\*.*?\*\*/,
`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.
* There is no chance for isAgentRunning to be set false.
@@ -29,10 +47,10 @@ export class MultiPageAgent extends PageAgentCore {
...config,
pageController: pageController as any,
customTools: customTools,
customSystemPrompt: SYSTEM_PROMPT,
customSystemPrompt: systemPrompt,
onBeforeTask: async (agent) => {
await tabsController.init(agent.task)
await tabsController.init(agent.task, includeInitialTab)
heartBeatInterval = window.setInterval(() => {
chrome.storage.local.set({
@@ -56,6 +74,11 @@ export class MultiPageAgent extends PageAgentCore {
})
},
onBeforeStep: async (agent) => {
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
},
onDispose: () => {
if (heartBeatInterval) {
window.clearInterval(heartBeatInterval)
@@ -66,8 +89,7 @@ export class MultiPageAgent extends PageAgentCore {
isAgentRunning: false,
})
// no need to dispose tabsController and pageController
// as they do not keep references
tabsController.dispose()
},
})
}
@@ -8,13 +8,21 @@ export function handlePageControlMessage(
sender: chrome.runtime.MessageSender,
sendResponse: (response: unknown) => void
): true | undefined {
const PREFIX = '[RemotePageController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
const { action, payload, targetTabId } = message
if (action === 'get_my_tab_id') {
debug('get_my_tab_id', sender.tab?.id)
sendResponse({ tabId: sender.tab?.id || null })
return
}
// proxy to content script
chrome.tabs
.sendMessage(targetTabId, {
type: 'PAGE_CONTROL',
@@ -25,6 +33,7 @@ export function handlePageControlMessage(
sendResponse(result)
})
.catch((error) => {
console.error(PREFIX, error)
sendResponse({
success: false,
error: error instanceof Error ? error.message : String(error),
@@ -12,10 +12,14 @@ export function initPageController() {
.then((response) => {
return (response as { tabId: number | null }).tabId
})
.catch((error) => {
console.error('[RemotePageController.ContentScript]: Failed to get my tab id', error)
return null
})
function getPC(): PageController {
if (!pageController) {
pageController = new PageController({ enableMask: false })
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
}
return pageController
}
@@ -2,13 +2,31 @@ import type { BrowserState } from '@page-agent/page-controller'
import type { TabsController } from './TabsController'
const PREFIX = '[RemotePageController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'PAGE_CONTROL'
action: string
targetTabId: number
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Agent side page controller.
* - live in the agent env (extension page or content script)
* - communicates with remote PageController via sw
*/
export class RemotePageController {
private tabsController: TabsController
tabsController: TabsController
constructor(tabsController: TabsController) {
this.tabsController = tabsController
@@ -18,46 +36,46 @@ export class RemotePageController {
return this.tabsController.currentTabId
}
async getCurrentUrl(): Promise<string> {
private async getCurrentUrl(): Promise<string> {
if (!this.currentTabId) return ''
const { url } = await this.tabsController.getTabInfo(this.currentTabId)
return url || ''
}
async getCurrentTitle(): Promise<string> {
private async getCurrentTitle(): Promise<string> {
if (!this.currentTabId) return ''
const { title } = await this.tabsController.getTabInfo(this.currentTabId)
return title || ''
}
get currentTabTitle(): Promise<string> {
return this.getCurrentTitle()
}
async getLastUpdateTime(): Promise<number> {
if (!this.currentTabId) throw new Error('tabsController not initialized.')
return await chrome.runtime.sendMessage({
return sendMessage({
type: 'PAGE_CONTROL',
action: 'get_last_update_time',
targetTabId: this.currentTabId,
})
}
// getBrowserState
async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState
if (!this.currentTabId) throw new Error('tabsController not initialized.')
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
let browserState = {} as BrowserState
debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl()
const currentTitle = await this.getCurrentTitle()
if (!this.currentTabId || !isContentScriptAllowed(currentUrl)) {
browserState = {
url: await this.currentTabUrl,
title: await this.currentTabTitle,
url: currentUrl,
title: currentTitle,
header: '',
content: '(empty page)',
content: '(empty page. either current page is not readable or not loaded yet.)',
footer: '',
}
} else {
browserState = await chrome.runtime.sendMessage({
browserState = await sendMessage({
type: 'PAGE_CONTROL',
action: 'get_browser_state',
targetTabId: this.currentTabId,
@@ -67,61 +85,58 @@ export class RemotePageController {
const sum = await this.tabsController.summarizeTabs()
browserState.header = sum + '\n\n' + (browserState.header || '')
debug('getBrowserState: success', this.currentTabId, browserState)
return browserState
}
// updateTree
async updateTree(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await chrome.runtime.sendMessage({
await sendMessage({
type: 'PAGE_CONTROL',
action: 'update_tree',
targetTabId: this.currentTabId,
})
}
// cleanUpHighlights
async cleanUpHighlights(): Promise<void> {
if (!this.currentTabId || !isContentScriptAllowed(await this.currentTabUrl)) {
if (!this.currentTabId || !isContentScriptAllowed(await this.getCurrentUrl())) {
return
}
await chrome.runtime.sendMessage({
await sendMessage({
type: 'PAGE_CONTROL',
action: 'clean_up_highlights',
targetTabId: this.currentTabId,
})
}
// clickElement
async clickElement(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('click_element', args)
const res = await this.remoteCallDomAction('click_element', args)
// @note may cause page navigation, wait for 1 second to ensure the page loading started
await new Promise((resolve) => setTimeout(resolve, 1000))
return res
}
// inputText
async inputText(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('input_text', args)
}
// selectOption
async selectOption(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('select_option', args)
}
// scroll
async scroll(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll', args)
}
// scrollHorizontally
async scrollHorizontally(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('scroll_horizontally', args)
}
// executeJavascript
async executeJavascript(...args: any[]): Promise<DomActionReturn> {
return this.remoteCallDomAction('execute_javascript', args)
}
@@ -133,35 +148,26 @@ export class RemotePageController {
/** @note Managed by content script via storage polling. */
dispose(): void {}
private async preCheck() {
if (!this.currentTabId) {
return 'RemotePageController not initialized.'
}
if (!isContentScriptAllowed(await this.currentTabUrl)) {
return 'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.'
}
return null
}
private async remoteCallDomAction(action: string, payload: any[]): Promise<DomActionReturn> {
const preCheckError = await this.preCheck()
if (preCheckError) {
return { success: false, message: preCheckError }
if (!this.currentTabId) {
return { success: false, message: 'RemotePageController not initialized.' }
}
return await chrome.runtime.sendMessage({
if (!isContentScriptAllowed(await this.getCurrentUrl())) {
return {
success: false,
message:
'Operation not allowed on this page. Use open_new_tab to navigate to a web page first.',
}
}
return sendMessage({
type: 'PAGE_CONTROL',
action: action,
targetTabId: this.currentTabId!,
payload,
})
}
private get currentTabUrl(): Promise<string> {
return this.getCurrentUrl()
}
}
interface DomActionReturn {
@@ -3,6 +3,12 @@
*/
import type { TabAction } from './TabsController'
const PREFIX = '[TabsController.background]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
export function handleTabControlMessage(
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
sender: chrome.runtime.MessageSender,
@@ -12,10 +18,12 @@ export function handleTabControlMessage(
switch (action as TabAction) {
case 'get_active_tab': {
debug('get_active_tab')
chrome.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => {
const tabId = tabs.length > 0 ? tabs[0].id || null : null
debug('get_active_tab: success', tabId)
sendResponse({ success: true, tabId })
})
.catch((error) => {
@@ -25,11 +33,12 @@ export function handleTabControlMessage(
}
case 'get_tab_info': {
debug('get_tab_info', payload)
chrome.tabs
.get(payload.tabId)
.then((tab) => {
const result = { title: tab.title || '', url: tab.url || '' }
sendResponse(result)
debug('get_tab_info: success', tab)
sendResponse(tab)
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -38,10 +47,11 @@ export function handleTabControlMessage(
}
case 'open_new_tab': {
debug('open_new_tab', payload)
chrome.tabs
.create({ url: payload.url, active: false })
.then((newTab) => {
// @todo: wait for tab to be fully loaded
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId })
})
.catch((error) => {
@@ -51,20 +61,22 @@ export function handleTabControlMessage(
}
case 'create_tab_group': {
debug('create_tab_group', payload)
chrome.tabs
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
.then((groupId) => {
console.log('Created tab group', groupId)
debug('create_tab_group: success', groupId)
sendResponse({ success: true, groupId })
})
.catch((error) => {
console.error('Failed to create tab group', error)
console.error(PREFIX, 'Failed to create tab group', error)
sendResponse({ error: error instanceof Error ? error.message : String(error) })
})
return true // async response
}
case 'update_tab_group': {
debug('update_tab_group', payload)
chrome.tabGroups
.update(payload.groupId, payload.properties)
.then(() => {
@@ -77,6 +89,7 @@ export function handleTabControlMessage(
}
case 'add_tab_to_group': {
debug('add_tab_to_group', payload)
chrome.tabs
.group({ tabIds: payload.tabId, groupId: payload.groupId })
.then(() => {
@@ -89,6 +102,7 @@ export function handleTabControlMessage(
}
case 'close_tab': {
debug('close_tab', payload)
chrome.tabs
.remove(payload.tabId)
.then(() => {
@@ -105,3 +119,42 @@ export function handleTabControlMessage(
return
}
}
export function setupTabChangeEvents() {
console.log('[TabsController.background] setupTabChangeEvents')
chrome.tabs.onCreated.addListener((tab) => {
debug('onCreated', tab)
chrome.runtime
.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
.catch((error) => {
debug('onCreated error:', error)
})
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
debug('onRemoved', tabId, removeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'removed',
payload: { tabId, removeInfo },
})
.catch((error) => {
debug('onRemoved error:', error)
})
})
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
debug('onUpdated', tabId, changeInfo)
chrome.runtime
.sendMessage({
type: 'TAB_CHANGE',
action: 'updated',
payload: { tabId, changeInfo, tab },
})
.catch((error) => {
debug('onUpdated error:', error)
})
})
}
+135 -49
View File
@@ -1,3 +1,20 @@
const PREFIX = '[TabsController]'
function debug(...messages: any[]) {
console.debug(`\x1b[90m${PREFIX}\x1b[0m`, ...messages)
}
function sendMessage(message: {
type: 'TAB_CONTROL'
action: TabAction
payload?: any
}): Promise<any> {
return chrome.runtime.sendMessage(message).catch((error) => {
console.error(PREFIX, message.action, error)
return null
})
}
/**
* Controller for managing browser tabs.
* - live in the agent env (extension page or content script)
@@ -12,7 +29,9 @@ export class TabsController extends EventTarget {
private task: string = ''
private windowId: number | null = null
async init(task: string) {
async init(task: string, includeInitialTab: boolean = true) {
debug('init', task, includeInitialTab)
this.task = task
this.tabs = []
this.currentTabId = null
@@ -20,23 +39,36 @@ export class TabsController extends EventTarget {
this.initialTabId = null
this.windowId = null
const result = await chrome.runtime.sendMessage({
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
})
this.initialTabId = result.tabId
this.currentTabId = result.tabId
this.tabs.push({
id: result.tabId,
isInitial: true,
})
if (!this.initialTabId) {
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)
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 }> {
const result = await chrome.runtime.sendMessage({
async openNewTab(url: string): Promise<string> {
debug('openNewTab', url)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'open_new_tab',
payload: { url },
@@ -102,7 +144,7 @@ export class TabsController extends EventTarget {
await this.switchToTab(tabId)
if (!this.tabGroupId) {
const result = await chrome.runtime.sendMessage({
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds: [tabId], windowId: this.windowId },
@@ -116,7 +158,7 @@ export class TabsController extends EventTarget {
this.tabGroupId = groupId
await chrome.runtime.sendMessage({
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
@@ -129,57 +171,43 @@ export class TabsController extends EventTarget {
},
})
} else {
await chrome.runtime.sendMessage({
await sendMessage({
type: 'TAB_CONTROL',
action: 'add_tab_to_group',
payload: { tabId: result.tabId, groupId: this.tabGroupId },
})
}
// wait for the new tab to be fully loaded
// @todo
await new Promise((resolve) => setTimeout(resolve, 500))
await this.waitUntilTabLoaded(tabId)
return {
success: true,
tabId,
message: `Opened new tab ID ${tabId} with URL ${url}`,
}
return `✅ Opened new tab ID ${tabId} with URL ${url}`
}
async switchToTab(tabId: number): Promise<{ success: boolean; message: string }> {
async switchToTab(tabId: number): Promise<string> {
debug('switchToTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
return {
success: false,
message: `Tab ID ${tabId} not found in tab list.`,
}
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
await this.updateCurrentTabId(tabId)
return {
success: true,
message: `Switched to tab ID ${tabId}.`,
}
return `✅ Switched to tab ID ${tabId}.`
}
async closeTab(tabId: number): Promise<{ success: boolean; message: string }> {
async closeTab(tabId: number): Promise<string> {
debug('closeTab', tabId)
const targetTab = this.tabs.find((t) => t.id === tabId)
if (!targetTab) {
return {
success: false,
message: `Tab ID ${tabId} not found in tab list.`,
}
throw new Error(`Tab ID ${tabId} not found in tab list.`)
}
if (targetTab.isInitial) {
return {
success: false,
message: `Cannot close the initial tab ID ${tabId}.`,
}
throw new Error(`Cannot close the initial tab ID ${tabId}.`)
}
const result = await chrome.runtime.sendMessage({
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'close_tab',
payload: { tabId },
@@ -196,29 +224,39 @@ export class TabsController extends EventTarget {
}
}
return {
success: true,
message: `Closed tab ID ${tabId}.`,
}
return `✅ Closed tab ID ${tabId}.`
} else {
return {
success: false,
message: `Failed to close tab ID ${tabId}: ${result.error}`,
}
throw new Error(`Failed to close tab ID ${tabId}: ${result.error}`)
}
}
async updateCurrentTabId(tabId: number | null) {
debug('updateCurrentTabId', tabId)
this.currentTabId = tabId
await chrome.storage.local.set({ currentTabId: tabId })
}
async getTabInfo(tabId: number): Promise<{ title: string; url: string }> {
const result = await chrome.runtime.sendMessage({
// use cached tab info if available
const tabMeta = this.tabs.find((t) => t.id === tabId)
if (tabMeta && tabMeta.url && tabMeta.title) {
return { title: tabMeta.title, url: tabMeta.url }
}
// otherwise, pull the latest tab info from the background script
debug('getTabInfo: pulling from background script', tabId)
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_tab_info',
payload: { tabId },
})
if (tabMeta) {
tabMeta.url = result.url
tabMeta.title = result.title
}
return result
}
@@ -230,9 +268,24 @@ export class TabsController extends EventTarget {
`| ${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')
}
async waitUntilTabLoaded(tabId: number): Promise<void> {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)
if (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
if (tab.status === 'complete') return
debug('waitUntilTabLoaded', tabId)
await waitUntil(() => tab.status === 'complete', 4_000)
}
dispose() {
this.dispatchEvent(new Event('dispose'))
}
@@ -251,6 +304,9 @@ export type TabAction =
interface TabMeta {
id: number
isInitial: boolean
url?: string
title?: string
status?: 'loading' | 'unloaded' | 'complete'
}
const TAB_GROUP_COLORS = [
@@ -269,3 +325,33 @@ type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
function randomColor(): TabGroupColor {
return TAB_GROUP_COLORS[Math.floor(Math.random() * TAB_GROUP_COLORS.length)]
}
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout
* @param error Error object to reject on timeout. If not provided, will resolve with false
*/
export async function waitUntil(
check: () => boolean | Promise<boolean>,
timeoutMS = 60_000,
error?: string
): Promise<boolean> {
if (await check()) return true
return new Promise((resolve, reject) => {
const start = Date.now()
const poll = async () => {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (error) {
return reject(new Error(error))
} else {
return resolve(false)
}
}
setTimeout(poll, 100)
}
setTimeout(poll, 100)
})
}
@@ -10,7 +10,7 @@ You excel at following tasks:
</intro>
<language_settings>
- Default working language: **中文**
- Default working language: **English**
- Use the language that user is using. Return in user's language.
</language_settings>
@@ -120,7 +120,6 @@ Exhibit the following reasoning patterns to successfully achieve the <user_reque
Here are examples of good output patterns. Use them as reference but never copy them directly.
<evaluation_examples>
- Positive Examples:
"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"
"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"
</evaluation_examples>
@@ -131,17 +130,16 @@ Here are examples of good output patterns. Use them as reference but never copy
<next_goal_examples>
"next_goal": "Click on the 'Add to Cart' button to proceed with the purchase flow."
"next_goal": "Extract details from the first item on the page."
</next_goal_examples>
</examples>
<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.",
"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."
"action":{"one_action_name": {// action-specific parameter}}
"next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",
"action":{
"Action name": {// Action parameters}
}
}
</output>
+16 -5
View File
@@ -6,7 +6,7 @@
* - switch_to_tab: Switch to an existing tab
* - close_tab: Close a tab (optionally switch to another)
*/
import zod from 'zod'
import * as zod from 'zod'
import type { TabsController } from './TabsController'
@@ -31,8 +31,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
}),
execute: async (input: unknown) => {
const { url } = input as { url: string }
const result = await tabsController.openNewTab(url)
return result.message
try {
return await tabsController.openNewTab(url)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
@@ -44,7 +47,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
return (await tabsController.switchToTab(tab_id)).message
try {
return await tabsController.switchToTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
@@ -56,7 +63,11 @@ export function createTabTools(tabsController: TabsController): Record<string, T
}),
execute: async (input: unknown) => {
const { tab_id } = input as { tab_id: number }
return (await tabsController.closeTab(tab_id)).message
try {
return await tabsController.closeTab(tab_id)
} catch (error) {
return `❌ Failed: ${error instanceof Error ? error.message : String(error)}`
}
},
},
}
+32 -13
View File
@@ -1,22 +1,34 @@
/**
* React hook for using AgentController
*/
import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/core'
import type {
AgentActivity,
AgentStatus,
HistoricalEvent,
SupportedLanguage,
} from '@page-agent/core'
import type { LLMConfig } from '@page-agent/llms'
import { useCallback, useEffect, useRef, useState } from 'react'
import { MultiPageAgent } from './MultiPageAgent'
import { DEMO_CONFIG } from './constants'
/** Language preference: undefined means follow system */
export type LanguagePreference = SupportedLanguage | undefined
export interface ExtConfig extends LLMConfig {
language?: LanguagePreference
}
export interface UseAgentResult {
status: AgentStatus
history: HistoricalEvent[]
activity: AgentActivity | null
currentTask: string
config: LLMConfig | null
config: ExtConfig | null
execute: (task: string) => Promise<void>
stop: () => void
configure: (config: LLMConfig) => Promise<void>
configure: (config: ExtConfig) => Promise<void>
}
export function useAgent(): UseAgentResult {
@@ -25,16 +37,17 @@ export function useAgent(): UseAgentResult {
const [history, setHistory] = useState<HistoricalEvent[]>([])
const [activity, setActivity] = useState<AgentActivity | null>(null)
const [currentTask, setCurrentTask] = useState('')
const [config, setConfig] = useState<LLMConfig | null>(null)
const [config, setConfig] = useState<ExtConfig | null>(null)
useEffect(() => {
chrome.storage.local.get('llmConfig').then((result) => {
if (result.llmConfig) {
setConfig(result.llmConfig as LLMConfig)
} else {
chrome.storage.local.get(['llmConfig', 'language']).then((result) => {
const llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
const language = (result.language as SupportedLanguage) || undefined
const full = { ...llmConfig, language }
if (!result.llmConfig) {
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
setConfig(DEMO_CONFIG)
}
setConfig(full)
})
}, [])
@@ -75,7 +88,7 @@ export function useAgent(): UseAgentResult {
const execute = useCallback(async (task: string) => {
const agent = agentRef.current
console.log('Fire task:', task, agent)
console.log('🚀 [useAgent] start executing task:', task)
if (!agent) throw new Error('Agent not initialized')
setCurrentTask(task)
@@ -84,11 +97,17 @@ export function useAgent(): UseAgentResult {
}, [])
const stop = useCallback(() => {
agentRef.current?.dispose()
agentRef.current?.stop()
}, [])
const configure = useCallback(async (newConfig: LLMConfig) => {
setConfig(newConfig)
const configure = useCallback(async ({ language, ...llmConfig }: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
if (language) {
await chrome.storage.local.set({ language })
} else {
await chrome.storage.local.remove('language')
}
setConfig({ ...llmConfig, language })
}, [])
return {
+36 -1
View File
@@ -40,7 +40,7 @@
}
.dark {
--background: oklch(0.145 0 0);
--background: oklch(0.19 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
@@ -111,6 +111,41 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
@keyframes blink-cursor {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
}
@keyframes glow-a {
0%,
100% {
opacity: 0.45;
transform: scale(1);
}
50% {
opacity: 0;
transform: scale(1.1);
}
}
@keyframes glow-b {
0%,
100% {
opacity: 0;
transform: scale(1.1);
}
50% {
opacity: 0.45;
transform: scale(1);
}
}
@layer base {
@@ -21,8 +21,9 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
// Focus state — soft multi-color glow matching ai-motion palette
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
// Error state.
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
@@ -0,0 +1,164 @@
import { MotionProps, motion, useInView } from 'motion/react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { cn } from '@/lib/utils'
interface TypingAnimationProps extends MotionProps {
children?: string
words?: string[]
className?: string
duration?: number
typeSpeed?: number
deleteSpeed?: number
delay?: number
pauseDelay?: number
loop?: boolean
as?: React.ElementType
startOnView?: boolean
showCursor?: boolean
blinkCursor?: boolean
cursorStyle?: 'line' | 'block' | 'underscore'
}
export function TypingAnimation({
children,
words,
className,
duration = 100,
typeSpeed,
deleteSpeed,
delay = 0,
pauseDelay = 1000,
loop = false,
as: Component = 'span',
startOnView = true,
showCursor = true,
blinkCursor = true,
cursorStyle = 'line',
...props
}: TypingAnimationProps) {
const MotionComponent = motion.create(Component, {
forwardMotionProps: true,
})
const [displayedText, setDisplayedText] = useState<string>('')
const [currentWordIndex, setCurrentWordIndex] = useState(0)
const [currentCharIndex, setCurrentCharIndex] = useState(0)
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
const elementRef = useRef<HTMLElement | null>(null)
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed || duration
const deletingSpeed = deleteSpeed || typingSpeed / 2
const shouldStart = startOnView ? isInView : true
useEffect(() => {
if (!shouldStart || wordsToAnimate.length === 0) return
const timeoutDelay =
delay > 0 && displayedText === ''
? delay
: phase === 'typing'
? typingSpeed
: phase === 'deleting'
? deletingSpeed
: pauseDelay
const timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ''
const graphemes = Array.from(currentWord)
switch (phase) {
case 'typing':
if (currentCharIndex < graphemes.length) {
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase('pause')
}
}
}
break
case 'pause':
setPhase('deleting')
break
case 'deleting':
if (currentCharIndex > 0) {
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase('typing')
}
break
}
}, timeoutDelay)
return () => clearTimeout(timeout)
}, [
shouldStart,
phase,
currentCharIndex,
currentWordIndex,
displayedText,
wordsToAnimate,
hasMultipleWords,
loop,
typingSpeed,
deletingSpeed,
pauseDelay,
delay,
])
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
const isComplete =
!loop &&
currentWordIndex === wordsToAnimate.length - 1 &&
currentCharIndex >= currentWordGraphemes.length &&
phase !== 'deleting'
const shouldShowCursor =
showCursor &&
!isComplete &&
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => {
switch (cursorStyle) {
case 'block':
return '▌'
case 'underscore':
return '_'
case 'line':
default:
return '|'
}
}
return (
<MotionComponent
ref={elementRef}
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
{...props}
>
{displayedText}
{shouldShowCursor && (
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
{getCursorChar()}
</span>
)}
</MotionComponent>
)
}
@@ -1,24 +1,13 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage } from '@/agent/TabsController.background'
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type === 'TAB_CONTROL') {
return handleTabControlMessage(message, sender, sendResponse)
} else if (message.type === 'PAGE_CONTROL') {
return handlePageControlMessage(message, sender, sendResponse)
} else {
sendResponse({ error: 'Unknown message type' })
return
}
})
// ============================================================================
// Extension Setup
// ============================================================================
import { handleTabControlMessage, setupTabChangeEvents } from '@/agent/TabsController.background'
export default defineBackground(() => {
console.log('[Background] Service Worker started')
// tab change events
setupTabChangeEvents()
// generate user auth token
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
@@ -28,21 +17,20 @@ export default defineBackground(() => {
chrome.storage.local.set({ PageAgentExtUserAuthToken: userAuthToken })
})
// message proxy
chrome.runtime.onMessage.addListener((message, sender, sendResponse): true | undefined => {
if (message.type === 'TAB_CONTROL') {
return handleTabControlMessage(message, sender, sendResponse)
} else if (message.type === 'PAGE_CONTROL') {
return handlePageControlMessage(message, sender, sendResponse)
} else {
sendResponse({ error: 'Unknown message type' })
return
}
})
// setup
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
// Tab change events
chrome.tabs.onCreated.addListener((tab) => {
chrome.runtime.sendMessage({ type: 'TAB_CHANGE', action: 'created', payload: { tab } })
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
chrome.runtime.sendMessage({
type: 'TAB_CHANGE',
action: 'removed',
payload: { tabId, removeInfo },
})
})
})
+7 -18
View File
@@ -6,7 +6,7 @@ const DEBUG_PREFIX = '[Content]'
export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_idle',
runAt: 'document_end',
main() {
console.debug(`${DEBUG_PREFIX} Loaded on ${window.location.href}`)
@@ -69,11 +69,12 @@ async function exposeAgentToPage() {
}
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
@@ -116,17 +117,6 @@ async function exposeAgentToPage() {
)
})
multiPageAgent.addEventListener('dispose', () => {
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_RESPONSE',
id,
action: 'dispose_event',
},
'*'
)
})
// result
const result = await multiPageAgent.execute(task)
@@ -155,9 +145,8 @@ async function exposeAgentToPage() {
break
}
case 'dispose': {
// @note stop ongoing processes but can still be re-used later
multiPageAgent?.dispose()
case 'stop': {
multiPageAgent?.stop()
break
}
@@ -1,35 +1,37 @@
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
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
onDispose?: () => void
}
export type Execute = (
task: string,
llmConfig: LLMConfig,
hooks?: ExecuteHooks
) => Promise<ExecutionResult>
export default defineUnlistedScript(() => {
const w = window as any
let _lastId = 0
function getId() {
_lastId += 1
return _lastId
}
w.execute = async (task: string, llmConfig: LLMConfig, hooks?: ExecuteHooks) => {
const execute: Execute = async (task, config) => {
if (typeof task !== 'string') throw new Error('Task must be a string')
if (task.trim().length === 0) throw new Error('Task cannot be empty')
if (!llmConfig) throw new Error('LLM config is required')
if (!llmConfig.baseURL) throw new Error('LLM config must have a baseURL')
if (!llmConfig.apiKey) throw new Error('LLM config must have an apiKey')
if (!llmConfig.model) throw new Error('LLM config must have a model')
if (!config) throw new Error('Config is required')
if (!config.baseURL) throw new Error('Config must have a baseURL')
if (!config.apiKey) throw new Error('Config must have an apiKey')
if (!config.model) throw new Error('Config must have a model')
const id = getId()
@@ -42,30 +44,25 @@ export default defineUnlistedScript(() => {
// events
if (data.action === 'status_change_event' && hooks?.onStatusChange) {
hooks.onStatusChange(data.payload)
if (data.action === 'status_change_event' && config.onStatusChange) {
config.onStatusChange(data.payload)
return
}
if (data.action === 'activity_event' && hooks?.onActivity) {
hooks.onActivity(data.payload)
if (data.action === 'activity_event' && config.onActivity) {
config.onActivity(data.payload)
return
}
if (data.action === 'history_change_event' && hooks?.onHistoryUpdate) {
hooks.onHistoryUpdate(data.payload)
if (data.action === 'history_change_event' && config.onHistoryUpdate) {
config.onHistoryUpdate(data.payload)
return
}
if (data.action === 'dispose_event' && hooks?.onDispose) {
hooks.onDispose()
return
}
// result
if (data.action !== 'execute_result') return
// execute_result
window.removeEventListener('message', handleMessage)
if (data.error) {
@@ -75,6 +72,7 @@ export default defineUnlistedScript(() => {
}
}
// @note will be removed on dispose or result
window.addEventListener('message', handleMessage)
})
@@ -83,7 +81,15 @@ export default defineUnlistedScript(() => {
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'execute',
payload: { task, llmConfig },
payload: {
task,
config: {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
includeInitialTab: config.includeInitialTab,
},
},
},
'*'
)
@@ -91,16 +97,23 @@ export default defineUnlistedScript(() => {
return promise
}
w.dispose = () => {
const stop = () => {
const id = getId()
window.postMessage(
{
channel: 'PAGE_AGENT_EXT_REQUEST',
id,
action: 'dispose',
action: 'stop',
},
'*'
)
}
;(window as any).PAGE_AGENT_EXT_VERSION = __EXT_VERSION__
;(window as any).PAGE_AGENT_EXT = {
version: __EXT_VERSION__,
execute,
stop,
}
})
@@ -1,4 +1,4 @@
import { Send, Settings, Square } from 'lucide-react'
import { History, Send, Settings, Square } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
@@ -8,20 +8,47 @@ import {
InputGroupButton,
InputGroupTextarea,
} from '@/components/ui/input-group'
import { saveSession } from '@/lib/db'
import { useAgent } from '../../agent/useAgent'
import { ConfigPanel } from './components/ConfigPanel'
import { HistoryDetail } from './components/HistoryDetail'
import { HistoryList } from './components/HistoryList'
import { ActivityCard, EventCard } from './components/cards'
import { EmptyState, Logo, StatusDot } from './components/misc'
import { EmptyState, Logo, MotionOverlay, StatusDot } from './components/misc'
type View =
| { name: 'chat' }
| { name: 'config' }
| { name: 'history' }
| { name: 'history-detail'; sessionId: string }
export default function App() {
const [showConfig, setShowConfig] = useState(false)
const [view, setView] = useState<View>({ name: 'chat' })
const [inputValue, setInputValue] = useState('')
const historyRef = useRef<HTMLDivElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
// Persist session when task finishes
const prevStatusRef = useRef(status)
useEffect(() => {
const prev = prevStatusRef.current
prevStatusRef.current = status
if (
prev === 'running' &&
(status === 'completed' || status === 'error') &&
history.length > 0 &&
currentTask
) {
saveSession({ task: currentTask, history, status }).catch((err) =>
console.error('[SidePanel] Failed to save session:', err)
)
}
}, [status, history, currentTask])
// Auto-scroll to bottom on new events
useEffect(() => {
if (historyRef.current) {
@@ -30,7 +57,7 @@ export default function App() {
}, [history, activity])
const handleSubmit = useCallback(
(e?: React.FormEvent) => {
(e?: React.SyntheticEvent) => {
e?.preventDefault()
if (!inputValue.trim() || status === 'running') return
@@ -56,33 +83,64 @@ export default function App() {
}
}
if (showConfig) {
// --- View routing ---
if (view.name === 'config') {
return (
<ConfigPanel
config={config}
onSave={async (newConfig) => {
await configure(newConfig)
setShowConfig(false)
setView({ name: 'chat' })
}}
onClose={() => setShowConfig(false)}
onClose={() => setView({ name: 'chat' })}
/>
)
}
if (view.name === 'history') {
return (
<HistoryList
onSelect={(id) => setView({ name: 'history-detail', sessionId: id })}
onBack={() => setView({ name: 'chat' })}
/>
)
}
if (view.name === 'history-detail') {
return <HistoryDetail sessionId={view.sessionId} onBack={() => setView({ name: 'history' })} />
}
// --- Chat view ---
const isRunning = status === 'running'
const showEmptyState = !currentTask && history.length === 0 && !isRunning
return (
<div className="flex flex-col h-screen bg-background">
<div className="relative flex flex-col h-screen bg-background">
<MotionOverlay active={isRunning} />
{/* Header */}
<header className="flex items-center justify-between border-b px-3 py-2">
<div className="flex items-center gap-2">
<Logo className="size-5" />
<span className="text-sm font-medium">Page Agent Ext</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<StatusDot status={status} />
<Button variant="ghost" size="icon-sm" onClick={() => setShowConfig(true)}>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setView({ name: 'history' })}
className="cursor-pointer"
>
<History className="size-3.5" />
</Button>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setView({ name: 'config' })}
className="cursor-pointer"
>
<Settings className="size-3.5" />
</Button>
</div>
@@ -142,7 +200,7 @@ export default function App() {
variant="default"
onClick={() => handleSubmit()}
disabled={!inputValue.trim()}
className="size-7"
className="size-7 cursor-pointer"
>
<Send className="size-3" />
</InputGroupButton>
@@ -1,14 +1,15 @@
import type { LLMConfig } from '@page-agent/llms'
import { Copy, Loader2 } from 'lucide-react'
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
interface ConfigPanelProps {
config: LLMConfig | null
onSave: (config: LLMConfig) => Promise<void>
config: ExtConfig | null
onSave: (config: ExtConfig) => Promise<void>
onClose: () => void
}
@@ -16,15 +17,19 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false)
// Update local state when config prop changes
useEffect(() => {
setApiKey(config?.apiKey || DEMO_API_KEY)
setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL)
setLanguage(config?.language)
}, [config])
// Poll for user auth token every second until found
@@ -62,15 +67,25 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const handleSave = async () => {
setSaving(true)
try {
await onSave({ apiKey, baseURL, model })
await onSave({ apiKey, baseURL, model, language })
} finally {
setSaving(false)
}
}
return (
<div className="flex flex-col gap-4 p-4">
<h2 className="text-base font-semibold">Settings</h2>
<div className="flex flex-col gap-4 p-4 relative">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">Settings</h2>
<Button
variant="ghost"
size="icon-sm"
onClick={onClose}
className="absolute top-2 right-3 cursor-pointer"
>
<CornerUpLeft className="size-3.5" />
</Button>
</div>
{/* User Auth Token Section */}
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
@@ -81,9 +96,24 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex gap-2 items-center">
<Input
readOnly
value={userAuthToken || 'Loading...'}
value={
userAuthToken
? showToken
? userAuthToken
: `${userAuthToken.slice(0, 4)}${''.repeat(userAuthToken.length - 8)}${userAuthToken.slice(-4)}`
: 'Loading...'
}
className="text-xs h-8 font-mono bg-background"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowToken(!showToken)}
disabled={!userAuthToken}
>
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
<Button
variant="outline"
size="icon"
@@ -109,7 +139,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label>
<Input
placeholder="gpt-4o"
placeholder="gpt-5.2"
value={model}
onChange={(e) => setModel(e.target.value)}
className="text-xs h-8"
@@ -118,23 +148,111 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">API Key</label>
<Input
type="password"
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
/>
<div className="flex gap-2 items-center">
<Input
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8"
/>
<Button
variant="outline"
size="icon"
className="h-8 w-8 shrink-0 cursor-pointer"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
</Button>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Language</label>
<select
value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
>
<option value="">System</option>
<option value="en-US">English</option>
<option value="zh-CN">中文</option>
</select>
</div>
<div className="flex gap-2 mt-2">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
Cancel
</Button>
<Button onClick={handleSave} disabled={saving} className="flex-1 h-8 text-xs">
<Button
onClick={handleSave}
disabled={saving}
className="flex-1 h-8 text-xs cursor-pointer"
>
{saving ? <Loader2 className="size-3 animate-spin" /> : 'Save'}
</Button>
</div>
{/* Footer */}
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
<div className="flex flex-col justify-between">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<svg role="img" viewBox="0 0 24 24" className="size-3 fill-current">
<path d={siGithub.path} />
</svg>
<span>Source Code</span>
</a>
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<Home className="size-3" />
<span>Home Page</span>
</a>
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 hover:text-foreground"
>
<HatGlasses className="size-3" />
<span>Privacy Policy</span>
</a>
</div>
<div className="flex flex-col items-end">
<span>
Extension <span className="font-mono">v{__EXT_VERSION__}</span>
</span>
<span>
PageAgent <span className="font-mono">v{__CORE_VERSION__}</span>
</span>
</div>
</div>
{/* attribute */}
<div className="text-[10px] text-muted-foreground bg-background fixed bottom-0 w-full flex justify-around">
<span className="leading-loose">
Built with ♥️ by{' '}
<a
href="https://github.com/gaomeng1900"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
@Simon
</a>
</span>
</div>
</div>
)
}
@@ -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>
)
}
@@ -315,9 +315,7 @@ export function EventCard({ event }: { event: HistoricalEvent }) {
<ResultCard
success={input?.success ?? true}
text={input?.text || event.action.output || ''}
>
<RawSection rawRequest={event.rawRequest} rawResponse={event.rawResponse} />
</ResultCard>
/>
</>
)
}
@@ -1,5 +1,10 @@
import type { AgentStatus } from '@page-agent/core'
import { Motion } from 'ai-motion'
import { BookOpen, Globe } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { siGithub } from 'simple-icons'
import { TypingAnimation } from '@/components/ui/typing-animation'
import { cn } from '@/lib/utils'
// Status dot indicator
@@ -19,7 +24,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
}[status]
return (
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 mr-2">
<span
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
/>
@@ -32,14 +37,111 @@ export function Logo({ className }: { className?: string }) {
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
}
// Empty state with logo
// Full-screen ai-motion glow overlay, shown only while running
export function MotionOverlay({ active }: { active: boolean }) {
const containerRef = useRef<HTMLDivElement>(null)
const motionRef = useRef<Motion | null>(null)
useEffect(() => {
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
const motion = new Motion({
mode,
borderWidth: 4,
borderRadius: 14,
glowWidth: mode === 'dark' ? 120 : 60,
styles: { position: 'absolute', inset: '0' },
})
motionRef.current = motion
containerRef.current!.appendChild(motion.element)
motion.autoResize(containerRef.current!)
return () => {
motion.dispose()
motionRef.current = null
}
}, [])
useEffect(() => {
const motion = motionRef.current
if (!motion) return
let disposed = false
if (active) {
motion.start()
motion.fadeIn()
} else {
motion.fadeOut().then(() => !disposed && motion.pause())
}
return () => {
disposed = true
}
}, [active])
return (
<div
ref={containerRef}
className="pointer-events-none absolute inset-0 z-10 opacity-60"
style={{ display: active ? undefined : 'none' }}
/>
)
}
// Empty state with logo and breathing glow
export function EmptyState() {
return (
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-6">
<Logo className="size-20 opacity-80" />
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
<div className="relative select-none pointer-events-none">
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
<Logo className="relative size-20 opacity-80" />
</div>
<div>
<h2 className="text-sm font-medium text-foreground">Page Agent Ext</h2>
<p className="text-xs text-muted-foreground mt-1">Enter a task to automate this page</p>
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
<TypingAnimation
className="text-sm text-muted-foreground"
words={[
'Enter a task to automate this page',
'Execute multi-page tasks',
'Call this extension from your web page',
'Use this extension in your own agents',
]}
cursorStyle="underscore"
loop
typeSpeed={20}
deleteSpeed={10}
pauseDelay={3000}
/>
</div>
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="GitHub"
>
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
<path d={siGithub.path} />
</svg>
</a>
<a
href="https://alibaba.github.io/page-agent/#/docs/features/chrome-extension"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Documentation"
>
<BookOpen className="size-4" />
</a>
<a
href="https://alibaba.github.io/page-agent"
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground transition-colors"
title="Website"
>
<Globe className="size-4" />
</a>
</div>
</div>
)
@@ -6,6 +6,16 @@ import { ErrorBoundary } from './components/ErrorBoundary'
import '@/assets/index.css'
// Sync dark mode with system preference
const syncDarkMode = () => {
document.documentElement.classList.toggle(
'dark',
matchMedia('(prefers-color-scheme: dark)').matches
)
}
syncDarkMode()
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkMode)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>
+70
View File
@@ -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')
}
+2
View File
@@ -0,0 +1,2 @@
declare const __EXT_VERSION__: string
declare const __CORE_VERSION__: string
+11 -5
View File
@@ -1,10 +1,12 @@
import tailwindcss from '@tailwindcss/vite'
import { mkdirSync } from 'node:fs'
import { mkdirSync, readFileSync } from 'node:fs'
import { defineConfig } from 'wxt'
const chromeProfile = '.wxt/chrome-data'
mkdirSync(chromeProfile, { recursive: true })
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
// See https://wxt.dev/api/config.html
export default defineConfig({
srcDir: 'src',
@@ -16,6 +18,10 @@ export default defineConfig({
},
vite: () => ({
plugins: [tailwindcss()],
define: {
__EXT_VERSION__: JSON.stringify(pkg.version),
__CORE_VERSION__: JSON.stringify(pkg.dependencies['@page-agent/core']),
},
optimizeDeps: {
force: true,
},
@@ -32,9 +38,9 @@ export default defineConfig({
},
}),
manifest: {
name: 'Page Agent Ext',
description:
'AI-powered browser automation assistant. Control web pages with natural language.',
default_locale: 'en',
name: '__MSG_extName__',
description: '__MSG_extDescription__',
homepage_url: 'https://alibaba.github.io/page-agent/',
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
host_permissions: ['<all_urls>'],
@@ -42,7 +48,7 @@ export default defineConfig({
64: 'assets/page-agent-64.png',
},
action: {
default_title: 'Open Page Agent',
default_title: '__MSG_extActionTitle__',
},
web_accessible_resources: [
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/llms",
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
+4 -2
View File
@@ -54,8 +54,10 @@ export class OpenAIClient implements LLMClient {
signal: abortSignal,
})
} catch (error: unknown) {
console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
const isAbortError = (error as any)?.name === 'AbortError'
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
if (!isAbortError) console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
}
// 3. Handle HTTP errors
+5 -2
View File
@@ -34,12 +34,15 @@ export class InvokeError extends Error {
super(message)
this.name = 'InvokeError'
this.type = type
this.retryable = this.isRetryable(type)
this.retryable = this.isRetryable(type, rawError)
this.rawError = rawError
this.rawResponse = rawResponse
}
private isRetryable(type: InvokeErrorType): boolean {
private isRetryable(type: InvokeErrorType, rawError?: unknown): boolean {
const isAbortError = (rawError as any)?.name === 'AbortError'
if (isAbortError) return false
const retryableTypes: InvokeErrorType[] = [
InvokeErrorType.NETWORK_ERROR,
InvokeErrorType.RATE_LIMIT,
+3
View File
@@ -49,6 +49,9 @@ export class LLM extends EventTarget {
): Promise<InvokeResult> {
return await withRetry(
async () => {
// in case user aborted before invoking
if (abortSignal.aborted) throw new Error('AbortError')
const result = await this.client.invoke(messages, tools, abortSignal, options)
return result
+5 -1
View File
@@ -2,7 +2,7 @@
* Utility functions for LLM integration
*/
import chalk from 'chalk'
import { z } from 'zod'
import * as z from 'zod'
import type { Tool } from './types'
@@ -74,6 +74,10 @@ export function modelPatch(body: Record<string, any>) {
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
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')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
body.reasoning_effort = 'low'
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "page-agent",
"private": false,
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
@@ -44,11 +44,11 @@
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"@page-agent/core": "1.3.0",
"@page-agent/llms": "1.3.0",
"@page-agent/page-controller": "1.3.0",
"@page-agent/ui": "1.3.0",
"chalk": "^5.6.2",
"zod": "^4.3.5",
"@page-agent/llms": "1.0.0",
"@page-agent/page-controller": "1.0.0",
"@page-agent/core": "1.0.0",
"@page-agent/ui": "1.0.0"
"zod": "^4.3.5"
}
}
+6 -2
View File
@@ -3,6 +3,7 @@ import { config as dotenvConfig } from 'dotenv'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
// import { analyzer } from 'vite-bundle-analyzer'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -14,8 +15,11 @@ dotenvConfig({ path: resolve(__dirname, '../../.env') })
// - alias all local packages so that they can be build in
// - no external
// - no d.ts. dts does not work with monorepo aliasing
export default defineConfig(({ mode }) => ({
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
export default defineConfig(() => ({
plugins: [
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
// analyzer()
],
publicDir: false,
esbuild: {
keepNames: true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/page-controller",
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
@@ -194,7 +194,7 @@ export class PageController extends EventTarget {
interactiveBlacklist: blacklist,
})
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.include_attributes)
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes)
this.selectorMap.clear()
this.selectorMap = dom.getSelectorMap(this.flatTree)
@@ -248,7 +248,7 @@ export class PageController extends EventTarget {
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
return {
success: true,
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opens in a new tab. You are not capable of reading new tabs.`,
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`,
}
}
+3 -3
View File
@@ -10,7 +10,7 @@ import {
export interface DomConfig {
interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[]
include_attributes?: string[]
includeAttributes?: string[]
highlightOpacity?: number
highlightLabelOpacity?: number
}
@@ -117,7 +117,7 @@ interface TreeNode {
*
* @todo 数据脱敏过滤器
*/
export function flatTreeToString(flatTree: FlatDomTree, include_attributes?: string[]): string {
export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string {
const DEFAULT_INCLUDE_ATTRIBUTES = [
'title',
'type',
@@ -146,7 +146,7 @@ export function flatTreeToString(flatTree: FlatDomTree, include_attributes?: str
'aria-owns',
]
const includeAttrs = [...(include_attributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
// Helper function to cap text length
const capTextLength = (text: string, maxLength: number): string => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/ui",
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
+2
View File
@@ -12,6 +12,7 @@ const enUS = {
question: 'Question: {{question}}',
waitingPlaceholder: 'Waiting for task to start...',
stop: 'Stop',
close: 'Close',
expand: 'Expand history',
collapse: 'Collapse history',
step: 'Step {{number}} · {{time}}{{duration}}',
@@ -59,6 +60,7 @@ const zhCN = {
question: '询问: {{question}}',
waitingPlaceholder: '等待任务开始...',
stop: '终止',
close: '关闭',
expand: '展开历史',
collapse: '收起历史',
step: '步骤 {{number}} · {{time}}{{duration}}',
-1
View File
@@ -1,3 +1,2 @@
export { Panel, type PanelConfig } from './panel/Panel'
export type { AgentActivity, PanelAgentAdapter } from './panel/types'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
+22 -9
View File
@@ -33,7 +33,7 @@ export class Panel {
#statusText: HTMLElement
#historySection: HTMLElement
#expandButton: HTMLElement
#stopButton: HTMLElement
#actionButton: HTMLElement
#inputSection: HTMLElement
#taskInput: HTMLInputElement
@@ -76,7 +76,7 @@ export class Panel {
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`)!
this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`)!
this.#stopButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
this.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`)!
this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`)!
this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`)!
@@ -105,6 +105,15 @@ export class Panel {
status === 'running' ? 'thinking' : status === 'idle' ? 'thinking' : status
this.#updateStatusIndicator(indicatorType)
// Morph action button: running = stop (■), not running = close (X)
if (status === 'running') {
this.#actionButton.textContent = '■'
this.#actionButton.title = this.#i18n.t('ui.panel.stop')
} else {
this.#actionButton.textContent = 'X'
this.#actionButton.title = this.#i18n.t('ui.panel.close')
}
// Show/hide based on status
if (status === 'running') {
this.show()
@@ -266,10 +275,14 @@ export class Panel {
}
/**
* Stop Agent
* Action button handler: stop when running, close (dispose) when idle
*/
#stopAgent(): void {
this.#agent.dispose()
#handleActionButton(): void {
if (this.#agent.status === 'running') {
this.#agent.stop()
} else {
this.#agent.dispose()
}
}
/**
@@ -383,7 +396,7 @@ export class Panel {
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
</button>
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.close')}">
X
</button>
</div>
@@ -420,10 +433,10 @@ export class Panel {
this.#toggle()
})
// Stop button
this.#stopButton.addEventListener('click', (e) => {
// Action button (stop / close)
this.#actionButton.addEventListener('click', (e) => {
e.stopPropagation()
this.#stopAgent()
this.#handleActionButton()
})
// Submit on Enter key in input field
+4 -1
View File
@@ -68,6 +68,9 @@ export interface PanelAgentAdapter extends EventTarget {
/** Execute a task */
execute(task: string): Promise<unknown>
/** Dispose the agent */
/** Stop the current task (agent remains reusable) */
stop(): void
/** Dispose the agent (terminal, cannot be reused) */
dispose(): void
}
+11 -18
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/website",
"private": true,
"version": "1.0.0",
"version": "1.3.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
@@ -10,33 +10,26 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.3",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0"
},
"dependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"motion": "^12.29.2",
"motion": "^12.34.0",
"next-themes": "^0.4.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rough-notation": "^0.5.1",
"simple-icons": "^16.6.0",
"simple-icons": "^16.8.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0"
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0",
"wouter": "^3.9.0"
}
}
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useLanguage } from '@/i18n/context'
export default function BetaNotice() {
const { t } = useTranslation('common')
const { isZh } = useLanguage()
return (
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
@@ -11,9 +11,13 @@ export default function BetaNotice() {
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-orange-800 dark:text-orange-200 mb-1">
{t('beta_notice.title')}
{isZh ? 'Beta 阶段' : 'Beta Stage'}
</h3>
<p className="text-sm text-orange-700 dark:text-orange-300">{t('beta_notice.content')}</p>
<p className="text-sm text-orange-700 dark:text-orange-300">
{isZh
? '当前功能未完成,接口可能随时变更。正式版本发布前请勿用于生产环境。'
: 'Current features are incomplete and the API may change at any time. Please do not use in production environments before the official release.'}
</p>
</div>
</div>
</div>
+7 -4
View File
@@ -1,8 +1,9 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
import { useLanguage } from '@/i18n/context'
export default function Footer() {
const { t } = useTranslation('common')
const { isZh } = useLanguage()
return (
<footer
@@ -11,14 +12,16 @@ export default function Footer() {
>
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-gray-600 dark:text-gray-300 text-sm">{t('footer.copyright')}</p>
<p className="text-gray-600 dark:text-gray-300 text-sm">
© 2026 page-agent. All rights reserved.
</p>
<div className="flex items-center space-x-6">
<a
href="https://github.com/alibaba/page-agent"
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"
aria-label={t('footer.github_label')}
aria-label={isZh ? '访问 GitHub 仓库' : 'Visit GitHub repository'}
>
<svg
role="img"
+15 -14
View File
@@ -1,15 +1,16 @@
import { BookOpen, Menu, X } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
import { Link } from 'wouter'
import { useLanguage } from '@/i18n/context'
import LanguageSwitcher from './LanguageSwitcher'
import ThemeSwitcher from './ThemeSwitcher'
import { HyperText } from './ui/hyper-text'
export default function Header() {
const { t } = useTranslation('common')
const { isZh } = useLanguage()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
@@ -24,7 +25,7 @@ export default function Header() {
<Link
href="~/"
className="flex items-center gap-2 sm:gap-3 group shrink-0"
aria-label={t('header.logo_alt')}
aria-label={isZh ? 'page-agent 首页' : 'page-agent home'}
onClick={() => setMobileMenuOpen(false)}
>
<img
@@ -43,7 +44,7 @@ export default function Header() {
animateOnHover={true}
aria-hidden="true"
>
{t('header.slogan')}
GUI Agent in your webpage
</HyperText>
</div>
</Link>
@@ -57,7 +58,7 @@ export default function Header() {
<Link
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"
aria-label={t('header.nav_docs')}
aria-label={isZh ? '文档' : 'Docs'}
>
<BookOpen className="w-5 h-5" />
</Link>
@@ -66,7 +67,7 @@ export default function Header() {
target="_blank"
rel="noopener noreferrer"
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={t('header.nav_source')}
aria-label="GitHub"
>
<svg
role="img"
@@ -83,7 +84,7 @@ export default function Header() {
<nav
className="hidden md:flex items-center space-x-6"
role="navigation"
aria-label={t('header.nav_docs')}
aria-label={isZh ? '文档' : 'Docs'}
>
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
@@ -93,14 +94,14 @@ export default function Header() {
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" />
{t('header.nav_docs')}
{isZh ? '文档' : 'Docs'}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
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"
aria-label={t('header.nav_source')}
aria-label="GitHub"
>
<svg
role="img"
@@ -110,7 +111,7 @@ export default function Header() {
>
<path d={siGithub.path} />
</svg>
{t('header.nav_source')}
GitHub
</a>
<ThemeSwitcher />
<LanguageSwitcher />
@@ -120,7 +121,7 @@ export default function Header() {
<button
type="button"
className="md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0"
aria-label={t('header.mobile_menu')}
aria-label={isZh ? '打开导航栏' : 'Open navigation'}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
@@ -142,14 +143,14 @@ export default function Header() {
onClick={() => setMobileMenuOpen(false)}
>
<BookOpen className="w-5 h-5" />
{t('header.nav_docs')}
{isZh ? '文档' : 'Docs'}
</Link>
<a
href="https://github.com/alibaba/page-agent"
target="_blank"
rel="noopener noreferrer"
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"
aria-label={t('header.nav_source')}
aria-label="GitHub"
>
<svg
role="img"
@@ -159,7 +160,7 @@ export default function Header() {
>
<path d={siGithub.path} />
</svg>
{t('header.nav_source')}
GitHub
</a>
<div className="flex items-center gap-3 px-3 py-2">
<ThemeSwitcher />
@@ -3,6 +3,8 @@
word-break: break-word;
overflow-wrap: break-word;
font-family: monospace;
font-size: 13px;
line-height: 1;
color: #171717;
}
@@ -1,22 +1,21 @@
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLanguage } from '@/i18n/context'
export default function LanguageSwitcher() {
const { i18n, t } = useTranslation('common')
const { language, isZh, setLanguage } = useLanguage()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const currentLang = i18n.language
const languages = [
{ code: 'zh-CN', label: '中文' },
{ code: 'en-US', label: 'English' },
{ code: 'zh-CN' as const, label: '中文' },
{ code: 'en-US' as const, label: 'English' },
]
const currentLanguage = languages.find((lang) => lang.code === currentLang) || languages[0]
const currentLanguage = languages.find((lang) => lang.code === language) || languages[0]
const handleLanguageChange = (langCode: string) => {
i18n.changeLanguage(langCode)
const handleLanguageChange = (langCode: 'zh-CN' | 'en-US') => {
setLanguage(langCode)
setIsOpen(false)
}
@@ -42,7 +41,7 @@ export default function LanguageSwitcher() {
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
aria-label={t('language.switch_label')}
aria-label={isZh ? '切换语言' : 'Switch language'}
aria-expanded={isOpen}
aria-haspopup="true"
>
@@ -83,14 +82,14 @@ export default function LanguageSwitcher() {
key={lang.code}
onClick={() => handleLanguageChange(lang.code)}
className={`w-full flex items-center gap-2 px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${
currentLang === lang.code
language === lang.code
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300'
}`}
role="menuitem"
>
<span>{lang.label}</span>
{currentLang === lang.code && (
{language === lang.code && (
<svg
className="w-4 h-4 ml-auto"
fill="none"
+2 -2
View File
@@ -1,8 +1,8 @@
// Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.0.0/dist/iife/page-agent.demo.js'
'https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js'
export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.0.0/files/dist/iife/page-agent.demo.js'
'https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js'
// Demo LLM for website testing
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
-73
View File
@@ -1,73 +0,0 @@
# 国际化配置说明
本项目使用 `react-i18next` 实现国际化支持。
## 目录结构
```
pages/i18n/
├── config.ts # i18next 配置和初始化
├── types.ts # TypeScript 类型声明
├── locales/
│ ├── zh-CN/ # 中文翻译
│ │ ├── common.json # 通用组件(Header, Footer等)
│ │ ├── home.json # 首页
│ │ └── docs.json # 文档页(待完善)
│ └── en-US/ # 英文翻译
│ ├── common.json
│ ├── home.json
│ └── docs.json
└── README.md # 本文件
```
## 使用方法
### 在组件中使用
```tsx
import { useTranslation } from 'react-i18next'
function MyComponent() {
const { t } = useTranslation('common') // 指定命名空间
return <h1>{t('header.nav_docs')}</h1>
}
```
### 使用多个命名空间
```tsx
const { t } = useTranslation(['home', 'common'])
// 使用时指定命名空间
t('home:hero.title')
t('common:header.nav_docs')
```
## 语言切换
用户可以通过以下方式切换语言:
1. **自动检测**:首次访问根据浏览器语言自动选择
2. **手动切换**:点击页面右上角的语言切换按钮
3. **持久化**:语言选择保存在 `localStorage` 中,刷新后保持
## 添加新翻译
1. 在对应的 JSON 文件中添加翻译条目(如 `zh-CN/home.json`
2. 在对应的英文文件中添加翻译(如 `en-US/home.json`
3. 在组件中使用 `t('namespace:key')` 获取翻译
## TypeScript 支持
`types.ts` 文件提供了类型声明,使得翻译 key 具有:
- 自动补全
- 编译期类型检查
- 防止拼写错误
## 待完成
- [ ] 文档页翻译(`docs.json`
- [ ] DocsLayout 导航结构国际化
- [ ] 404 页面国际化
-45
View File
@@ -1,45 +0,0 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import commonEn from './locales/en-US/common'
import docsEn from './locales/en-US/docs'
import homeEn from './locales/en-US/home'
import commonZh from './locales/zh-CN/common'
import docsZh from './locales/zh-CN/docs'
import homeZh from './locales/zh-CN/home'
const resources = {
'zh-CN': {
common: commonZh,
home: homeZh,
docs: docsZh,
},
'en-US': {
common: commonEn,
home: homeEn,
docs: docsEn,
},
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en-US',
defaultNS: 'common',
// 语言检测配置
detection: {
// localStorage 优先(用户手动选择),其次检测浏览器语言
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
interpolation: {
escapeValue: false, // React 已经做了 XSS 防护
},
})
export default i18n
+35
View File
@@ -0,0 +1,35 @@
import { ReactNode, createContext, use, useState } from 'react'
type Lang = 'en-US' | 'zh-CN'
const LanguageContext = createContext<{
language: Lang
isZh: boolean
setLanguage: (lang: Lang) => void
} | null>(null)
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLang] = useState<Lang>(() => {
const stored = localStorage.getItem('language') as Lang
if (stored === 'zh-CN' || stored === 'en-US') return stored
return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'
})
const setLanguage = (lang: Lang) => {
setLang(lang)
localStorage.setItem('language', lang)
}
return (
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage }}>
{children}
</LanguageContext>
)
}
// eslint-disable-next-line react-refresh/only-export-components
export function useLanguage() {
const ctx = use(LanguageContext)
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider')
return ctx
}
@@ -1,43 +0,0 @@
export default {
header: {
logo_alt: 'page-agent home',
slogan: 'GUI Agent in your webpage',
nav_docs: 'Docs',
nav_source: 'GitHub',
mobile_menu: 'Open navigation',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: 'Visit GitHub repository',
},
beta_notice: {
title: 'Beta Stage',
content:
'Current features are incomplete and the API may change at any time. Please do not use in production environments before the official release.',
},
language: {
zh: '中文',
en: 'English',
switch_label: 'Switch language',
},
nav: {
introduction: 'Introduction',
features: 'Features',
integration: 'Integration',
advanced: 'Advanced',
overview: 'Overview',
quick_start: 'Quick Start',
limitations: 'Limitations',
models: 'Models',
custom_tools: 'Custom Tools',
knowledge_injection: 'Instructions',
data_masking: 'Data Masking',
chrome_extension: 'Chrome Extension',
cdn_setup: 'CDN Setup',
best_practices: 'Best Practices',
third_party_agent: 'Third-party Agent',
security_permissions: 'Security & Permissions',
page_agent: 'PageAgent',
page_agent_core: 'PageAgentCore',
},
}
@@ -1,157 +0,0 @@
export default {
overview: {
title: 'Overview',
subtitle:
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
what_is: 'What is page-agent?',
what_is_desc:
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.',
features_title: 'Core Features',
feature_dom: {
title: '🧠 Smart DOM Analysis',
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.',
},
feature_secure: {
title: '🔒 Secure & Controllable',
desc: 'Supports operation allowlists, data masking protection. Inject custom knowledge to make AI work by your rules.',
},
feature_backend: {
title: '⚡ Zero Backend',
desc: 'CDN or NPM import with custom LLM endpoints.',
},
feature_accessible: {
title: '♿ Accessible Intelligence',
desc: 'Provides natural language interface for complex B2B systems and admin panels. Makes software easy for everyone.',
},
vs_browser_use: 'vs. browser-use',
table_feature: 'Feature',
table_deployment: 'Deployment',
table_deployment_pa: 'Embedded component',
table_deployment_bu: 'External tool',
table_scope: 'Scope',
table_scope_pa: 'Current page',
table_scope_bu: 'Entire browser',
table_user: 'Target Users',
table_user_pa: 'Web developers',
table_user_bu: 'Scraper/Agent developers',
table_scenario: 'Use Case',
table_scenario_pa: 'UX enhancement',
table_scenario_bu: 'Automation tasks',
use_cases_title: 'Use Cases',
use_case1_title: 'Connect Support Bots:',
use_case1_desc:
"Turn your support assistant into a full agent. Customer service bots no longer just say 'Please click the settings button then click...'—they operate for users directly.",
use_case2_title: 'Modernize Legacy Apps:',
use_case2_desc:
'One line of code transforms old apps into agents. Product experts help users navigate complex B2B software. Reduce support costs and improve satisfaction.',
use_case3_title: 'Interactive Training:',
use_case3_desc:
"Demonstrate workflows in real-time. Let AI show the complete process of 'how to submit an expense report.'",
use_case4_title: 'Accessibility:',
use_case4_desc:
'Provide natural language interaction for visually impaired and elderly users. Connect screen readers or voice assistants to make software accessible to everyone.',
get_started_title: '🚀 Get Started',
get_started_desc:
'Ready to add an AI operator to your website? Check our quick start guide for integration in minutes.',
get_started_button: 'Quick Start →',
},
quick_start: {
title: 'Quick Start',
subtitle: 'Integrate page-agent in minutes.',
installation: 'Installation Steps',
step1_title: '1. Import Options',
step1_cdn: 'CDN Import',
step1_npm: 'NPM Install',
step2_title: '2. Initialize Configuration',
step3_title: '3. Start Using',
},
limitations: {
title: 'Limitations',
subtitle: "Understand page-agent's current capabilities and technical constraints",
page_support: 'Page Support Limitations',
spa_limit_title: 'Single Page Application Limits',
spa_limit_1: '• SPA only: Currently operates within a single page',
spa_limit_2: '• Multi-page relay in design: Cannot execute continuous tasks across pages yet',
spa_limit_3: '• Requires integration: Cannot operate on sites without page-agent',
interaction_limits: 'Interaction Limitations',
supported_ops: 'Supported Operations',
op_click: 'Click',
op_input: 'Text input',
op_scroll: 'Scroll',
op_submit: 'Form submit',
op_select: 'Select',
op_focus: 'Focus',
unsupported_ops: 'Unsupported Operations',
op_hover: 'Mouse hover',
op_drag: 'Drag & drop',
op_context: 'Right-click menu',
op_draw: 'Drawing',
op_keyboard: 'Keyboard shortcuts',
op_position: 'Position-based control',
understanding_limits: 'Understanding Limitations',
no_vision_title: 'No Visual Recognition',
no_vision_desc:
'page-agent operates based on DOM structure with no visual recognition. Cannot understand:',
no_vision_1: '• Image content: Cannot recognize text, icons, or visual elements in images',
no_vision_2: '• Canvas: Cannot understand graphics drawn on Canvas',
no_vision_3: '• WebGL 3D: Cannot operate elements in 3D scenes',
no_vision_4: '• SVG graphics: Cannot understand visual content and paths in SVG',
website_requirements: 'Website Requirements',
req_semantic_title: 'Semantic & Usability',
req_semantic_desc:
'All operations rely on semantic tags and attributes. Poor semantic structure or lack of accessibility features may affect AI understanding accuracy.',
req_ux_title: 'UI/UX',
req_ux_desc:
'Counter-intuitive interaction rules, visual-only operation hints, complex mouse interactions, or rapidly appearing/disappearing elements can affect AI understanding and operation.',
req_env_title: 'Environment',
req_env_desc: 'modern browser',
future: 'Future Plans',
future_title: 'Coming Soon',
future_1: '• Multi-page relay capabilities',
future_2: '• Richer mouse interaction support',
future_3: '• Basic visual understanding',
future_4: '• Smarter error recovery',
},
models: {
title: 'Models',
subtitle:
'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.',
recommendation_logic: 'Recommended: Lightweight models with strong ToolCall capabilities.',
recommended: 'Recommended Models',
available: 'Tested Models',
available_verified: 'Verified Working Models',
tips: 'Tips',
tip_1: 'Reasoning models (like GPT-5) are slower with no advantage',
tip_2:
'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended',
tip_3:
'Small models or those unable to handle complex tool definitions typically perform poorly',
security: '🔐 Production Authentication',
security_warning: '⚠️ Never commit real LLM API Keys to your frontend code',
security_desc:
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
security_backend_proxy: 'Backend Proxy Pattern',
security_backend_desc:
'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:',
security_method_1: '• Session/Cookie-based authentication',
security_method_2: '• OIDC (OpenID Connect) single sign-on',
security_method_3: '• Temporary Access Key or JWT Token',
configuration: 'Configuration',
},
custom_tools: {
title: 'Custom Tools',
subtitle:
'Extend AI Agent capabilities by registering custom tools. Use Zod to define strict input schemas for safe business logic calls.',
registration: 'Tool Registration',
registration_desc:
'Each custom tool requires four core properties: name, description, input schema, and execute function.',
page_filter: 'Page Filter',
page_filter_desc:
'Control tool visibility on specific pages via the pageFilter property to enhance security and UX.',
best_practices: 'Best Practices',
bp_performance: '⚡ Performance Optimization',
bp_1: '• Use pageFilter to reduce unnecessary tool loading',
bp_2: '• Implement appropriate caching in execute functions',
bp_3: '• Avoid long-running sync operations in tools',
},
}
@@ -1,82 +0,0 @@
export default {
hero: {
badge: 'GUI Agent in your webpage',
title_line1: 'The AI Operator',
title_line2: 'Living in Your Web App',
subtitle_emoji: '🪄One line of code',
subtitle_main: ' adds intelligent GUI Agents to your website.',
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
tab_try: '🚀 Try It Now',
tab_other: '🌐 Try on Other Sites',
input_placeholder: 'Describe what you want AI to do...',
execute_button: 'Run',
default_task:
'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown',
},
try_other: {
step1_title: 'Step 1:',
step1_content: 'Show your bookmarks bar',
step2_title: 'Step 2:',
step2_content: 'Drag this button to your bookmarks',
cdn_label: 'CDN Source',
cdn_international: 'International',
cdn_china: 'China Mirror',
step3_title: 'Step 3:',
step3_content: 'Click the bookmark on any site to activate',
notice_title: '⚠️ Heads Up',
notice_items: {
item1: 'Demo only—link may expire without notice',
item2: 'This free demo uses DeepSeek API (see their terms and privacy policy)',
item3: 'Some sites block script injection (CSP policies)',
item4: 'Works on single-page apps only—reload required after navigation',
item5: 'Text-only understanding—no image recognition or drag-and-drop',
item6_prefix: 'Full limitations in',
item6_link: 'Docs',
},
},
benefits: {
no_backend: 'Pure Front-end Solution',
private_model: 'Your Own Models',
data_masking: 'Built-in Privacy',
open_source: 'MIT Open Source',
},
features: {
section_title: 'Why PageAgent',
in_page: {
title: 'In-page Solution',
desc: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.',
},
secure_integration: {
title: 'Secure by Design',
desc: 'Control what AI can access with allowlists, data masking, and custom knowledge injection. Your rules, your data.',
},
zero_backend: {
title: 'Zero Backend Setup',
desc: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
},
accessible: {
title: 'Natural Language UI',
desc: 'Transform complex admin panels into chat interfaces. Make powerful tools accessible to everyone, not just experts.',
},
},
use_cases: {
section_title: 'Where It Shines',
section_subtitle: 'From simple forms to complex workflows, AI understands and executes',
case1: {
title: 'Supercharge Support Bots',
desc: '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.',
},
case2: {
title: 'Modernize Legacy Apps',
desc: 'Add AI superpowers to old software without rebuilding. One script tag transforms complex enterprise tools into chat-driven interfaces.',
},
case3: {
title: 'Interactive Walkthroughs',
desc: "Show, don't tell. Let AI demonstrate workflows in real-time—perfect for onboarding or training new users on complex systems.",
},
case4: {
title: 'Accessibility First',
desc: 'Make web apps accessible through natural language. Perfect for screen readers, voice control, or users who find traditional interfaces challenging.',
},
},
}
@@ -1,42 +0,0 @@
export default {
header: {
logo_alt: 'page-agent 首页',
slogan: 'GUI Agent in your webpage',
nav_docs: '文档',
nav_source: 'GitHub',
mobile_menu: '打开导航栏',
},
footer: {
copyright: '© 2025 page-agent. All rights reserved.',
github_label: '访问 GitHub 仓库',
},
beta_notice: {
title: 'Beta 阶段',
content: '当前功能未完成,接口可能随时变更。正式版本发布前请勿用于生产环境。',
},
language: {
zh: '中文',
en: 'English',
switch_label: '切换语言',
},
nav: {
introduction: '介绍',
features: '功能特性',
integration: '集成指南',
advanced: '高级',
overview: '概览',
quick_start: '快速开始',
limitations: '使用限制',
models: '模型',
custom_tools: '自定义工具',
knowledge_injection: '知识注入',
data_masking: '数据脱敏',
chrome_extension: 'Chrome 扩展',
cdn_setup: 'CDN 引入',
best_practices: '最佳实践',
third_party_agent: '接入第三方 Agent',
security_permissions: '安全与权限',
page_agent: 'PageAgent',
page_agent_core: 'PageAgentCore',
},
}
@@ -1,152 +0,0 @@
export default {
overview: {
title: 'Overview',
subtitle:
'page-agent 是一个完全基于 Web 技术的 GUI Agent,简单几步,让你的网站拥有 AI 操作员。',
what_is: '什么是 page-agent',
what_is_desc:
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或Agent客户端开发者;将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
features_title: '核心特性',
feature_dom: {
title: '🧠 智能 DOM 理解',
desc: '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。',
},
feature_secure: {
title: '🔒 安全可控',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
feature_backend: {
title: '⚡ 零后端部署',
desc: 'CDN 或 NPM 引入,自定义 LLM 接入点。',
},
feature_accessible: {
title: '♿ 普惠智能',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
vs_browser_use: '与 browser-use 的区别',
table_feature: '特性',
table_deployment: '部署方式',
table_deployment_pa: '页面内嵌组件',
table_deployment_bu: '外部工具',
table_scope: '操作范围',
table_scope_pa: '当前页面',
table_scope_bu: '整个浏览器',
table_user: '目标用户',
table_user_pa: '网站开发者',
table_user_bu: '爬虫/Agent 开发者',
table_scenario: '使用场景',
table_scenario_pa: '用户体验增强',
table_scenario_bu: '自动化任务',
use_cases_title: '应用场景',
use_case1_title: '对接答疑机器人:',
use_case1_desc:
'把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
use_case2_title: '交互升级/智能化改造:',
use_case2_desc:
'一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
use_case3_title: '产品教学:',
use_case3_desc:
'向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
use_case4_title: '无障碍支持:',
use_case4_desc:
'为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
get_started_title: '🚀 开始使用',
get_started_desc:
'准备好为你的网站添加 AI 操作员了吗?查看我们的快速开始指南,几分钟内完成集成。',
get_started_button: '快速开始 →',
},
quick_start: {
title: 'Quick Start',
subtitle: '几分钟内完成 page-agent 的集成。',
installation: '安装步骤',
step1_title: '1. 引入方式',
step1_cdn: 'CDN 引入',
step1_npm: 'NPM 安装',
step2_title: '2. 初始化配置',
step3_title: '3. 开始使用',
},
limitations: {
title: '使用限制',
subtitle: '了解 page-agent 当前的功能边界和技术限制',
page_support: '页面支持限制',
spa_limit_title: '单页应用限制',
spa_limit_1: '• 仅支持单页应用(SPA):目前只能在单个页面内进行操作',
spa_limit_2: '• 多页接力功能正在设计中:暂时无法跨页面执行连续任务',
spa_limit_3: '• 无法操作未接入该能力的网站:需要目标网站主动集成 page-agent',
interaction_limits: '交互行为限制',
supported_ops: '支持的操作',
op_click: '点击操作',
op_input: '文本输入',
op_scroll: '页面滚动',
op_submit: '表单提交',
op_select: '选择操作',
op_focus: '焦点切换',
unsupported_ops: '不支持的操作',
op_hover: '鼠标悬停(hover',
op_drag: '拖拽操作',
op_context: '右键菜单',
op_draw: '图形绘制',
op_keyboard: '键盘快捷键',
op_position: '基于点击区域或鼠标位置的控制',
understanding_limits: '网页理解限制',
no_vision_title: '无视觉能力',
no_vision_desc: 'page-agent 基于 DOM 结构进行理解和操作,没有视觉识别能力,无法理解以下内容:',
no_vision_1: '• 图片内容:无法识别图片中的文字、图标或视觉元素',
no_vision_2: '• Canvas 画布:无法理解 Canvas 中绘制的图形和内容',
no_vision_3: '• WebGL 3D 内容:无法操作 3D 场景中的元素',
no_vision_4: '• SVG 图形:无法理解 SVG 中的视觉内容和路径',
website_requirements: '被操作网站要求',
req_semantic_title: '语义化和易用性',
req_semantic_desc:
'所有操作都基于 DOM 元素的语义化标签和属性。如果页面结构不够语义化,或者没有任何 accessibility 特性,可能影响 AI 的理解准确性。',
req_ux_title: 'UI/UX',
req_ux_desc:
'反常识的交互规则、基于视觉的操作提示、复杂的鼠标交互、快速出现快速消失的元素等,都会影响 AI 的理解和操作。',
req_env_title: '环境要求',
req_env_desc: 'modern browser',
future: '未来规划',
future_title: '即将支持',
future_1: '• 多页面接力操作能力',
future_2: '• 更丰富的鼠标交互支持',
future_3: '• 基础的视觉理解能力',
future_4: '• 更智能的错误恢复机制',
},
models: {
title: '模型',
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
recommendation_logic: '推荐使用 ToolCall 能力强的轻量级模型。',
recommended: '推荐模型',
available: '已测试模型',
available_verified: '已验证可用模型',
tips: '提示',
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
tip_2:
'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature',
tip_3: '小模型或者无法适应复杂 Tool 定义的模型,通常效果不佳',
security: '🔐 生产环境鉴权建议',
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码',
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key,建议采用以下架构:',
security_backend_proxy: '后端代理转发',
security_backend_desc:
'在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:',
security_method_1: '• Session/Cookie 会话认证',
security_method_2: '• OIDC (OpenID Connect) 单点登录',
security_method_3: '• 临时 Access Key 或 JWT Token',
configuration: '配置方式',
},
custom_tools: {
title: '自定义工具',
subtitle:
'通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义严格的输入接口,让 AI 安全调用你的业务逻辑。',
registration: '工具注册',
registration_desc:
'每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。',
page_filter: '页面过滤器',
page_filter_desc: '通过 pageFilter 属性控制工具在哪些页面可见,提升安全性和用户体验。',
best_practices: '最佳实践',
bp_performance: '⚡ 性能优化',
bp_1: '• 使用 pageFilter 减少不必要的工具加载',
bp_2: '• 在 execute 函数中实现适当的缓存机制',
bp_3: '• 避免在工具中执行耗时的同步操作',
},
}
@@ -1,81 +0,0 @@
export default {
hero: {
badge: 'GUI Agent in your webpage',
title_line1: '让你的 Web 应用',
title_line2: '拥有 AI 操作员',
subtitle_emoji: '🪄一行代码',
subtitle_main: ',为你的网站添加 GUI Agent。',
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
tab_try: '🚀 立即尝试',
tab_other: '🌐 其他网页尝试',
input_placeholder: '输入您想要 AI 执行的任务...',
execute_button: '执行',
default_task: '从导航栏中进入文档页,打开“快速开始”相关的文档,帮我总结成 markdown',
},
try_other: {
step1_title: '步骤 1:',
step1_content: '显示收藏夹栏',
step2_title: '步骤 2:',
step2_content: '拖拽下面按钮到收藏夹栏',
cdn_label: 'CDN 源',
cdn_international: '国际',
cdn_china: '国内镜像',
step3_title: '步骤 3:',
step3_content: '在其他网站点击收藏夹中的按钮即可使用',
notice_title: '⚠️ 注意',
notice_items: {
item1: '仅做技术评估,链接定期失效',
item2: '使用 DeepSeek 模型,参考 DeepSeek 用户协议和隐私政策',
item3: '部分网站屏蔽了链接嵌入,将无反应',
item4: '仅支持单页应用,页面跳转后需要重新注入',
item5: '仅识别文本,不识别图像,不支持拖拽等复杂交互',
item6_prefix: '详细使用限制参照',
item6_link: '《文档》',
},
},
benefits: {
no_backend: '纯前端方案',
private_model: '支持私有模型',
data_masking: '无痛脱敏',
open_source: 'MIT 开源',
},
features: {
section_title: '核心特性',
in_page: {
title: '纯页面内方案',
desc: '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。',
},
secure_integration: {
title: '安全可控集成',
desc: '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。',
},
zero_backend: {
title: '零后端部署',
desc: '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。',
},
accessible: {
title: '普惠智能交互',
desc: '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。',
},
},
use_cases: {
section_title: '应用场景',
section_subtitle: '从简单的表单填写到复杂的业务流程,AI 都能理解并执行',
case1: {
title: '对接答疑机器人',
desc: '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。',
},
case2: {
title: '交互升级/智能化改造',
desc: '一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。',
},
case3: {
title: '产品教学',
desc: '向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。',
},
case4: {
title: '无障碍支持',
desc: '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。',
},
},
}
-16
View File
@@ -1,16 +0,0 @@
import 'react-i18next'
import type commonZh from './locales/zh-CN/common'
import type docsZh from './locales/zh-CN/docs'
import type homeZh from './locales/zh-CN/home'
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'common'
resources: {
common: typeof commonZh
home: typeof homeZh
docs: typeof docsZh
}
}
}
+37 -1
View File
@@ -71,7 +71,43 @@ html.dark,
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Noto Sans',
'Liberation Sans',
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji';
}
/* 文档正文排版优化 */
.prose {
letter-spacing: 0.01em;
font-weight: 380;
}
.prose p {
line-height: 1.6;
}
/* 标题使用中等字重(相对细体更重,但比默认 bold 更轻) */
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
font-weight: 480;
}
/* strong/b 也用中等字重 */
.prose strong,
.prose b {
font-weight: 480;
}
/* 确保文档页面标题在暗色模式下可见 - 只针对 prose 内的标题 */
+6 -5
View File
@@ -2,14 +2,15 @@ import { createRoot } from 'react-dom/client'
import { Router } from 'wouter'
import { useHashLocation } from 'wouter/use-hash-location'
import './i18n/config'
import './i18n/types'
import { LanguageProvider } from './i18n/context'
import { default as PagesRouter } from './router'
import './index.css'
createRoot(document.getElementById('root')!).render(
<Router hook={useHashLocation}>
<PagesRouter />
</Router>
<LanguageProvider>
<Router hook={useHashLocation}>
<PagesRouter />
</Router>
</LanguageProvider>
)
+102 -54
View File
@@ -1,7 +1,6 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import { Bot, Box, MessageSquare, PlayCircle, Shield, Sparkles, Users, Zap } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, useSearchParams } from 'wouter'
import Footer from '../components/Footer'
@@ -18,6 +17,7 @@ import {
DEMO_BASE_URL,
DEMO_MODEL,
} from '../constants'
import { useLanguage } from '../i18n/context'
function getInjection(useCN?: boolean) {
const cdn = useCN ? CDN_DEMO_CN_URL : CDN_DEMO_URL
@@ -40,11 +40,15 @@ function getInjection(useCN?: boolean) {
}
export default function HomePage() {
const { t, i18n } = useTranslation(['home', 'common'])
const [task, setTask] = useState(() => t('home:hero.default_task'))
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
const defaultTask = t('home:hero.default_task')
useEffect(() => {
setTask(defaultTask)
}, [defaultTask])
@@ -66,7 +70,7 @@ export default function HomePage() {
win.pageAgent = new PageAgent({
// 把 react 根元素排除掉,挂了很多冒泡时间导致假阳
interactiveBlacklist: [document.getElementById('root')!],
language: i18n.language as any,
language: language,
instructions: {
system: 'You are a helpful assistant on PageAgent website.',
@@ -131,7 +135,7 @@ export default function HomePage() {
aria-hidden="true"
></span>
<AnimatedGradientText colorFrom="#3b82f6" colorTo="#8b5cf6">
{t('home:hero.badge')}
GUI Agent in your webpage
</AnimatedGradientText>
</div>
@@ -139,20 +143,24 @@ export default function HomePage() {
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"
>
{t('home:hero.title_line1')}
{isZh ? '让你的 Web 应用' : 'The AI Operator'}
<br />
{t('home:hero.title_line2')}
{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">
{t('home:hero.subtitle_emoji')}
{isZh ? '🪄一行代码' : '🪄One line of code'}
</span>
</Highlighter>
{t('home:hero.subtitle_main')}
{isZh
? ',为你的网站添加 GUI Agent。'
: ' adds intelligent GUI Agents to your website.'}
<br />
{t('home:hero.subtitle_detail')}
{isZh
? '用户/答疑机器人给出文字指示,AI 帮你操作页面。'
: 'Users give natural language commands, AI handles the rest.'}
</p>
{/* Try It Now Section - Tab Card */}
@@ -173,7 +181,7 @@ export default function HomePage() {
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{t('home:hero.tab_try')}
{isZh ? '🚀 立即尝试' : '🚀 Try It Now'}
</button>
<button
onClick={() => setActiveTab('other')}
@@ -183,7 +191,7 @@ export default function HomePage() {
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
{t('home:hero.tab_other')}
{isZh ? '🌐 其他网页尝试' : '🌐 Try on Other Sites'}
</button>
</div>
@@ -195,7 +203,11 @@ export default function HomePage() {
<input
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder={t('home:hero.input_placeholder')}
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
/>
@@ -206,7 +218,7 @@ export default function HomePage() {
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
>
{t('home:hero.execute_button')}
{isZh ? '执行' : 'Run'}
</button>
</div>
</div>
@@ -220,15 +232,17 @@ export default function HomePage() {
<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">
{t('home:try_other.step1_title')}
{isZh ? '步骤 1:' : 'Step 1:'}
</span>{' '}
{t('home:try_other.step1_content')}
{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"></span>
<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>
@@ -239,9 +253,11 @@ export default function HomePage() {
<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">
{t('home:try_other.step2_title')}
{isZh ? '步骤 2:' : 'Step 2:'}
</span>{' '}
{t('home:try_other.step2_content')}
{isZh
? '拖拽下面按钮到收藏夹栏'
: 'Drag this button to your bookmarks'}
</p>
<div className="flex items-center justify-center gap-3">
<select
@@ -252,9 +268,11 @@ export default function HomePage() {
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">
{t('home:try_other.cdn_international')}
{isZh ? '国际' : 'International'}
</option>
<option value="china">
{isZh ? '国内镜像' : 'China Mirror'}
</option>
<option value="china">{t('home:try_other.cdn_china')}</option>
</select>
<div
dangerouslySetInnerHTML={{
@@ -268,9 +286,11 @@ export default function HomePage() {
<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">
{t('home:try_other.step3_title')}
{isZh ? '步骤 3:' : 'Step 3:'}
</span>{' '}
{t('home:try_other.step3_content')}
{isZh
? '在其他网站点击收藏夹中的按钮即可使用'
: 'Click the bookmark on any site to activate'}
</p>
</div>
</div>
@@ -278,37 +298,47 @@ export default function HomePage() {
{/* 右侧:注意事项 */}
<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">
{t('home:try_other.notice_title')}
{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>
{t('home:try_other.notice_items.item1')}
{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>
{t('home:try_other.notice_items.item2')}
{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>
{t('home:try_other.notice_items.item3')}
{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>
{t('home:try_other.notice_items.item4')}
{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>
{t('home:try_other.notice_items.item5')}
{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>
{t('home:try_other.notice_items.item6_prefix')}{' '}
{isZh ? '详细使用限制参照' : 'Full limitations in'}{' '}
<Link
href="/docs/introduction/limitations"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
{t('home:try_other.notice_items.item6_link')}
{isZh ? '《文档》' : 'Docs'}
</Link>
</li>
</ul>
@@ -329,28 +359,28 @@ export default function HomePage() {
className="w-2 h-2 bg-green-500 rounded-full mr-2"
aria-hidden="true"
></span>
{t('home:benefits.no_backend')}
{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>
{t('home:benefits.private_model')}
{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>
{t('home:benefits.data_masking')}
{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>
{t('home:benefits.open_source')}
{isZh ? 'MIT 开源' : 'MIT Open Source'}
</li>
</ul>
</div>
@@ -376,10 +406,12 @@ export default function HomePage() {
<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">
{t('home:features.in_page.title')}
{isZh ? '纯页面内方案' : 'In-page Solution'}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.in_page.desc')}
{isZh
? '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。'
: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.'}
</p>
</article>
@@ -395,10 +427,12 @@ export default function HomePage() {
<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">
{t('home:features.zero_backend.title')}
{isZh ? '零后端部署' : 'Zero Backend Setup'}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.zero_backend.desc')}
{isZh
? '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。'
: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.'}
</p>
</article>
@@ -414,10 +448,12 @@ export default function HomePage() {
<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">
{t('home:features.accessible.title')}
{isZh ? '普惠智能交互' : 'Natural Language UI'}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.accessible.desc')}
{isZh
? '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。'
: 'Transform complex admin panels into chat interfaces. Make powerful tools accessible to everyone, not just experts.'}
</p>
</article>
@@ -433,10 +469,12 @@ export default function HomePage() {
<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">
{t('home:features.secure_integration.title')}
{isZh ? '安全可控集成' : 'Secure by Design'}
</h3>
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
{t('home:features.secure_integration.desc')}
{isZh
? '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。'
: 'Control what AI can access with allowlists, data masking, and custom knowledge injection. Your rules, your data.'}
</p>
</article>
</div>
@@ -451,10 +489,12 @@ export default function HomePage() {
className="text-4xl lg:text-5xl mb-6"
colors={{ first: '#3b82f6', second: '#8b5cf6' }}
>
{t('home:use_cases.section_title')}
{isZh ? '应用场景' : 'Where It Shines'}
</SparklesText>
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
{t('home:use_cases.section_subtitle')}
{isZh
? '从简单的表单填写到复杂的业务流程,AI 都能理解并执行'
: 'From simple forms to complex workflows, AI understands and executes'}
</p>
</div>
@@ -468,10 +508,12 @@ export default function HomePage() {
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case1.title')}
{isZh ? '对接答疑机器人' : 'Supercharge Support Bots'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case1.desc')}
{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>
@@ -485,10 +527,12 @@ export default function HomePage() {
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case2.title')}
{isZh ? '交互升级/智能化改造' : 'Modernize Legacy Apps'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case2.desc')}
{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>
@@ -502,10 +546,12 @@ export default function HomePage() {
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case3.title')}
{isZh ? '产品教学' : 'Interactive Walkthroughs'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case3.desc')}
{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>
@@ -519,10 +565,12 @@ export default function HomePage() {
</div>
<div>
<h3 className="text-xl font-bold mb-2 text-gray-900 dark:text-white">
{t('home:use_cases.case4.title')}
{isZh ? '无障碍支持' : 'Accessibility First'}
</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('home:use_cases.case4.desc')}
{isZh
? '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。'
: 'Make web apps accessible through natural language. Perfect for screen readers, voice control, or users who find traditional interfaces challenging.'}
</p>
</div>
</div>
+61 -34
View File
@@ -1,7 +1,10 @@
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { siGooglechrome } from 'simple-icons'
import { Link, useLocation } from 'wouter'
import { SparklesText } from '@/components/ui/sparkles-text'
import { useLanguage } from '@/i18n/context'
interface DocsLayoutProps {
children: ReactNode
}
@@ -17,45 +20,50 @@ interface NavSection {
}
export default function DocsLayout({ children }: DocsLayoutProps) {
const { t } = useTranslation('common')
const { isZh } = useLanguage()
const [location] = useLocation()
const navigationSections: NavSection[] = [
{
title: t('nav.introduction'),
title: isZh ? '介绍' : 'Introduction',
items: [
{ title: t('nav.overview'), path: '/introduction/overview' },
{ title: t('nav.quick_start'), path: '/introduction/quick-start' },
{ title: t('nav.limitations'), path: '/introduction/limitations' },
],
},
{
title: t('nav.features'),
items: [
{ title: t('nav.models'), path: '/features/models' },
{ title: t('nav.custom_tools'), path: '/features/custom-tools' },
{ title: t('nav.knowledge_injection'), path: '/features/custom-instructions' },
{ title: t('nav.data_masking'), path: '/features/data-masking' },
{ title: t('nav.chrome_extension'), path: '/features/chrome-extension' },
],
},
{
title: t('nav.integration'),
items: [
{ title: t('nav.third_party_agent'), path: '/integration/third-party-agent' },
{ title: t('nav.cdn_setup'), path: '/integration/cdn-setup' },
{ title: isZh ? '概览' : 'Overview', path: '/introduction/overview' },
{ title: isZh ? '快速开始' : 'Quick Start', path: '/introduction/quick-start' },
{ title: isZh ? '使用限制' : 'Limitations', path: '/introduction/limitations' },
{
title: '🚧 ' + t('nav.security_permissions'),
title: isZh ? '故障排查' : 'Troubleshooting',
path: '/introduction/troubleshooting',
},
{
title: '🚧 ' + (isZh ? '最佳实践' : 'Best Practices'),
path: '/integration/best-practices',
},
],
},
{
title: isZh ? '功能特性' : 'Features',
items: [
{ title: isZh ? '模型' : 'Models', path: '/features/models' },
{ title: isZh ? '自定义工具' : 'Custom Tools', path: '/features/custom-tools' },
{ title: isZh ? '知识注入' : 'Instructions', path: '/features/custom-instructions' },
{ title: isZh ? '数据脱敏' : 'Data Masking', path: '/features/data-masking' },
{ title: isZh ? 'Chrome 扩展' : 'Chrome Extension', path: '/features/chrome-extension' },
{
title: isZh ? '接入第三方 Agent' : 'Third-party Agent',
path: '/integration/third-party-agent',
},
],
},
{
title: isZh ? '高级' : 'Advanced',
items: [
{ title: 'PageAgent', path: '/advanced/page-agent' },
{ title: 'PageAgentCore', path: '/advanced/page-agent-core' },
{ title: isZh ? '自定义 UI' : 'Custom UI', path: '/advanced/custom-ui' },
{
title: '🚧 ' + (isZh ? '安全与权限' : 'Security & Permissions'),
path: '/integration/security-permissions',
},
{ title: '🚧 ' + t('nav.best_practices'), path: '/integration/best-practices' },
],
},
{
title: t('nav.advanced'),
items: [
{ title: t('nav.page_agent'), path: '/advanced/page-agent' },
{ title: t('nav.page_agent_core'), path: '/advanced/page-agent-core' },
],
},
]
@@ -75,6 +83,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
<ul className="space-y-2" role="list">
{section.items.map((item) => {
const isActive = location === item.path
const isChromeExtension = item.path === '/features/chrome-extension'
return (
<li key={item.path}>
<Link
@@ -86,7 +95,25 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
}`}
aria-current={isActive ? 'page' : undefined}
>
{item.title}
{isChromeExtension ? (
<span className="flex items-center gap-1.5">
<svg
className="w-3.5 h-3.5 shrink-0"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d={siGooglechrome.path} fill="currentColor" />
</svg>
<SparklesText
className="text-[length:inherit] font-[inherit] font-medium"
sparklesCount={3}
>
{item.title}
</SparklesText>
</span>
) : (
item.title
)}
</Link>
</li>
)
@@ -100,7 +127,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
{/* Main Content */}
<main className="flex-1 min-w-0" id="main-content" role="main">
<div className="prose prose-lg dark:prose-invert max-w-none">{children}</div>
<div className="prose dark:prose-invert max-w-none">{children}</div>
</main>
</div>
</div>
@@ -0,0 +1,272 @@
import CodeEditor from '@/components/CodeEditor'
import { APIDivider, APIReference } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context'
export default function CustomUIDocs() {
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6">{isZh ? '自定义 UI' : 'Custom UI'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? 'PageAgent 的核心逻辑(PageAgentCore)和 UI 完全解耦,通过事件通讯。你可以用自己的 UI 替换内置 Panel。'
: 'PageAgent core logic (PageAgentCore) is fully decoupled from UI through events. You can replace the built-in Panel with your own UI.'}
</p>
{/* Architecture */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? '架构' : 'Architecture'}</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? 'PageAgent 由三个独立模块组成,可自由组合:'
: 'PageAgent consists of three independent modules that can be freely combined:'}
</p>
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
<li>
<strong>PageAgentCore</strong> -{' '}
{isZh ? '核心 Agent 逻辑,不包含 UI' : 'Core agent logic, no UI'}
</li>
<li>
<strong>PageController</strong> -{' '}
{isZh ? 'DOM 操作和视觉反馈' : 'DOM operations and visual feedback'}
</li>
<li>
<strong>UI (Panel)</strong> -{' '}
{isZh
? '用户界面,可替换为自定义实现'
: 'User interface, replaceable with custom implementation'}
</li>
</ul>
</section>
<APIDivider title={isZh ? '事件系统' : 'Event System'} />
{/* Two Event Streams */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? '两个事件流' : 'Two Event Streams'}</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? 'PageAgentCore 提供两种不同性质的事件流,方便 UI 渲染:'
: 'PageAgentCore provides two distinct event streams for UI rendering:'}
</p>
{/* Comparison Table */}
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
<thead>
<tr className="bg-gray-100 dark:bg-gray-800">
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left"></th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left">
Historical Events
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left">
Activity Events
</th>
</tr>
</thead>
<tbody>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '事件名' : 'Event Name'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
<code>historychange</code>
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
<code>activity</code>
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '持久性' : 'Persistence'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '持久化到 agent.history' : 'Persisted in agent.history'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '瞬态' : 'Transient'}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '传给 LLM' : 'Sent to LLM'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '是' : 'Yes'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '否' : 'No'}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '用途' : 'Purpose'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh ? '构成 Agent 记忆,显示历史步骤' : 'Forms agent memory, displays history'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2">
{isZh
? '实时 UI 反馈(如 loading 状态)'
: 'Real-time UI feedback (e.g., loading state)'}
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* All Events */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? '所有事件' : 'All Events'}</h2>
<APIReference
properties={[
{
name: 'statuschange',
type: 'Event',
description: isZh
? 'Agent 状态变化 (idle → running → completed/error)'
: 'Agent status changes (idle → running → completed/error)',
},
{
name: 'historychange',
type: 'Event',
description: isZh
? '历史事件更新,读取 agent.history 获取完整历史'
: 'History updated, read agent.history for full history',
},
{
name: 'activity',
type: 'CustomEvent<AgentActivity>',
description: isZh
? '实时活动反馈:thinking, executing, executed, retrying, error'
: 'Real-time activity: thinking, executing, executed, retrying, error',
},
{
name: 'dispose',
type: 'Event',
description: isZh ? 'Agent 被销毁' : 'Agent is disposed',
},
]}
/>
</section>
{/* HistoricalEvent Types */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">HistoricalEvent</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh ? 'agent.history 数组中的事件类型:' : 'Event types in agent.history array:'}
</p>
<CodeEditor
language="typescript"
code={`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 }`}
/>
</section>
{/* AgentActivity Types */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">AgentActivity</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh ? 'activity 事件的 detail 类型:' : 'The detail type of activity events:'}
</p>
<CodeEditor
language="typescript"
code={`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 }`}
/>
</section>
<APIDivider title={isZh ? 'React 示例' : 'React Example'} />
{/* React Hooks Example */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '使用 React Hooks' : 'Using React Hooks'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh ? '监听事件并更新 React 状态:' : 'Listen to events and update React state:'}
</p>
<CodeEditor
language="tsx"
code={`function useAgent(agent: PageAgentCore) {
const [status, setStatus] = useState(agent.status)
const [history, setHistory] = useState(agent.history)
const [activity, setActivity] = useState<AgentActivity | null>(null)
useEffect(() => {
const onStatus = () => setStatus(agent.status)
const onHistory = () => setHistory([...agent.history])
const onActivity = (e: Event) => setActivity((e as CustomEvent).detail)
agent.addEventListener('statuschange', onStatus)
agent.addEventListener('historychange', onHistory)
agent.addEventListener('activity', onActivity)
return () => {
agent.removeEventListener('statuschange', onStatus)
agent.removeEventListener('historychange', onHistory)
agent.removeEventListener('activity', onActivity)
}
}, [agent])
return { status, history, activity }
}`}
/>
</section>
<APIDivider title={isZh ? '完整组装示例' : 'Complete Assembly Example'} />
{/* Assembly Example */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '组装 Core + Controller + 自定义 UI' : 'Assembling Core + Controller + Custom UI'}
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '参考内置 PageAgent 的实现方式,用自定义 UI 替换 Panel'
: 'Following the built-in PageAgent pattern, replace Panel with custom UI:'}
</p>
<CodeEditor
language="typescript"
code={`import { PageAgentCore } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller'
// 1. Create PageController
const pageController = new PageController({ enableMask: true })
// 2. Create PageAgentCore with controller
const agent = new PageAgentCore({
pageController,
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
})
// 3. Mount your custom UI
const root = createRoot(document.getElementById('my-ui')!)
root.render(<MyAgentUI agent={agent} />)
// 4. Handle user input (optional)
agent.onAskUser = async (question) => window.prompt(question) || ''
// 5. Execute task
await agent.execute('Fill the form with test data')
// 6. Cleanup
agent.dispose()`}
/>
</section>
</div>
)
}
@@ -1,11 +1,9 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
import { APIDivider, APIReference, TypeRef } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context'
export default function PageAgentCoreDocs() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
@@ -309,7 +307,7 @@ const result = await agent.execute('Fill in the form with test data')`}
: 'Elements to force include for interaction',
},
{
name: 'include_attributes',
name: 'includeAttributes',
type: 'string[]',
description: isZh
? '在 DOM 提取中包含的额外属性'
@@ -378,13 +376,6 @@ const result = await agent.execute('Fill in the form with test data')`}
? '执行任务并返回结果。包含 success、data 和 history 字段。'
: 'Execute a task and return result. Contains success, data, and history fields.',
},
{
name: 'pushObservation(content: string)',
type: 'void',
description: isZh
? '向历史流推送一个观察事件,会在下一步时被 LLM 看到'
: 'Push an observation to history stream, will be seen by LLM in next step',
},
{
name: 'dispose(reason?: string)',
type: 'void',
@@ -1,12 +1,11 @@
import { useTranslation } from 'react-i18next'
import { Link } from 'wouter'
import CodeEditor from '@/components/CodeEditor'
import { APIReference, TypeRef } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context'
export default function PageAgentDocs() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
@@ -193,9 +192,7 @@ agent.panel.dispose()`}
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800/50">
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
{isZh ? '特性' : 'Feature'}
</th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300"></th>
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-300">
PageAgent
</th>
@@ -1,12 +1,13 @@
import { useTranslation } from 'react-i18next'
import { siGithub } from 'simple-icons'
import { siChromewebstore, siGithub } from 'simple-icons'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function ChromeExtension() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
const chromeWebStoreUrl =
'https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj'
const githubReleasesUrl = 'https://github.com/alibaba/page-agent/releases'
return (
<div>
@@ -14,70 +15,92 @@ export default function ChromeExtension() {
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? '可选的 Chrome 扩展,解锁多页任务和第三方 API 集成。'
: 'Optional Chrome extension that unlocks multi-page tasks and third-party API integration.'}
? '可选的 Chrome 扩展。PageAgent.js 继续负责页面内自动化;扩展 API 额外提供多页面任务、浏览器级控制,以及从浏览器外部发起任务的能力。'
: 'An optional Chrome extension. PageAgent.js keeps handling in-page automation, while the extension API adds multi-page tasks, browser-level control, and tasks initiated from outside the browser.'}
</p>
<BetaNotice />
<div className="space-y-8 mt-8">
{/* Hero Section */}
<section className="p-6 bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl">
<div className="flex items-start gap-4">
<div>
<p className="text-gray-600 dark:text-gray-300">
{isZh
? '解锁多页任务!借助 Chrome 扩展,Agent 可以跨标签页和页面导航,突破单页限制。'
: 'Unlock multi-page tasks! With the Chrome extension, your agent can navigate across tabs and pages, breaking the single-page limitation.'}
</p>
</div>
</div>
</section>
{/* Features */}
<section>
<h2 className="text-2xl font-bold mb-4">{isZh ? '核心特性' : 'Key Features'}</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="grid md:grid-cols-3 gap-4">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="font-semibold mb-2">🔓 {isZh ? '多页任务' : 'Multi-Page Tasks'}</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{isZh
? '跨多个页面和标签页执行任务,不再限于单页操作。'
: 'Execute tasks across multiple pages and tabs. No longer limited to single-page operations.'}
? '跨多个页面和标签页连续执行任务,不再限于单页上下文。'
: 'Run tasks across multiple pages and tabs without being limited to a single page context.'}
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="font-semibold mb-2">
🔌 {isZh ? '开放第三方接口' : 'Third-Party API'}
🧭 {isZh ? '浏览器级控制' : 'Browser-Level Control'}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{isZh
? '用户授权后,你的网页、本地 Agent 或云端 Agent 都能通过扩展操作用户浏览器!'
: 'After user authorization, your webpage, local agent, or cloud agent can control the browser through the extension.'}
? '支持跨标签导航、页面切换和更完整的浏览器自动化能力。'
: 'Enable richer browser automation, including cross-tab navigation and page switching.'}
</p>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<h3 className="font-semibold mb-2">
🔌 {isZh ? '开放集成接口' : 'Open Integration API'}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm">
{isZh
? '用户主动授权后,页面 JS、本地 Agent 或云端 Agent 可通过扩展发起多页面任务。'
: 'With explicit user authorization, page JS, local agents, or cloud agents can trigger multi-page tasks through the extension.'}
</p>
</div>
</div>
</section>
{/* Download */}
{/* Install */}
<section>
<h2 className="text-2xl font-bold mb-4">{isZh ? '下载测试版' : 'Download Beta'}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '扩展目前处于 Beta 阶段,请从 GitHub Releases 下载最新版本。'
: 'The extension is currently in beta. Download the latest version from GitHub Releases.'}
</p>
<a
href="https://github.com/alibaba/page-agent/releases"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
</svg>
{isZh ? '前往 GitHub Releases 下载' : 'Download from GitHub Releases'}
</a>
<h2 className="text-2xl font-bold mb-4">{isZh ? '获取扩展' : 'Get the Extension'}</h2>
<div className="flex flex-wrap gap-3">
<a
href={chromeWebStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white! font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siChromewebstore.path} />
</svg>
{isZh ? '从 Chrome 应用商店安装' : 'Install from Chrome Web Store'}
</a>
<a
href={githubReleasesUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
</svg>
{isZh ? 'GitHub Releases(更新版本)' : 'GitHub Releases (faster updates)'}
</a>
</div>
</section>
{/* Relationship with PageAgent.js */}
<section>
<h2 className="text-2xl font-bold mb-4">
{isZh ? '与 PageAgent.js 的关系' : 'How It Relates to PageAgent.js'}
</h2>
<div className="p-5 bg-gray-50 dark:bg-gray-800 rounded-lg space-y-3 text-gray-600 dark:text-gray-300">
<p>
{isZh
? 'PageAgent.js 本身即可在页面内完成自动化。Chrome 扩展是可选的能力扩展。'
: 'PageAgent.js already works for in-page automation. The Chrome extension is optional, not a dependency.'}
</p>
<p>
{isZh
? '通过扩展,你可以执行多页面任务、控制浏览器,以及从浏览器外部(本地服务或云端服务)发起任务。'
: 'With the extension, you can perform multi-page tasks, browser-level control, and tasks triggered outside the browser (local or cloud services).'}
</p>
</div>
</section>
{/* Third-party Integration */}
@@ -87,32 +110,33 @@ export default function ChromeExtension() {
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '用户授权后,外部应用可以调用扩展 API 来控制浏览器。'
: 'After user authorization, external applications can call the extension API to control the browser.'}
? '通过页面 JavaScript 调用 `window.PAGE_AGENT_EXT`,你的应用可以发起跨页面任务并控制浏览器行为。'
: 'By calling `window.PAGE_AGENT_EXT` from page JavaScript, your app can trigger multi-page tasks and control browser behavior.'}
</p>
{/* Auth Flow */}
<h3 className="text-xl font-semibold mb-3">{isZh ? '授权流程' : 'Authorization Flow'}</h3>
<h3 className="text-xl font-semibold mb-3">
{isZh ? '授权与安全' : 'Authorization and Security'}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '扩展使用基于 Token 的授权机制,扩展端和页面端必须持有匹配的 Token。'
: 'The extension uses a token-based authorization mechanism. Both extension and page must have matching tokens.'}
? '扩展权限范围较广(例如页面访问、导航、多标签控制)。若被滥用,可能危害用户隐私。为此,调用能力由 Token 保护,用户必须主动将 Token 提供给其信任的应用。'
: 'The extension has broad permissions (such as page access, navigation, and multi-tab control). If abused, it can harm user privacy. That is why access is protected by a token, and users must actively share the token only with applications they trust.'}
</p>
<CodeEditor
code={
isZh
? `// 1. 用户安装扩展并在扩展设置中配置 auth token
// 2. 你的页面读取相同的 token 并存入 localStorage
// 3. Token 匹配后,扩展会暴露 window.execute() 和 window.dispose()
? `// 1) 用户在扩展侧边栏获取 auth token
// 2) 仅在可信应用中设置该 token
// 3) token 匹配后,扩展会暴露 window.PAGE_AGENT_EXT
// ⚠️ 请在扩展弹窗中查看你的 auth token,然后填入下方
// ⚠️ 不要把 token 提供给不可信页面或脚本
localStorage.setItem('PageAgentExtUserAuthToken', '<从扩展中获取的-token>')`
: `// 1. User installs extension and sets an auth token in extension settings
// 2. Your page reads the same token and stores it in localStorage
// 3. After token match, extension exposes window.execute() and window.dispose()
: `// 1) Get auth token from the extension side panel
// 2) Set it only in trusted applications
// 3) After token match, extension exposes window.PAGE_AGENT_EXT
// ⚠️ Check your extension popup for the auth token
// ⚠️ Never provide the token to untrusted pages or scripts
localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')`
}
language="javascript"
@@ -123,46 +147,136 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
<section>
<h2 className="text-2xl font-bold mb-4">{isZh ? 'API 参考' : 'API Reference'}</h2>
<h3 className="text-xl font-semibold mb-3">window.execute(task, llmConfig, hooks?)</h3>
{/* AI Assistant Instructions */}
<section className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-purple-900 dark:text-purple-300 mb-2">
🤖 {isZh ? '给 AI 编程助手的文档' : 'Instructions for Your AI Assistant'}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-3 text-sm">
{isZh
? '如果你在使用 AI 编程助手(如 Cursor、GitHub Copilot),可以将以下文档链接提供给它,让它更好地理解和使用 Page Agent 扩展 API'
: 'If you are using an AI coding assistant (like Cursor, GitHub Copilot), share these documentation links with it for better understanding of Page Agent Extension API:'}
</p>
<div className="space-y-2">
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/docs/extension_api.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'}
</a>
</div>
</section>
{/* TypeScript Declaration */}
<h2 className="text-2xl font-bold mb-4">
{isZh ? 'TypeScript 类型声明' : 'TypeScript Declaration'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '使用 LLM 配置执行任务。返回一个 Promise,在任务完成时 resolve。可选的 hooks 参数用于监听任务执行过程中的事件。'
: 'Execute a task with LLM configuration. Returns a Promise that resolves when the task completes. Optional hooks parameter for listening to events during task execution.'}
? '推荐把 `execute` 的类型声明加入你的项目,获得完整类型提示。'
: 'Add this `execute` declaration to your project for full type support.'}
</p>
<CodeEditor
code={
isZh
? `// 使用 LLM 配置和 hooks 执行任务
const result = await window.execute(
? `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(
'在 GitHub 上搜索 "page-agent" 并打开第一个结果',
{
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5-2'
},
{
model: 'gpt-5.2',
// includeInitialTab: false, // 设为 false 排除初始标签页
onStatusChange: status => console.log('状态变化:', status),
onActivity: activity => console.log('活动:', activity),
onHistoryUpdate: history => console.log('历史更新:', history),
onDispose: () => console.log('已停止')
onHistoryUpdate: history => console.log('历史更新:', history)
}
)
console.log(result) // 任务执行结果`
: `// Execute a task with LLM configuration and hooks
const result = await window.execute(
: `// Execute a task with configuration
const result = await window.PAGE_AGENT_EXT.execute(
'Search for "page-agent" on GitHub and open the first result',
{
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5-2'
},
{
model: 'gpt-5.2',
// includeInitialTab: false, // Set to false to exclude initial tab
onStatusChange: status => console.log('Status change:', status),
onActivity: activity => console.log('Activity:', activity),
onHistoryUpdate: history => console.log('History update:', history),
onDispose: () => console.log('Disposed')
onHistoryUpdate: history => console.log('History update:', history)
}
)
@@ -171,138 +285,47 @@ console.log(result) // Task execution result`
language="javascript"
/>
<h3 className="text-xl font-semibold mt-6 mb-3">window.dispose()</h3>
<h3 className="text-xl font-semibold mt-6 mb-3">PAGE_AGENT_EXT.stop()</h3>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '停止当前正在运行的任务。停止后 Agent 可以重新使用。'
: 'Stop the current running task. The agent can be reused after disposal.'}
{isZh ? '停止当前正在运行的任务。' : 'Stop the current running task.'}
</p>
<CodeEditor
code={
isZh
? `// 停止当前任务
window.dispose()`
window.PAGE_AGENT_EXT.stop()`
: `// Stop current task execution
window.dispose()`
window.PAGE_AGENT_EXT.stop()`
}
language="javascript"
/>
</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 */}
<section>
<h2 className="text-2xl font-bold mb-4">
{isZh
? '将 MultiPageAgent 融入你自己的插件'
? '将 MultiPageAgent 集成你自己的插件'
: 'Integrate MultiPageAgent into Your Extension'}
</h2>
<p>@TODO</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? '你可以将 MultiPageAgent 集成到自己的浏览器扩展中,实现跨页面的 AI 自动化能力。'
: 'You can integrate MultiPageAgent into your own browser extension for cross-page AI automation capabilities.'}
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
: 'Start with the extension API docs, then use the background entry implementation as a reference.'}
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
</svg>
packages/extension/src/entrypoints/background.ts
</a>
</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">TODO</p>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh ? '参考源码实现:' : 'Reference implementation:'}
</p>
<a
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/src/entrypoints/background.ts"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d={siGithub.path} />
</svg>
packages/extension/src/entrypoints/background.ts
</a>
</section>
</div>
</div>
@@ -1,10 +1,8 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function Instructions() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
@@ -1,48 +1,51 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function CustomTools() {
const { t } = useTranslation('docs')
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('custom_tools.title')}</h1>
<h1 className="text-4xl font-bold mb-6">{isZh ? '自定义工具' : 'Custom Tools'}</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{t('custom_tools.subtitle')}
{isZh
? '通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义严格的输入接口,让 AI 安全调用你的业务逻辑。'
: 'Extend AI Agent capabilities by registering custom tools. Use Zod to define strict input schemas for safe business logic calls.'}
</p>
<div className="space-y-8">
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.registration')}</h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '工具注册' : 'Tool Registration'}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('custom_tools.registration_desc')}
{isZh
? '每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。'
: 'Each custom tool requires four core properties: name, description, input schema, and execute function.'}
</p>
<CodeEditor
code={`import zod from 'zod'
code={`import * as zod from 'zod'
import { PageAgent, tool } from 'page-agent'
// override internal tool
const customTools = {
ask_user: tool({
description:
'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await do_some_thing(input.question)
return "✅ Received user answer: " + answer
},
})
ask_user: tool({
description:
'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await do_some_thing(input.question)
return "✅ Received user answer: " + answer
},
})
}
// remove internal tool
const customTools = {
ask_user: null // never ask user questions
scroll: null, // never scroll
}
const pageAgent = new PageAgent({customTools})
@@ -52,12 +55,14 @@ const pageAgent = new PageAgent({customTools})
</section>
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.page_filter')}</h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '页面过滤器' : 'Page Filter'}</h2>
<BetaNotice />
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('custom_tools.page_filter_desc')}
{isZh
? '通过 pageFilter 属性控制工具在哪些页面可见,提升安全性和用户体验。'
: 'Control tool visibility on specific pages via the pageFilter property to enhance security and UX.'}
</p>
<CodeEditor
@@ -84,16 +89,28 @@ const pageAgent = new PageAgent({customTools})
</section>
<section>
<h2 className="text-2xl font-bold mb-4">{t('custom_tools.best_practices')}</h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '最佳实践' : 'Best Practices'}</h2>
<div className="space-y-4">
<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">
{t('custom_tools.bp_performance')}
{isZh ? '⚡ 性能优化' : '⚡ Performance Optimization'}
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1 text-sm">
<li>{t('custom_tools.bp_1')}</li>
<li>{t('custom_tools.bp_2')}</li>
<li>{t('custom_tools.bp_3')}</li>
<li>
{isZh
? '• 使用 pageFilter 减少不必要的工具加载'
: '• Use pageFilter to reduce unnecessary tool loading'}
</li>
<li>
{isZh
? '• 在 execute 函数中实现适当的缓存机制'
: '• Implement appropriate caching in execute functions'}
</li>
<li>
{isZh
? '• 避免在工具中执行耗时的同步操作'
: '• Avoid long-running sync operations in tools'}
</li>
</ul>
</div>
</div>
@@ -1,10 +1,8 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function DataMasking() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
@@ -1,107 +1,140 @@
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
// Recommended models: lightweight with excellent tool call capabilities
const MODELS = {
recommended: [
'gpt-4.1-mini',
'claude-haiku-4.5',
'gemini-3-flash',
'deepseek-3.2',
'gpt-5.2',
'qwen-3-max',
],
verified: [
'qwen-3-plus',
'gpt-4.1',
'gpt-5',
'gpt-5-mini',
'grok-4',
'grok-code-fast',
'claude-sonnet-3.5',
'claude-sonnet-4.5',
const BASELINE = new Set([
'gpt-5.1',
'claude-haiku-4.5',
'gemini-3-flash',
'deepseek-3.2',
'qwen3-coder-next',
])
// 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'],
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
Qwen: ['qwen3-coder-next', 'qwen-3-max', 'qwen-3-plus', 'qwen3:14b (ollama)'],
DeepSeek: ['deepseek-3.2'],
Anthropic: [
'claude-opus-4.6',
'claude-opus-4.5',
'gemini-2.5',
'gemini-3-pro',
'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-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() {
const { t } = useTranslation('docs')
const allModels = [...MODELS.recommended, ...MODELS.verified]
const { isZh } = useLanguage()
return (
<div className="max-w-4xl">
<h1 className="text-4xl font-bold mb-4">{t('models.title')}</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">{t('models.subtitle')}</p>
<h1 className="text-4xl font-bold mb-4">{isZh ? '模型' : 'Models'}</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
{isZh
? '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。'
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
</p>
{/* Models Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-3">{t('models.available')}</h2>
<h2 className="text-2xl font-semibold mb-3">{isZh ? '已测试模型' : 'Tested Models'}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t('models.recommendation_logic')}
{isZh
? '推荐使用 ToolCall 能力强的轻量级模型。'
: 'Recommended: Fast, lightweight models with strong ToolCall capabilities.'}
</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="flex flex-wrap gap-2">
{allModels.map((model) => {
const isRecommended = MODELS.recommended.includes(model)
return (
<div
key={model}
className={`px-3 py-1.5 rounded-md text-sm font-medium font-mono transition-colors ${
isRecommended
? '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}
{isRecommended && <span className="ml-1"></span>}
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
{Object.entries(MODEL_GROUPS).map(([brand, models]) => (
<Fragment key={brand}>
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 pt-2">
{brand}
</span>
<div className="flex flex-wrap gap-2">
{models.map((model) => (
<ModelBadge key={model} model={model} baseline={BASELINE.has(model)} />
))}
</div>
)
})}
</Fragment>
))}
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-5"> baseline models</p>
</div>
</section>
{/* Tips Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.tips')}</h2>
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2>
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>{t('models.tip_2')}</li>
<li>{t('models.tip_3')}</li>
<li>
{isZh
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature'
: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended'}
</li>
<li>
{isZh
? '小模型或者无法适应复杂 Tool 定义的模型,通常效果不佳'
: 'Small models or those unable to handle complex tool definitions typically perform poorly'}
</li>
</ul>
</div>
</section>
{/* Security Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.security')}</h2>
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '🔐 生产环境鉴权建议' : '🔐 Production Authentication'}
</h2>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{t('models.security_warning')}
{isZh
? '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码'
: '⚠️ Never commit real LLM API Keys to your frontend code'}
</p>
</div>
<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">
{t('models.security_backend_proxy')}
{isZh ? '后端代理转发' : 'Backend Proxy Pattern'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t('models.security_backend_desc')}
{isZh
? '在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:'
: 'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:'}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>{t('models.security_method_1')}</li>
<li>{t('models.security_method_2')}</li>
<li>{t('models.security_method_3')}</li>
<li>{isZh ? '• Session/Cookie 会话认证' : '• Session/Cookie-based authentication'}</li>
<li>
{isZh ? '• OIDC (OpenID Connect) 单点登录' : '• OIDC (OpenID Connect) single sign-on'}
</li>
<li>
{isZh ? '• 临时 Access Key 或 JWT Token' : '• Temporary Access Key or JWT Token'}
</li>
</ul>
</div>
</section>
{/* Configuration Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{t('models.configuration')}</h2>
<h2 className="text-2xl font-semibold mb-4">{isZh ? '配置方式' : 'Configuration'}</h2>
<CodeEditor
code={`// OpenAI-compatible services (e.g., Alibaba Bailian)
const pageAgent = new PageAgent({
@@ -113,8 +146,8 @@ const pageAgent = new PageAgent({
// Self-hosted models (e.g., Ollama)
const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
apiKey: 'N/A', // Ollama typically accepts any value
model: 'qwen3:latest'
apiKey: 'NA',
model: 'qwen3:14b'
});
// Free testing endpoint
@@ -126,6 +159,51 @@ const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
`}
/>
</section>
{/* Ollama Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">Ollama</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
</p>
<CodeEditor
code={`LLM_BASE_URL="http://localhost:11434/v1"
LLM_API_KEY="NA"
LLM_MODEL_NAME="qwen3:14b"`}
/>
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
</h3>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
{isZh
? '确保 OLLAMA_ORIGINS 设置为 * 以避免 403 错误'
: 'Add * to OLLAMA_ORIGINS to avoid 403 errors'}
</li>
<li>
{isZh
? '小于 10B 参数的模型通常效果不佳'
: 'Models smaller than 10B are unlikely to be strong enough'}
</li>
<li>{isZh ? '需要支持 tool_call 的模型' : 'Requires tool_call capable models'}</li>
<li>
{isZh
? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作'
: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
<code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded">
$env:OLLAMA_CONTEXT_LENGTH=64000; ollama serve
</code>
</li>
</ul>
</li>
</ul>
</div>
</section>
</div>
)
}
+20 -17
View File
@@ -3,6 +3,7 @@ import { Route, Switch } from 'wouter'
import Header from '../../components/Header'
import DocsLayout from './Layout'
import CustomUIDocs from './advanced/custom-ui/page'
import PageAgentCoreDocs from './advanced/page-agent-core/page'
// Advanced
import PageAgentDocs from './advanced/page-agent/page'
@@ -13,14 +14,13 @@ import CustomTools from './features/custom-tools/page'
import DataMasking from './features/data-masking/page'
import Models from './features/models/page'
import BestPractices from './integration/best-practices/page'
// Integration
import CdnSetup from './integration/cdn-setup/page'
import SecurityPermissions from './integration/security-permissions/page'
import ThirdPartyAgent from './integration/third-party-agent/page'
import Limitations from './introduction/limitations/page'
// Introduction
import Overview from './introduction/overview/page'
import QuickStart from './introduction/quick-start/page'
import Troubleshooting from './introduction/troubleshooting/page'
function DocsPage({ children }: { children: React.ReactNode }) {
return (
@@ -52,6 +52,11 @@ export default function DocsRouter() {
<Limitations />
</DocsPage>
</Route>
<Route path="/introduction/troubleshooting">
<DocsPage>
<Troubleshooting />
</DocsPage>
</Route>
{/* Features */}
<Route path="/features/custom-tools">
@@ -80,10 +85,20 @@ export default function DocsRouter() {
</DocsPage>
</Route>
{/* Integration */}
<Route path="/integration/cdn-setup">
{/* Advanced */}
<Route path="/advanced/page-agent">
<DocsPage>
<CdnSetup />
<PageAgentDocs />
</DocsPage>
</Route>
<Route path="/advanced/page-agent-core">
<DocsPage>
<PageAgentCoreDocs />
</DocsPage>
</Route>
<Route path="/advanced/custom-ui">
<DocsPage>
<CustomUIDocs />
</DocsPage>
</Route>
<Route path="/integration/security-permissions">
@@ -102,18 +117,6 @@ export default function DocsRouter() {
</DocsPage>
</Route>
{/* Advanced */}
<Route path="/advanced/page-agent">
<DocsPage>
<PageAgentDocs />
</DocsPage>
</Route>
<Route path="/advanced/page-agent-core">
<DocsPage>
<PageAgentCoreDocs />
</DocsPage>
</Route>
{/* Default redirect or 404 */}
<Route path="/docs">
<DocsPage>
@@ -1,13 +1,18 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function BestPractices() {
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6"></h1>
<h1 className="text-4xl font-bold mb-6">{isZh ? '最佳实践' : 'Best Practices'}</h1>
<BetaNotice />
<p className="text-xl text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
使 page-agent
{isZh
? '使用 page-agent 的最佳实践和常见问题解决方案。'
: 'Best practices and common solutions for using page-agent.'}
</p>
<CodeEditor code={`// TODO`} />
</div>
@@ -1,68 +0,0 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
export default function CdnSetup() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
return (
<div className="space-y-10">
<header>
<h1 className="text-4xl font-bold mb-4">{isZh ? 'CDN 引入' : 'CDN Setup'}</h1>
<p className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed">
{isZh
? 'CDN 提供两种构建版本:Demo 版用于快速体验,标准版用法与 NPM 一致。'
: 'CDN provides two builds: Demo for quick testing, standard build with usage identical to NPM.'}
</p>
</header>
{/* Demo Build Section */}
<section>
<h2 className="text-2xl font-bold mb-3">{isZh ? '🚀 快速体验' : '🚀 Quick Try'}</h2>
<p className="text-gray-600 dark:text-gray-300 mb-3">
{isZh
? '自动初始化并使用内置 Demo LLM,无需任何配置:'
: 'Auto-initializes with built-in demo LLM, no configuration needed:'}
</p>
<CodeEditor
className="mb-3"
code={`<script src="${CDN_DEMO_URL}" crossorigin="true"></script>`}
/>
<div className="bg-yellow-50 dark:bg-yellow-900/20 px-4 py-3 rounded-lg mb-4 text-sm">
<span className="text-yellow-800 dark:text-yellow-200">
{' '}
{isZh
? '仅用于技术评估,Demo 模型有速率限制。'
: 'For evaluation only. Demo model has rate limits.'}
</span>
</div>
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-semibold w-28">
{isZh ? '位置' : 'Location'}
</th>
<th className="text-left py-2 px-3 font-semibold">URL</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-2 px-3">{isZh ? '全球' : 'Global'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_URL}</td>
</tr>
<tr>
<td className="py-2 px-3">{isZh ? '中国' : 'China'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_CN_URL}</td>
</tr>
</tbody>
</table>
</section>
</div>
)
}
@@ -1,10 +1,8 @@
import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import { useLanguage } from '@/i18n/context'
export default function SecurityPermissions() {
const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
@@ -1,21 +1,32 @@
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
export default function ThirdPartyAgentPage() {
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6"> Agent</h1>
<h1 className="text-4xl font-bold mb-6">
{isZh ? '接入第三方 Agent' : 'Third-party Agent Integration'}
</h1>
<p className="mb-6 leading-relaxed text-gray-600 dark:text-gray-300">
pageAgent Agent Agent
{isZh
? '将 pageAgent 作为工具接入你的答疑助手或 Agent 系统,成为你 Agent 的眼和手。'
: 'Integrate pageAgent as a tool in your support assistant or Agent system, becoming the eyes and hands of your Agent.'}
</p>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">💡 </h3>
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{isZh ? '💡 核心价值' : '💡 Core Value'}
</h3>
<p className="text-blue-800 dark:text-blue-200">
"嘴巴""眼睛""手"
{isZh
? '让你的答疑机器人不再只是"嘴巴",而是拥有"眼睛"和"手"的完整智能体。'
: 'Transform your support bot from just a "mouth" into a complete intelligent agent with "eyes" and "hands".'}
</p>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '集成方式' : 'Integration Method'}</h2>
<div className="space-y-4 mb-6">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
@@ -23,14 +34,14 @@ export default function ThirdPartyAgentPage() {
1. Function Calling
</h3>
<CodeEditor
code={`// 定义工具
code={`// ${isZh ? '定义工具' : 'Define tool'}
const pageAgentTool = {
name: "page_agent",
description: "执行网页操作",
description: "${isZh ? '执行网页操作' : 'Execute web page operations'}",
parameters: {
type: "object",
properties: {
instruction: { type: "string", description: "操作指令" }
instruction: { type: "string", description: "${isZh ? '操作指令' : 'Operation instruction'}" }
},
required: ["instruction"]
},
@@ -40,48 +51,74 @@ const pageAgentTool = {
}
}
// 注册到你的 agent 中`}
// ${isZh ? '注册到你的 agent 中' : 'Register to your agent'}`}
language="javascript"
/>
</div>
</div>
<h2 className="text-2xl font-bold mb-4"></h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '应用场景' : 'Use Cases'}</h2>
<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">
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🤖 </h4>
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
{isZh ? '🤖 智能客服系统' : '🤖 Smart Customer Service'}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"帮我提交工单"
{isZh
? '客服机器人帮用户直接操作系统,如"帮我提交工单"'
: 'Support bots directly operate systems for users, e.g., "Help me submit a ticket"'}
</p>
</div>
<div className="bg-linear-to-br from-green-50 to-blue-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>
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
{isZh ? '📋 业务流程助手' : '📋 Business Process Assistant'}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"完成客户入职"
{isZh
? '引导新员工完成复杂流程,如"完成客户入职"'
: 'Guide new employees through complex processes, e.g., "Complete customer onboarding"'}
</p>
</div>
<div className="bg-linear-to-br from-purple-50 to-pink-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>
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
{isZh ? '🎯 个人效率助手' : '🎯 Personal Productivity Assistant'}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"预订会议室"
{isZh
? '跨网站帮你完成任务,如"预订会议室"'
: 'Complete tasks across websites, e.g., "Book a meeting room"'}
</p>
</div>
<div className="bg-linear-to-br from-orange-50 to-red-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>
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">
{isZh ? '🔧 运维自动化' : '🔧 DevOps Automation'}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">
"重启服务器"
{isZh
? '通过自然语言操作管理后台,如"重启服务器"'
: 'Operate admin panels via natural language, e.g., "Restart server"'}
</p>
</div>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-100 mb-2">
{isZh ? '⚠️ 注意事项' : '⚠️ Notes'}
</h3>
<ul className="text-yellow-800 dark:text-yellow-200 space-y-1 text-sm">
<li> </li>
<li> </li>
<li> </li>
<li>
{' '}
{isZh
? '确保目标网站允许自动化操作'
: 'Ensure target website allows automated operations'}
</li>
<li> {isZh ? '实现适当的频率限制' : 'Implement appropriate rate limiting'}</li>
<li>
{' '}
{isZh
? '敏感操作建议要求人工确认'
: 'Recommend human confirmation for sensitive operations'}
</li>
</ul>
</div>
</div>
@@ -1,142 +1,156 @@
import { useTranslation } from 'react-i18next'
import { Link } from 'wouter'
import { useLanguage } from '@/i18n/context'
export default function LimitationsPage() {
const { t } = useTranslation('docs')
const { isZh } = useLanguage()
return (
<div className="max-w-4xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">
{t('limitations.title')}
{isZh ? '使用限制' : 'Limitations'}
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300">{t('limitations.subtitle')}</p>
<p className="text-xl text-gray-600 dark:text-gray-300">
{isZh
? 'Page Agent 基于 DOM 理解网页并执行操作。这决定了它的能力边界。'
: 'Page Agent understands web pages via DOM and performs actions accordingly. This defines its capability boundary.'}
</p>
</div>
<div className="prose prose-lg dark:prose-invert max-w-none">
<h2 className="text-2xl font-bold mb-3">{t('limitations.page_support')}</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-400 p-4 mb-6">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">
{t('limitations.spa_limit_title')}
</h3>
<ul className="text-blue-700 dark:text-blue-300 space-y-2">
<li>{t('limitations.spa_limit_1')}</li>
<li>{t('limitations.spa_limit_2')}</li>
<li>{t('limitations.spa_limit_3')}</li>
</ul>
{/* PageAgent.js vs PageAgentExt */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? 'PageAgent.js vs PageAgentExt' : 'PageAgent.js vs PageAgentExt'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh
? 'PageAgent.js 是核心库,运行在页面内。PageAgentExt 是可选的浏览器扩展,提供额外的浏览器级控制能力。'
: 'PageAgent.js is the core library running inside a page. PageAgentExt is an optional browser extension that adds browser-level control.'}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 pr-4"></th>
<th className="text-left py-3 px-4 font-semibold">PageAgent.js</th>
<th className="text-left py-3 pl-4 font-semibold">
PageAgentExt{' '}
<Link
href="/docs/features/chrome-extension"
className="text-xs font-normal text-blue-600 dark:text-blue-400 hover:underline"
>
{isZh ? '了解更多' : 'learn more'}
</Link>
</th>
</tr>
</thead>
<tbody className="text-gray-600 dark:text-gray-300">
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '接入方式' : 'Integration'}
</td>
<td className="py-3 px-4">
{isZh ? '网站开发者主动集成' : 'Site developer integrates the library'}
</td>
<td className="py-3 pl-4">
{isZh ? '用户安装浏览器扩展' : 'User installs a browser extension'}
</td>
</tr>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '可操作范围' : 'Scope'}
</td>
<td className="py-3 px-4">
{isZh ? '当前页面(为 SPA 设计)' : 'Current page (designed for SPAs)'}
</td>
<td className="py-3 pl-4">
{isZh ? '任意网页、多标签页' : 'Any web page, multi-tab'}
</td>
</tr>
<tr>
<td className="py-3 pr-4 font-medium text-gray-900 dark:text-white">
{isZh ? '额外能力' : 'Extra capabilities'}
</td>
<td className="py-3 px-4"></td>
<td className="py-3 pl-4">
{isZh ? '新建/切换/关闭标签页' : 'Open / switch / close tabs'}
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.interaction_limits')}</h2>
{/* Interaction Limitations */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? '交互能力' : 'Interaction Capabilities'}
</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<h3 className="font-semibold mb-4">{t('limitations.supported_ops')}</h3>
<div className="grid md:grid-cols-2 gap-4 mb-6">
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_click')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_input')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_scroll')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_submit')}</span>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="font-semibold mb-3 text-green-700 dark:text-green-400">
{isZh ? '支持' : 'Supported'}
</h3>
<ul className="space-y-1.5 text-sm">
{[
isZh ? '点击、文本输入、选择' : 'Click, text input, select',
isZh ? '页面滚动(垂直 / 水平)' : 'Scroll (vertical / horizontal)',
isZh ? '表单提交、焦点切换' : 'Form submit, focus',
isZh ? '执行 JavaScript(可选)' : 'Execute JavaScript (opt-in)',
].map((text) => (
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
<span className="mr-2 text-green-600 dark:text-green-400"></span>
{text}
</li>
))}
</ul>
</div>
<div className="space-y-2">
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_select')}</span>
</div>
<div className="flex items-center text-green-600 dark:text-green-400">
<span className="mr-2"></span>
<span>{t('limitations.op_focus')}</span>
</div>
</div>
</div>
<h3 className="font-semibold mb-4">{t('limitations.unsupported_ops')}</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>{t('limitations.op_hover')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_drag')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_context')}</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>{t('limitations.op_draw')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_keyboard')}</span>
</div>
<div className="flex items-center text-red-600 dark:text-red-400">
<span className="mr-2"></span>
<span>{t('limitations.op_position')}</span>
</div>
<div>
<h3 className="font-semibold mb-3 text-red-700 dark:text-red-400">
{isZh ? '不支持' : 'Not supported'}
</h3>
<ul className="space-y-1.5 text-sm">
{[
isZh ? '悬停、拖拽、右键菜单' : 'Hover, drag & drop, right-click',
isZh ? '键盘快捷键' : 'Keyboard shortcuts',
isZh ? '坐标定位操作' : 'Position-based control',
isZh ? '绘图操作' : 'Drawing',
].map((text) => (
<li key={text} className="flex items-center text-gray-700 dark:text-gray-300">
<span className="mr-2 text-red-600 dark:text-red-400"></span>
{text}
</li>
))}
</ul>
</div>
</div>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.understanding_limits')}</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">
{t('limitations.no_vision_title')}
</h3>
<p className="text-red-700 dark:text-red-300 mb-3">{t('limitations.no_vision_desc')}</p>
<ul className="text-red-700 dark:text-red-300 space-y-1">
<li>{t('limitations.no_vision_1')}</li>
<li>{t('limitations.no_vision_2')}</li>
<li>{t('limitations.no_vision_3')}</li>
<li>{t('limitations.no_vision_4')}</li>
</ul>
{/* Understanding Limitations */}
<h2 className="text-2xl font-bold mb-3">{isZh ? '理解能力' : 'Understanding'}</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 border-l-4 border-amber-400 p-4 mb-6">
<p className="text-amber-800 dark:text-amber-200 mb-2 font-medium">
{isZh
? 'Page Agent 不使用多模态模型,不截图,没有视觉能力。仅通过 DOM 结构理解页面。'
: 'Page Agent does not use multimodal models, does not take screenshots, and has no visual capability. It reads pages through DOM structure only.'}
</p>
<p className="text-amber-700 dark:text-amber-300 text-sm">
{isZh
? '图片、Canvas、WebGL、SVG 等视觉内容无法被识别。页面的语义化程度和可访问性直接影响 AI 的理解准确性。'
: 'Images, Canvas, WebGL, SVG and other visual content cannot be recognized. Page semantic quality and accessibility directly affect AI accuracy.'}
</p>
</div>
<h2 className="text-2xl font-bold mb-3">{t('limitations.website_requirements')}</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 mb-6">
<div className="space-y-4">
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_semantic_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">
{t('limitations.req_semantic_desc')}
</p>
</div>
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_ux_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">{t('limitations.req_ux_desc')}</p>
</div>
<div>
<h3 className="font-semibold mb-2">{t('limitations.req_env_title')}</h3>
<p className="text-gray-600 dark:text-gray-300">{t('limitations.req_env_desc')}</p>
</div>
</div>
</div>
<h2>{t('limitations.future')}</h2>
<div className="bg-green-50 dark:bg-green-900/20 border-l-4 border-green-400 p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
{t('limitations.future_title')}
</h3>
<ul className="text-green-700 dark:text-green-300 space-y-1">
<li>{t('limitations.future_1')}</li>
<li>{t('limitations.future_2')}</li>
<li>{t('limitations.future_3')}</li>
<li>{t('limitations.future_4')}</li>
</ul>
{/* Website Quality */}
<h2 className="text-2xl font-bold mb-3">
{isZh ? '网页质量影响' : 'Page Quality Matters'}
</h2>
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-6">
<p className="text-gray-600 dark:text-gray-300 text-sm">
{isZh
? '反常识的交互逻辑、纯视觉的操作提示、快速出现消失的元素等都会降低自动化成功率。语义化的 HTML 和良好的可访问性会显著提升效果。'
: 'Counter-intuitive interactions, visual-only cues, and rapidly appearing/disappearing elements reduce automation success. Semantic HTML and good accessibility significantly improve results.'}
</p>
</div>
</div>
</div>
@@ -1,23 +1,16 @@
import { useTranslation } from 'react-i18next'
import { useLanguage } from '@/i18n/context'
export default function Overview() {
const { t } = useTranslation('docs')
const { isZh } = useLanguage()
return (
<article>
{/* 头图 */}
{/* <figure className="mb-8 rounded-xl overflow-hidden">
<img
src="https://img.alicdn.com/imgextra/i3/O1CN014xLeic1gnF2iWdzjJ_!!6000000004186-49-tps-1280-353.webp"
alt="page-agent"
className="w-full h-64 object-cover"
/>
</figure> */}
<div className="mb-8">
<h1 className="text-4xl font-bold mb-4">{t('overview.title')}</h1>
<h1 className="text-4xl font-bold mb-4">Overview</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-4 leading-relaxed">
{t('overview.subtitle')}
{isZh
? 'page-agent 是一个完全基于 Web 技术的 GUI Agent,简单几步,让你的网站拥有 AI 操作员。'
: 'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.'}
</p>
{/* Status Badges */}
@@ -55,57 +48,75 @@ export default function Overview() {
</div>
<section>
<h2 className="text-2xl font-bold mb-4">{t('overview.what_is')}</h2>
<h2 className="text-2xl font-bold mb-4">
{isZh ? '什么是 page-agent' : 'What is page-agent?'}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-8 leading-relaxed ">
{t('overview.what_is_desc')}
{isZh
? 'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或Agent客户端开发者;将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。'
: 'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent is built for web developers and web applications first. Integrate it into your site to let users interact with pages through natural language.'}
</p>
</section>
<section>
<h2 className="text-2xl font-bold mb-3">{t('overview.features_title')}</h2>
<h2 className="text-2xl font-bold mb-3">{isZh ? '核心特性' : 'Core Features'}</h2>
<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">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{t('overview.feature_dom.title')}
{isZh ? '🧠 智能 DOM 理解' : '🧠 Smart DOM Analysis'}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_dom.desc')}</p>
<p className="text-gray-700 dark:text-gray-300">
{isZh
? '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。'
: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.'}
</p>
</div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('overview.feature_secure.title')}
{isZh ? '🔒 安全可控' : '🔒 Secure & Controllable'}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_secure.desc')}</p>
<p className="text-gray-700 dark:text-gray-300">
{isZh
? '支持操作黑白名单、数据脱敏保护。注入自定义知识库,让 AI 按你的规则工作。'
: 'Supports operation allowlists, data masking protection. Inject custom knowledge to make AI work by your rules.'}
</p>
</div>
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('overview.feature_backend.title')}
{isZh ? '⚡ 零后端部署' : '⚡ Zero Backend'}
</h3>
<p className="text-gray-700 dark:text-gray-300">{t('overview.feature_backend.desc')}</p>
<p className="text-gray-700 dark:text-gray-300">
{isZh
? 'CDN 或 NPM 引入,自定义 LLM 接入点。'
: 'CDN or NPM import with custom LLM endpoints.'}
</p>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('overview.feature_accessible.title')}
{isZh ? '♿ 普惠智能' : '♿ Accessible Intelligence'}
</h3>
<p className="text-gray-700 dark:text-gray-300">
{t('overview.feature_accessible.desc')}
{isZh
? '为复杂 B端系统、管理后台提供自然语言入口。让每个用户都能轻松上手。'
: 'Provides natural language interface for complex B2B systems and admin panels. Makes software easy for everyone.'}
</p>
</div>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.vs_browser_use')}</h2>
<h2 className="text-2xl font-bold mb-4">
{isZh ? '与 browser-use 的区别' : 'vs. browser-use'}
</h2>
<div className="overflow-x-auto mb-8">
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
<thead>
<tr className="bg-gray-50 dark:bg-gray-800">
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
{t('overview.table_feature')}
</th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left"></th>
<th className="border border-gray-300 dark:border-gray-600 px-4 py-3 text-left">
page-agent
</th>
@@ -117,53 +128,53 @@ export default function Overview() {
<tbody>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_deployment')}
{isZh ? '部署方式' : 'Deployment'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_pa')}
{isZh ? '页面内嵌组件' : 'Embedded component'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_deployment_bu')}
{isZh ? '外部工具' : 'External tool'}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scope')}
{isZh ? '操作范围' : 'Scope'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_pa')}
{isZh ? '当前页面' : 'Current page'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scope_bu')}
{isZh ? '整个浏览器' : 'Entire browser'}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_user')}
{isZh ? '目标用户' : 'Target Users'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_pa')}
{isZh ? '网站开发者' : 'Web developers'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_user_bu')}
{isZh ? '爬虫/Agent 开发者' : 'Scraper/Agent developers'}
</td>
</tr>
<tr>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3 font-medium">
{t('overview.table_scenario')}
{isZh ? '使用场景' : 'Use Case'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_pa')}
{isZh ? '用户体验增强' : 'UX enhancement'}
</td>
<td className="border border-gray-300 dark:border-gray-600 px-4 py-3">
{t('overview.table_scenario_bu')}
{isZh ? '自动化任务' : 'Automation tasks'}
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-bold mb-4">{t('overview.use_cases_title')}</h2>
<h2 className="text-2xl font-bold mb-4">{isZh ? '应用场景' : 'Use Cases'}</h2>
<ul className="space-y-4 mb-8">
<li className="flex items-start space-x-3">
@@ -171,7 +182,10 @@ export default function Overview() {
1
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case1_title')}</strong> {t('overview.use_case1_desc')}
<strong>{isZh ? '对接答疑机器人:' : 'Connect Support Bots:'}</strong>{' '}
{isZh
? '把你的答疑助手变成全能Agent。客服机器人不再只说「请先点击设置按钮然后点击...」,而是直接帮用户现场操作。'
: "Turn your support assistant into a full agent. Customer service bots no longer just say 'Please click the settings button then click...'—they operate for users directly."}
</div>
</li>
<li className="flex items-start space-x-3">
@@ -179,7 +193,10 @@ export default function Overview() {
2
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case2_title')}</strong> {t('overview.use_case2_desc')}
<strong>{isZh ? '交互升级/智能化改造:' : 'Modernize Legacy Apps:'}</strong>{' '}
{isZh
? '一行代码,老应用变身Agent,产品专家帮用户操作复杂 B 端软件。降低人工支持成本,提高用户满意度。'
: 'One line of code transforms old apps into agents. Product experts help users navigate complex B2B software. Reduce support costs and improve satisfaction.'}
</div>
</li>
<li className="flex items-start space-x-3">
@@ -187,7 +204,10 @@ export default function Overview() {
3
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case3_title')}</strong> {t('overview.use_case3_desc')}
<strong>{isZh ? '产品教学:' : 'Interactive Training:'}</strong>{' '}
{isZh
? '向用户演示交互过程,边做边教。例如让AI演示「如何提交报销申请」的完整操作流程。'
: "Demonstrate workflows in real-time. Let AI show the complete process of 'how to submit an expense report.'"}
</div>
</li>
<li className="flex items-start space-x-3">
@@ -195,7 +215,10 @@ export default function Overview() {
4
</span>
<div className="text-gray-700 dark:text-gray-300">
<strong>{t('overview.use_case4_title')}</strong> {t('overview.use_case4_desc')}
<strong>{isZh ? '无障碍支持:' : 'Accessibility:'}</strong>{' '}
{isZh
? '为视障用户、老年用户提供自然语言交互,对接屏幕阅读器或语音助理,让软件人人可用。'
: 'Provide natural language interaction for visually impaired and elderly users. Connect screen readers or voice assistants to make software accessible to everyone.'}
</div>
</li>
</ul>
@@ -1,19 +1,19 @@
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/components/CodeEditor'
import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
import { useLanguage } from '@/i18n/context'
export default function QuickStart() {
const { t, i18n } = useTranslation('docs')
const isZh = i18n.language === 'zh-CN'
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6">{t('quick_start.title')}</h1>
<h1 className="text-4xl font-bold mb-6">Quick Start</h1>
<p className=" mb-6 leading-relaxed">{t('quick_start.subtitle')}</p>
<p className=" mb-6 leading-relaxed">
{isZh ? '几分钟内完成 page-agent 的集成。' : 'Integrate page-agent in minutes.'}
</p>
<h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2>
<h2 className="text-2xl font-bold mb-3">{isZh ? '安装步骤' : 'Installation Steps'}</h2>
<div className="space-y-4 mb-6">
{/* Demo CDN - One Line */}
@@ -27,7 +27,7 @@ export default function QuickStart() {
</span>
</div>
<CodeEditor
code={`<script src="${isZh ? CDN_DEMO_CN_URL : CDN_DEMO_URL}" crossorigin="true"></script>`}
code={`<script src="DEMO_CDN_URL" crossorigin="true"></script>`}
language="html"
/>
<table className="w-full border-collapse text-sm">
@@ -67,7 +67,7 @@ import { PageAgent } from 'page-agent'`}
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('quick_start.step2_title')}
{isZh ? '2. 初始化配置' : '2. Initialize Configuration'}
</h3>
<CodeEditor
code={`const agent = new PageAgent({
@@ -82,7 +82,7 @@ import { PageAgent } from 'page-agent'`}
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('quick_start.step3_title')}
{isZh ? '3. 开始使用' : '3. Start Using'}
</h3>
<CodeEditor
code={`// ${isZh ? '程序化执行自然语言指令' : 'Execute natural language instructions programmatically'}
@@ -0,0 +1,424 @@
import { useEffect, useRef, useState } from 'react'
import { Link } from 'wouter'
import CodeEditor from '@/components/CodeEditor'
import { useLanguage } from '@/i18n/context'
// ---------------------------------------------------------------------------
// Data: each section is a typed object for easy extension
// ---------------------------------------------------------------------------
interface TroubleshootingSection {
id: string
title: { en: string; zh: string }
symptom: { en: string; zh: string }
color: 'red' | 'amber' | 'orange' | 'violet'
content: (isZh: boolean) => React.ReactNode
}
const SECTIONS: TroubleshootingSection[] = [
{
id: 'format-errors',
title: { en: 'Model Response Format Errors', zh: '模型返回格式错误' },
symptom: {
en: 'The model returns malformed tool calls, plain text, or unexpected JSON instead of structured actions.',
zh: '模型返回了格式错误的 tool call、纯文本或非预期的 JSON,而非结构化的操作指令。',
},
color: 'amber',
content: FormatErrorsContent,
},
{
id: 'low-success-rate',
title: { en: 'Low Task Success Rate', zh: '任务成功率低' },
symptom: {
en: 'The agent appears to understand the task but frequently fails to complete it, or produces incorrect results.',
zh: 'Agent 似乎理解了任务,但频繁执行失败或产生不正确的结果。',
},
color: 'amber',
content: LowSuccessRateContent,
},
{
id: 'wrong-element',
title: { en: "Can't Hit Target Elements", zh: '无法点击目标元素' },
symptom: {
en: 'The agent repeatedly retries but keeps interacting with the wrong element, or fails to locate the correct one.',
zh: 'Agent 反复重试,但始终点击在错误的元素上,或无法定位到正确的目标元素。',
},
color: 'amber',
content: WrongElementContent,
},
{
id: 'api-errors',
title: { en: 'API Request Errors', zh: 'API 请求错误' },
symptom: {
en: 'HTTP 400 Bad Request or similar errors when calling the LLM API.',
zh: '调用 LLM API 时出现 HTTP 400 Bad Request 或类似的参数错误。',
},
color: 'amber',
content: ApiErrorsContent,
},
]
// ---------------------------------------------------------------------------
// Section content components
// ---------------------------------------------------------------------------
function FormatErrorsContent(isZh: boolean) {
return (
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
<li>
<strong>{isZh ? '确认模型是否支持' : 'Verify model compatibility'}</strong>
<p className="mt-1">
{isZh
? '并非所有模型都能正确处理 page-agent 的 tool 定义。请查看'
: 'Not all models can handle page-agent tool definitions correctly. Check the '}
<Link
href="/features/models"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
{isZh ? '已测试模型列表' : 'tested models list'}
</Link>
{isZh ? '。' : '.'}
</p>
</li>
<li>
<strong>
{isZh ? '检查代理/网关的参数转发' : 'Check proxy/gateway parameter forwarding'}
</strong>
<p className="mt-1">
{isZh
? '如果使用了 API 代理或网关,请确保请求中的 '
: 'If using an API proxy or gateway, make sure the '}
<code>tools</code>
{isZh
? ' 字段被完整、无修改地转发给模型供应商。部分代理可能会剥离或修改此字段。'
: ' parameter is forwarded to the model provider intact. Some proxies may strip or alter this field.'}
</p>
</li>
<li>
<strong>{isZh ? '寻求社区帮助' : 'Get community help'}</strong>
<p className="mt-1">
{isZh ? (
<>
{' '}
<a
href="https://github.com/alibaba/page-agent/discussions"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
GitHub Discussions
</a>{' '}
</>
) : (
<>
If the above steps don't help, join the{' '}
<a
href="https://github.com/alibaba/page-agent/discussions"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
GitHub Discussions
</a>{' '}
with your model name and error details.
</>
)}
</p>
</li>
</ol>
)
}
function LowSuccessRateContent(isZh: boolean) {
return (
<>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 italic">
{isZh
? '按以下顺序逐步排查,从最简单的情况开始:'
: 'Follow this diagnostic funnel from simplest to most advanced:'}
</p>
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
<li>
<strong>{isZh ? '先从简单指令开始' : 'Start with a simple instruction'}</strong>
<p className="mt-1">
{isZh
? '给一个具体的、单步的简单指令(如"点击登录按钮"),看 Agent 能否完成。如果连简单操作都失败了,问题可能不在模型能力上。'
: 'Give a concrete, single-step instruction (e.g. "click the login button"). If even simple actions fail, the issue is likely not model capability.'}
</p>
</li>
<li>
<strong>{isZh ? '尝试最强模型' : 'Try the strongest model available'}</strong>
<p className="mt-1">
{isZh
? '切换到你能获取到的最先进、最大的模型,以排除是否是模型智能水平不足导致的问题。'
: "Switch to the most capable model you have access to, to isolate whether it's a model intelligence issue."}
</p>
</li>
<li>
<strong>{isZh ? '优化指令质量' : 'Improve instruction quality'}</strong>
<p className="mt-1">
{isZh
? '给出尽可能具体的指令。对于复杂任务,建议使用另一个 LLM 来预先拆分和细化用户的需求,然后逐步执行。'
: "Be as specific as possible. For complex tasks, consider using another LLM to decompose and refine the user's request before execution."}
</p>
</li>
<li>
<strong>{isZh ? '提供充足的上下文' : 'Provide sufficient context'}</strong>
<p className="mt-1">
{isZh
? '通过 instructions 配置注入网站背景描述、关键术语解释等上下文信息,帮助 Agent 更好地理解页面。'
: 'Use the instructions config to inject website descriptions, key terminology, and background context to help the agent understand the page.'}
</p>
</li>
<li>
<strong>{isZh ? '检查 HTML 清洗结果' : 'Check HTML sanitization output'}</strong>
<p className="mt-1">
{isZh
? '使用开发者工具检查清洗后的 HTML,确认关键信息、文本和可操作元素是否被正确保留。'
: 'Inspect the sanitized HTML in dev tools to confirm that key information, text, and interactive elements are preserved correctly.'}
</p>
</li>
</ol>
</>
)
}
function WrongElementContent(isZh: boolean) {
return (
<ol className="list-decimal pl-5 space-y-4 text-gray-700 dark:text-gray-300">
<li>
<strong>{isZh ? '了解现实局限' : 'Understand the reality'}</strong>
<p className="mt-1">
{isZh
? '并非所有网站都提供了完善的语义化 HTML 和 accessibility 标签。对于此类网站,DOM 清洗可能无法产出足够好的结果。'
: 'Not all websites provide proper semantic HTML and accessibility labels. For such sites, DOM sanitization may not produce good enough results.'}
</p>
</li>
<li>
<strong>{isZh ? '检查目标元素类型' : 'Check target element type'}</strong>
<p className="mt-1">
{isZh
? '确认目标元素是否为图片、Canvas、或需要复杂交互(如拖拽、基于坐标的点击)的元素。这些本身就超出了当前的能力范围。'
: 'Verify if the target is an image, Canvas, or requires complex interactions (drag-and-drop, coordinate-based clicking). These are beyond current capabilities.'}
</p>
</li>
<li>
<strong>{isZh ? '检查清洗后的 HTML' : 'Inspect sanitized HTML'}</strong>
<p className="mt-1">
{isZh
? '检查清洗结果中是否存在关键信息丢失、可操作元素未被编号等问题。'
: 'Look for missing key information or unnumbered interactive elements in the sanitized output.'}
</p>
</li>
<li>
<strong>{isZh ? '注入 accessibility 增强' : 'Inject accessibility improvements'}</strong>
<p className="mt-1">
{isZh
? '通过注入脚本为网站添加 aria-label、语义化标签等 accessibility 属性,改善 DOM 清洗质量。'
: 'Inject scripts to add aria-labels, semantic attributes, and other a11y improvements to enhance DOM sanitization quality.'}
</p>
</li>
<li>
<strong>{isZh ? '开发专用 Tool' : 'Build a custom Tool'}</strong>
<p className="mt-1">
{isZh ? (
<>
{' '}
<Link
href="/features/custom-tools"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
Tool
</Link>{' '}
</>
) : (
<>
For consistently difficult elements, consider building a{' '}
<Link
href="/features/custom-tools"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
custom Tool
</Link>{' '}
to interact with them directly.
</>
)}
</p>
</li>
</ol>
)
}
function ApiErrorsContent(isZh: boolean) {
return (
<div className="space-y-4 text-gray-700 dark:text-gray-300">
<p>
{isZh
? '一些 LLM 供应商使用了与 OpenAI 不完全兼容的参数格式,导致请求参数校验失败。'
: 'Some LLM providers use parameter formats that are not fully compatible with the OpenAI spec, causing request validation failures.'}
</p>
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4">
<p className="font-medium mb-2">
{isZh ? '解决方案:使用 customFetch' : 'Solution: use customFetch'}
</p>
<p className="text-sm mb-3">
{isZh
? '通过 customFetch 配置拦截请求,在发送前调整参数格式以适配目标供应商的要求。'
: 'Use the customFetch config to intercept requests and adapt parameters before sending them to the target provider.'}
</p>
<CodeEditor
code={`const agent = new PageAgent({
// ...
customFetch: async (url, init) => {
const body = JSON.parse(init.body)
// Adapt parameters for your provider
delete body.stream_options
return fetch(url, { ...init, body: JSON.stringify(body) })
},
})`}
/>
</div>
<p className="text-sm">
{isZh ? '参见 ' : 'See '}
<Link
href="/advanced/page-agent-core"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
PageAgentCore API
</Link>
{isZh ? ' 了解 customFetch 的完整用法。' : ' for full customFetch documentation.'}
</p>
</div>
)
}
// ---------------------------------------------------------------------------
// Color mapping for symptom callouts
// ---------------------------------------------------------------------------
const SYMPTOM_COLORS = {
red: 'border-red-400 bg-red-50 dark:bg-red-900/15 text-red-800 dark:text-red-200',
amber: 'border-amber-400 bg-amber-50 dark:bg-amber-900/15 text-amber-800 dark:text-amber-200',
orange:
'border-orange-400 bg-orange-50 dark:bg-orange-900/15 text-orange-800 dark:text-orange-200',
violet:
'border-violet-400 bg-violet-50 dark:bg-violet-900/15 text-violet-800 dark:text-violet-200',
} as const
// ---------------------------------------------------------------------------
// Right-side TOC with IntersectionObserver
// ---------------------------------------------------------------------------
function useActiveSection(ids: string[]) {
const [activeId, setActiveId] = useState(ids[0])
const observerRef = useRef<IntersectionObserver | null>(null)
useEffect(() => {
observerRef.current?.disconnect()
const visibleEntries = new Map<string, number>()
observerRef.current = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
visibleEntries.set(entry.target.id, entry.intersectionRatio)
} else {
visibleEntries.delete(entry.target.id)
}
}
// Pick the first visible section in document order
const firstVisible = ids.find((id) => visibleEntries.has(id))
if (firstVisible) setActiveId(firstVisible)
},
{ rootMargin: '-80px 0px -60% 0px', threshold: [0, 0.25] }
)
for (const id of ids) {
const el = document.getElementById(id)
if (el) observerRef.current.observe(el)
}
return () => observerRef.current?.disconnect()
}, [ids])
return activeId
}
// ---------------------------------------------------------------------------
// Page component
// ---------------------------------------------------------------------------
export default function TroubleshootingPage() {
const { isZh } = useLanguage()
const sectionIds = SECTIONS.map((s) => s.id)
const activeId = useActiveSection(sectionIds)
return (
<div className="max-w-5xl mx-auto">
{/* Header */}
<div className="mb-10">
<h1 className="text-4xl font-bold mb-4 text-gray-900 dark:text-white">Troubleshooting</h1>
</div>
{/* Two-column: content + TOC */}
<div className="flex gap-8">
{/* Main content */}
<div className="flex-1 min-w-0 space-y-12">
{SECTIONS.map((section) => (
<section key={section.id} id={section.id} className="scroll-mt-24">
<h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">
{isZh ? section.title.zh : section.title.en}
</h2>
{/* Symptom callout */}
<div
className={`border-l-4 px-4 py-3 rounded-r-lg mb-6 ${SYMPTOM_COLORS[section.color]}`}
>
<span className="text-xs font-semibold uppercase tracking-wider opacity-70">
{isZh ? '症状' : 'Symptom'}
</span>
<p className="mt-1 text-sm">{isZh ? section.symptom.zh : section.symptom.en}</p>
</div>
{/* Diagnostic steps */}
<div className="prose-sm">{section.content(isZh)}</div>
</section>
))}
</div>
{/* Right TOC */}
<aside className="hidden lg:block w-48 shrink-0">
<div className="sticky top-24">
<h4 className="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3">
{isZh ? '目录' : 'On this page'}
</h4>
<nav className="space-y-1">
{SECTIONS.map((section) => (
<button
key={section.id}
type="button"
onClick={() =>
document
.getElementById(section.id)
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
className={`block text-sm py-1 text-left transition-colors ${
activeId === section.id
? 'text-blue-600 dark:text-blue-400 font-medium'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
{isZh ? section.title.zh : section.title.en}
</button>
))}
</nav>
</div>
</aside>
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More