Compare commits
185 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17e92b6200 | |||
| 2e0cbef034 | |||
| 95b6785f05 | |||
| 2917941b13 | |||
| 31c496cc65 | |||
| cef39d5090 | |||
| 3063b2a06d | |||
| d66a390516 | |||
| aae6c93f11 | |||
| 2f5476b76c | |||
| 88ea2c6708 | |||
| 37760a97df | |||
| de76d97303 | |||
| 92196ac793 | |||
| 80dda37f8c | |||
| 5e7fe18463 | |||
| 3d014f15e7 | |||
| ce8f0fdbd8 | |||
| 48fede38fd | |||
| f303ed5fc5 | |||
| 6dbc33efd9 | |||
| 53628532df | |||
| 2aae34d74a | |||
| e27d1fb57f | |||
| 8ad4d19232 | |||
| 0fa7dc0531 | |||
| 89546887bd | |||
| b0bcba2922 | |||
| 618ae8bf14 | |||
| 5ed5d574fd | |||
| bde9d9023a | |||
| c8a8858d24 | |||
| 6491118cde | |||
| b4acd02007 | |||
| 5695b3d6aa | |||
| 0a160697d1 | |||
| 11d3ca3d6e | |||
| ee88b23891 | |||
| ca68be7a56 | |||
| 4ad2abe997 | |||
| 8f08322721 | |||
| fb043e5a81 | |||
| 1628d48c97 | |||
| 025cb3391e | |||
| 4aab7f991f | |||
| 67846db28f | |||
| cac55fa2bf | |||
| a74b7542ba | |||
| a9f462ed37 | |||
| c33b3d8278 | |||
| de3a6e4660 | |||
| e2c00b1bfc | |||
| 692ac4acca | |||
| c4bdd8fc4b | |||
| aa88221845 | |||
| b425e48afe | |||
| ff54b363aa | |||
| 4d931b4430 | |||
| 6054ca1217 | |||
| 7f9f0d1589 | |||
| ddcfa5b499 | |||
| 441b41c713 | |||
| ed71dd08b9 | |||
| 1c354ab5d3 | |||
| 151760713a | |||
| 4450d6302b | |||
| 2d055d3909 | |||
| 16da7d936d | |||
| efe08f445f | |||
| 4e7f755ae9 | |||
| 28bb2204e7 | |||
| 0c3b4592b2 | |||
| 222bbef670 | |||
| fa8ab451eb | |||
| b4ebb2b98f | |||
| c757270101 | |||
| 5873e68d63 | |||
| 0c124c3b44 | |||
| d46a57f8ef | |||
| 153fa23a71 | |||
| 5852054e3a | |||
| 5988d4ba24 | |||
| 0b6a698f6b | |||
| 01db520881 | |||
| 30b9956c4f | |||
| 4b37a3e538 | |||
| 928d8d2fb3 | |||
| 8ece20f54b | |||
| 26b4afca6a | |||
| 5f43b76b57 | |||
| 15d609aebc | |||
| d66bf8f3cf | |||
| d3b42036af | |||
| e448a43769 | |||
| 349de7bf48 | |||
| eec601e6b2 | |||
| 5b280a4a67 | |||
| 46deaf373f | |||
| e098d5701f | |||
| 21d4be0020 | |||
| 3f6a1856ac | |||
| 816e24a102 | |||
| 80e2a93a8c | |||
| 2f8096883f | |||
| 088f002583 | |||
| 27e9e78636 | |||
| a0c979602e | |||
| 3dc9edb8b5 | |||
| 32b68ab7c8 | |||
| 75a61c05c1 | |||
| bdfa98358b | |||
| dc8b38ccb9 | |||
| e1c288aaeb | |||
| 7a97de2a37 | |||
| 19b03b83ea | |||
| 598b144d06 | |||
| 1cd8a8de60 | |||
| 9beffca985 | |||
| 53db2069ce | |||
| 121104e13d | |||
| 0dacbda9da | |||
| 8bc27414a4 | |||
| 76509d93f0 | |||
| ff31b0c03e | |||
| 6b2ab73d65 | |||
| 2754023a80 | |||
| fa5c65db53 | |||
| 53c2c19d69 | |||
| e5437b445a | |||
| 09bdf9ddaf | |||
| b58d2a09ef | |||
| ce442742ba | |||
| 632f22962e | |||
| 72c3b12fb4 | |||
| 4807d550a3 | |||
| f9722f0619 | |||
| e1557a5d2e | |||
| 06280c2ba5 | |||
| 645a7ceb52 | |||
| a47c72ce84 | |||
| dcc6cd1cf3 | |||
| b4b9f6ce2d | |||
| 14e31e6ec4 | |||
| 69f2478fd7 | |||
| 5fec6846b0 | |||
| fe792f1ceb | |||
| d1d27a76de | |||
| 3e7e99420a | |||
| 0ffd221a61 | |||
| ee208b9779 | |||
| 6ad20c8f45 | |||
| ff4caaec22 | |||
| 91ae86f56e | |||
| c2daf13ead | |||
| ce2258fe16 | |||
| 68d68182e2 | |||
| 5f74f98a97 | |||
| 07b4dc32f7 | |||
| 9d5c8b2302 | |||
| 0c61ead38f | |||
| d92ad8f959 | |||
| a51772e191 | |||
| 89c83f454d | |||
| 7f67463a24 | |||
| 2311c1918a | |||
| b45a2af9bf | |||
| b4377ed152 | |||
| b7be29d792 | |||
| cbe223dac5 | |||
| 22fe448d95 | |||
| 49bdc70c78 | |||
| d6bd6a7170 | |||
| 2b99620de3 | |||
| d913dd7bd0 | |||
| 8cb57befb3 | |||
| b1be05309b | |||
| e5cf60df1b | |||
| 58a3d5fbc6 | |||
| 6abe75f199 | |||
| e27ebda960 | |||
| 9b603d90ba | |||
| b89228693d | |||
| 66cc9e0a3c | |||
| 53144a8021 | |||
| 43b7c1b136 |
@@ -9,7 +9,7 @@ body:
|
|||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ contact_links:
|
|||||||
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
||||||
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
||||||
- name: Code of Conduct / 行为准则
|
- name: Code of Conduct / 行为准则
|
||||||
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
|
url: https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md
|
||||||
about: Community expectations and standards. 社区行为期望与标准。
|
about: Community expectations and standards. 社区行为期望与标准。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ body:
|
|||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ Closes #(issue)
|
|||||||
|
|
||||||
## Requirements / 要求
|
## Requirements / 要求
|
||||||
|
|
||||||
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
||||||
|
- [ ] This PR is NOT generated by a bot or AI agent acting autonomously. I have authored or meaningfully reviewed every change. / 此 PR 不是由 bot 或 AI 自主生成的,我已亲自编写或充分审查了每一处变更。
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20, 24, 25]
|
node-version: [24]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -29,11 +29,6 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
# Copied files for npm publish (generated by prepublishOnly)
|
|
||||||
packages/page-agent/README.md
|
|
||||||
packages/page-agent/README-zh.md
|
|
||||||
packages/page-agent/LICENSE
|
|
||||||
|
|
||||||
# extension
|
# extension
|
||||||
.output
|
.output
|
||||||
.wxt
|
.wxt
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"contenteditable",
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"historychange",
|
"historychange",
|
||||||
"HITL",
|
"HITL",
|
||||||
|
|||||||
+33
-41
@@ -1,21 +1,23 @@
|
|||||||
# Contributing to Page-Agent
|
# Contributing to PageAgent
|
||||||
|
|
||||||
Thank you for your interest in contributing to Page-Agent! We welcome contributions from everyone.
|
♥️ We welcome contributions from everyone.
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Development Setup
|
### Development Setup
|
||||||
|
|
||||||
1. **Prerequisites**
|
1. **Prerequisites**
|
||||||
|
- `macOS` / `Linux` / `WSL`
|
||||||
- `node.js >= 20` with `npm >= 10`
|
- `node.js >= 20` with `npm >= 10`
|
||||||
- An editor that supports `ts/eslint/prettier`
|
- An editor that supports `ts/eslint/prettier`
|
||||||
- Make sure `eslint`, `prettier` and `commitlint` work well
|
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
|
||||||
|
|
||||||
2. **Setup**
|
2. **Setup**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm i
|
||||||
npm start # Start demo and documentation site
|
npm start # Start demo and documentation site
|
||||||
|
npm run build # Build libs and website
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
@@ -27,11 +29,11 @@ This is a **monorepo** with npm workspaces containing **4 main packages**:
|
|||||||
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
|
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
|
||||||
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||||
|
|
||||||
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
> We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
||||||
|
>
|
||||||
- When developing. Use alias so that we don't have to pre-build.
|
> - When developing. Use alias so that we don't have to pre-build.
|
||||||
- When bundling. Use external and disable ts `paths` alias to leave deps out.
|
> - When bundling. Use external and disable ts `paths` alias.
|
||||||
- When bundling `IIFE` and `Website`. Bundle everything including local packages.
|
> - When bundling `IIFE` and `Website`. Bundle everything together.
|
||||||
|
|
||||||
## 🤝 How to Contribute
|
## 🤝 How to Contribute
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
|
|||||||
- Update documentation as needed
|
- Update documentation as needed
|
||||||
|
|
||||||
4. **Test Your Changes**
|
4. **Test Your Changes**
|
||||||
|
- Build and lint everything.
|
||||||
- Test in our demo website
|
- Test in our demo website
|
||||||
- Test it on other websites if applicable
|
- Test it on other websites if applicable
|
||||||
- `@TODO: test suite`
|
- `@TODO: test suite`
|
||||||
@@ -90,14 +93,17 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
|
|||||||
- Keep functions small and focused
|
- Keep functions small and focused
|
||||||
- Add JSDoc for public APIs
|
- Add JSDoc for public APIs
|
||||||
|
|
||||||
### Vibe coding with AI
|
### Vibe Coding with AI
|
||||||
|
|
||||||
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**.
|
> [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)
|
||||||
- Be very careful if AI ever touched the core lib!!!
|
|
||||||
|
- Vibe coding is **RECOMMENDED** when maintaining **the demo, the website, the UI and tests**.
|
||||||
|
- We have a [website/AGENTS.md](packages/website/AGENTS.md) for that.
|
||||||
|
- Vibe coding is **NOT** allowed for the core lib!!!
|
||||||
|
- NEVER try to vibe coding the MV3 extension!!! It is HELL.
|
||||||
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
|
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
|
||||||
- Update the [AI instructions](AGENTS.md) when structure changed.
|
|
||||||
|
|
||||||
If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
|
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
|
||||||
|
|
||||||
- claude-code (`CLAUDE.md`)
|
- claude-code (`CLAUDE.md`)
|
||||||
|
|
||||||
@@ -135,26 +141,21 @@ If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add
|
|||||||
LLM_MODEL_NAME="qwen3:14b"
|
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`
|
> @see https://alibaba.github.io/page-agent/docs/features/models#ollama for configuration
|
||||||
|
|
||||||
- Restart the dev server to load new env vars
|
- **Restart the dev server** to load new env vars
|
||||||
- If not provided, the demo will the free testing proxy by default
|
- If not provided, the demo will use the free testing proxy by default. By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
|
||||||
|
|
||||||
### Website Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extension Development
|
### Extension Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# make sure you ran `npm run build:libs` first
|
||||||
|
# and every time you changed the core libs
|
||||||
npm run dev -w @page-agent/ext
|
npm run dev -w @page-agent/ext
|
||||||
npm run zip -w @page-agent/ext
|
npm run zip -w @page-agent/ext
|
||||||
```
|
```
|
||||||
|
|
||||||
- Load extension in Chrome via `chrome://extensions` -> **Load unpacked**
|
- Update `packages/extension/docs/extension_api.md` for API integration details
|
||||||
- Use `packages/extension/docs/extension_api.md` (EN) or `packages/extension/docs/extension_api_zh.md` (ZH) for API integration details
|
|
||||||
|
|
||||||
### Testing on Other Websites
|
### Testing on Other Websites
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ npm run zip -w @page-agent/ext
|
|||||||
|
|
||||||
- Click the bookmark on any page to load Page-Agent
|
- Click the bookmark on any page to load Page-Agent
|
||||||
|
|
||||||
> Warning: AK in your local `.env` will be inlined in the iife script.
|
> Warning: AK in your local `.env` will be inlined in the iife script. Be very careful when you distribute the script.
|
||||||
|
|
||||||
### Adding Documentation
|
### Adding Documentation
|
||||||
|
|
||||||
@@ -180,29 +181,20 @@ Ask an AI to help you add documentation to the `website/` package. Follow the ex
|
|||||||
|
|
||||||
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
|
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
|
||||||
|
|
||||||
## 🎯 Contribution Areas
|
|
||||||
|
|
||||||
We especially welcome contributions in:
|
|
||||||
|
|
||||||
- **Browser compatibility** improvements
|
|
||||||
- **Performance optimizations** for DOM processing
|
|
||||||
- **Documentation** and examples
|
|
||||||
- **Testing** and quality assurance
|
|
||||||
- **Accessibility** features
|
|
||||||
- **Internationalization** support
|
|
||||||
|
|
||||||
## 🚫 What We Don't Accept
|
## 🚫 What We Don't Accept
|
||||||
|
|
||||||
- Changes that break existing API compatibility (Discuss first)
|
- Breaking changes and large PRs without prior discussion
|
||||||
- Heavy dependencies to core library
|
- Heavy dependencies to core libs
|
||||||
- Contributions without proper testing
|
- Contributions without proper testing
|
||||||
- Code that doesn't follow project conventions
|
- Code that doesn't follow project conventions
|
||||||
|
- Dependencies or code with licenses incompatible with MIT
|
||||||
|
- Bot or AI-generated pull requests without meaningful human involvement
|
||||||
|
|
||||||
## 📄 Legal
|
## 📄 Legal
|
||||||
|
|
||||||
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
|
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
|
||||||
|
|
||||||
> You may need to sign a github CLA before you create a PR.
|
> CLA is optional.
|
||||||
|
|
||||||
## 💬 Questions?
|
## 💬 Questions?
|
||||||
|
|
||||||
@@ -210,4 +202,4 @@ By contributing to this project, you agree that your contributions will be licen
|
|||||||
- Check existing documentation and issues first
|
- Check existing documentation and issues first
|
||||||
- Be respectful and constructive in discussions
|
- Be respectful and constructive in discussions
|
||||||
|
|
||||||
Thank you for helping make Page-Agent better! 🎉
|
Thank you for helping make PageAgent better! 🎉
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 Alibaba
|
Copyright (c) 2026 SimonLuvRamen
|
||||||
Copyright (c) 2026 Simon
|
Copyright (c) 2026 Alibaba Group Holding Limited
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
# Page Agent
|
|
||||||
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
|
||||||
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
|
||||||
|
|
||||||
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
|
||||||
|
|
||||||
🌐 [English](./README.md) | **中文**
|
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- **🎯 轻松集成**
|
|
||||||
- 无需 `浏览器插件` / `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),支持跨页面任务。**
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 一行代码集成
|
|
||||||
|
|
||||||
通过我们免费的 Demo LLM 快速体验 PageAgent:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="{URL}" crossorigin="true"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
| Mirrors | URL |
|
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
|
||||||
| 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 安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install page-agent
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { PageAgent } from 'page-agent'
|
|
||||||
|
|
||||||
const agent = new PageAgent({
|
|
||||||
model: 'deepseek-chat',
|
|
||||||
baseURL: 'https://api.deepseek.com',
|
|
||||||
apiKey: 'YOUR_API_KEY',
|
|
||||||
language: 'zh-CN',
|
|
||||||
})
|
|
||||||
|
|
||||||
await agent.execute('点击登录按钮')
|
|
||||||
```
|
|
||||||
|
|
||||||
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/#/docs/introduction/overview)。
|
|
||||||
|
|
||||||
## 🏗️ 架构设计
|
|
||||||
|
|
||||||
PageAgent adopts a simplified monorepo structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/
|
|
||||||
├── core/ # ** Core agent logic (npm: @page-agent/core) **
|
|
||||||
├── llms/ # LLM 客户端 (npm: @page-agent/llms)
|
|
||||||
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller)
|
|
||||||
├── ui/ # 面板 UI (npm: @page-agent/ui)
|
|
||||||
├── page-agent/ # 入口类 & iife 包 (npm: page-agent)
|
|
||||||
├── extension/ # Chrome 扩展,支持跨页面任务
|
|
||||||
└── website/ # 网站 & 文档站点
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🤝 贡献
|
|
||||||
|
|
||||||
欢迎社区贡献!请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) 了解环境配置和本地开发说明。
|
|
||||||
|
|
||||||
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
|
|
||||||
|
|
||||||
## 👏 致谢
|
|
||||||
|
|
||||||
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
|
|
||||||
|
|
||||||
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
|
|
||||||
|
|
||||||
```
|
|
||||||
DOM processing components and prompt are derived from browser-use:
|
|
||||||
|
|
||||||
Browser Use
|
|
||||||
Copyright (c) 2024 Gregor Zunic
|
|
||||||
Licensed under the MIT License
|
|
||||||
|
|
||||||
Original browser-use project: <https://github.com/browser-use/browser-use>
|
|
||||||
|
|
||||||
We gratefully acknowledge the browser-use project and its contributors for their
|
|
||||||
excellent work on web automation and DOM interaction patterns that helped make
|
|
||||||
this project possible.
|
|
||||||
|
|
||||||
Third-party dependencies and their licenses can be found in the package.json
|
|
||||||
file and in the node_modules directory after installation.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 许可证
|
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
|
||||||
@@ -5,30 +5,35 @@
|
|||||||
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://www.npmjs.com/package/page-agent) [](https://bundlephobia.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://bundlephobia.com/package/page-agent) [](https://www.npmjs.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
||||||
|
|
||||||
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
||||||
|
|
||||||
🌐 **English** | [中文](./README-zh.md)
|
🌐 **English** | [中文](./docs/README-zh.md)
|
||||||
|
|
||||||
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/#/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a>
|
<a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
|
||||||
|
|
||||||
<video id="demo-video" src="https://github.com/user-attachments/assets/34d8444d-cbfb-44a3-a24e-fd5c167bb0bf" controls crossorigin muted></video>
|
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **🎯 Easy integration**
|
- **🎯 Easy integration**
|
||||||
- No need for `browser extension` / `python` / `headless browser`.
|
- No need for `browser extension` / `python` / `headless browser`.
|
||||||
- Just in-page javascript. Everything happens in your web page.
|
- Just in-page javascript. Everything happens in your web page.
|
||||||
- The best tool for your agent to control web pages.
|
|
||||||
- **📖 Text-based DOM manipulation**
|
- **📖 Text-based DOM manipulation**
|
||||||
- No screenshots. No OCR or multi-modal LLMs needed.
|
- No screenshots. No multi-modal LLMs or special permissions needed.
|
||||||
- No special permissions required.
|
|
||||||
- **🧠 Bring your own LLMs**
|
- **🧠 Bring your own LLMs**
|
||||||
- **🎨 Pretty UI with human-in-the-loop**
|
- **🎨 Pretty UI with human-in-the-loop**
|
||||||
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/#/docs/features/chrome-extension) for multi-page tasks.**
|
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
|
||||||
|
|
||||||
|
## 💡 Use Cases
|
||||||
|
|
||||||
|
- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
|
||||||
|
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
|
||||||
|
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
|
||||||
|
- **Multi-page Agent** — Extend your own agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -40,12 +45,12 @@ Fastest way to try PageAgent with our free Demo LLM:
|
|||||||
<script src="{URL}" crossorigin="true"></script>
|
<script src="{URL}" crossorigin="true"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
|
||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.3.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.9/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.3.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.5.9/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
> **⚠️ For technical evaluation only.** Demo LLM has rate limits and usage restrictions. Slow. May change without notice.
|
|
||||||
|
|
||||||
### NPM Installation
|
### NPM Installation
|
||||||
|
|
||||||
@@ -57,8 +62,8 @@ npm install page-agent
|
|||||||
import { PageAgent } from 'page-agent'
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
const agent = new PageAgent({
|
const agent = new PageAgent({
|
||||||
model: 'deepseek-chat',
|
model: 'qwen3.5-plus',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKey: 'YOUR_API_KEY',
|
apiKey: 'YOUR_API_KEY',
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
})
|
})
|
||||||
@@ -66,28 +71,15 @@ const agent = new PageAgent({
|
|||||||
await agent.execute('Click the login button')
|
await agent.execute('Click the login button')
|
||||||
```
|
```
|
||||||
|
|
||||||
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/#/docs/introduction/overview).
|
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 (npm: @page-agent/core) **
|
|
||||||
├── llms/ # LLM client (npm: @page-agent/llms)
|
|
||||||
├── 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
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development.
|
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
|
||||||
|
|
||||||
Please read [Code of Conduct](CODE_OF_CONDUCT.md) before contributing.
|
Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.
|
||||||
|
|
||||||
|
Contributions generated entirely by bots or agents without substantial human involvement will not be accepted, and bot accounts may be blocked.
|
||||||
|
|
||||||
## 👏 Acknowledgments
|
## 👏 Acknowledgments
|
||||||
|
|
||||||
@@ -98,12 +90,10 @@ This project builds upon the excellent work of **[`browser-use`](https://github.
|
|||||||
```
|
```
|
||||||
DOM processing components and prompt are derived from browser-use:
|
DOM processing components and prompt are derived from browser-use:
|
||||||
|
|
||||||
Browser Use
|
Browser Use <https://github.com/browser-use/browser-use>
|
||||||
Copyright (c) 2024 Gregor Zunic
|
Copyright (c) 2024 Gregor Zunic
|
||||||
Licensed under the MIT License
|
Licensed under the MIT License
|
||||||
|
|
||||||
Original browser-use project: <https://github.com/browser-use/browser-use>
|
|
||||||
|
|
||||||
We gratefully acknowledge the browser-use project and its contributors for their
|
We gratefully acknowledge the browser-use project and its contributors for their
|
||||||
excellent work on web automation and DOM interaction patterns that helped make
|
excellent work on web automation and DOM interaction patterns that helped make
|
||||||
this project possible.
|
this project possible.
|
||||||
@@ -119,3 +109,11 @@ file and in the node_modules directory after installation.
|
|||||||
---
|
---
|
||||||
|
|
||||||
**⭐ Star this repo if you find PageAgent helpful!**
|
**⭐ Star this repo if you find PageAgent helpful!**
|
||||||
|
|
||||||
|
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We provide security fixes on a best-effort basis for:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| --------------------------------------------------------- | --------- |
|
||||||
|
| `main` | Yes |
|
||||||
|
| Latest npm release of `page-agent` and workspace packages | Yes |
|
||||||
|
| Older releases | No |
|
||||||
|
|
||||||
|
Please upgrade to the latest release before reporting an issue against an older build.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
|
||||||
|
|
||||||
|
Use GitHub's private vulnerability reporting flow:
|
||||||
|
|
||||||
|
- Open https://github.com/alibaba/page-agent/security/policy
|
||||||
|
- Click `Report a vulnerability`
|
||||||
|
|
||||||
|
If private reporting is unavailable, open a minimal public issue only to request a private contact channel. Do not include exploit details.
|
||||||
|
|
||||||
|
## What to Include
|
||||||
|
|
||||||
|
- Affected package or feature
|
||||||
|
- Exact version, commit, or build
|
||||||
|
- Browser, OS, and runtime environment
|
||||||
|
- Reproduction steps or a proof of concept
|
||||||
|
- Expected impact
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
We prioritize reports that show a real security boundary failure, such as:
|
||||||
|
|
||||||
|
- Unauthorized access to data, tokens, or extension capabilities
|
||||||
|
- Bypassing explicit safety constraints
|
||||||
|
- Sensitive data exposure caused by default behavior
|
||||||
|
|
||||||
|
The following usually do not qualify by themselves:
|
||||||
|
|
||||||
|
- Unsafe custom integrations that ignore documented safeguards
|
||||||
|
- Intentionally embedding secrets into client-side builds
|
||||||
|
- Reports against unsupported older versions
|
||||||
|
|
||||||
|
## Disclosure
|
||||||
|
|
||||||
|
Please avoid public disclosure until maintainers have had a reasonable chance to investigate and ship a fix.
|
||||||
@@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.5.1] - 2026-03-05
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- **`data-browser-use-ignore` → `data-page-agent-ignore`** - DOM ignore attribute renamed to match the project identity
|
||||||
|
- **Config types restructured** - `PageAgentConfig` split into `AgentConfig` + `PageAgentCoreConfig`; config definitions moved from `config/index.ts` to `types.ts`
|
||||||
|
- **Zod v3/v4 dual support** - Libraries now accept both `zod@^3.25` and `zod@^4.0` as peer dependencies
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Experimental `llms.txt` support** - Agent can fetch and include a site's `llms.txt` in context. Enable via `experimentalLlmsTxt: true`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Default `maxSteps` changed from 20 to 40 for better for complex tasks out of the box
|
||||||
|
- Added 400ms wait between agent steps for page reactions
|
||||||
|
- Increased click wait time (100ms → 200ms) for more reliable interactions
|
||||||
|
- Removed debug `console.log` statements from scroll actions
|
||||||
|
- Reset observations on new task start
|
||||||
|
- Improved logging across packages
|
||||||
|
|
||||||
|
### Extension v0.1.9
|
||||||
|
|
||||||
|
> PageAgent 1.5.1
|
||||||
|
|
||||||
|
- **Advanced config panel** - New collapsible section exposing Max Steps, System Instruction, and experimental `llms.txt` toggle
|
||||||
|
- Streamlined User Auth Token description
|
||||||
|
- Moved testing API notice below auth token section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.4.0] - 2026-02-27
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Update Terms of Use and Privacy Policy
|
||||||
|
- **Robust tool-call validation** - Action inputs are now validated against tool schemas individually, producing clear error messages (e.g. `Invalid input for action "click_element_by_index"`) instead of unreadable union parse errors
|
||||||
|
- **Primitive action input coercion** - Small models that output `{"click_element_by_index": 2}` instead of `{"click_element_by_index": {"index": 2}}` are now auto-corrected using tool schemas
|
||||||
|
- **Qwen model updates** - Added `qwen3.5-plus` as the default free testing model; disabled `enable_thinking` for Qwen models to avoid incompatible responses
|
||||||
|
- **Updated default LLM endpoint** - Migrated demo and extension to a new testing endpoint with legacy endpoint auto-migration
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- Unified zod imports (`* as z`) across all packages for consistency
|
||||||
|
- Better Zod error formatting with `z.prettifyError()` in LLM client
|
||||||
|
- Exported `InvokeError` and `InvokeErrorType` as values (not just types) from `@page-agent/llms`
|
||||||
|
- Exported `SupportedLanguage` type from `@page-agent/core`
|
||||||
|
|
||||||
|
### Extension v0.1.8
|
||||||
|
|
||||||
|
- **Language setting** - Added language selector (System / English / 中文) in config panel
|
||||||
|
- **UI makeover** - New empty state with breathing glow and typing animation; ai-motion glow overlay while running; refined focus styles
|
||||||
|
- **Testing endpoint notice** - Shows terms of use notice when using the free testing API
|
||||||
|
- **Legacy endpoint migration** - Auto-migrates old Supabase testing endpoint to new endpoint on startup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.3.0] - 2026-02-13
|
## [1.3.0] - 2026-02-13
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
@@ -96,9 +153,10 @@ PageAgent is now ready for production use. The API is stable and breaking change
|
|||||||
- **Ask User Tool** - Agent can ask users for clarification
|
- **Ask User Tool** - Agent can ask users for clarification
|
||||||
- **i18n Support** - English and Chinese localization
|
- **i18n Support** - English and Chinese localization
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
// Version 1.0.0
|
||||||
interface PageAgentConfig {
|
interface PageAgentConfig {
|
||||||
// LLM Configuration (required)
|
// LLM Configuration (required)
|
||||||
baseURL: string
|
baseURL: string
|
||||||
@@ -146,7 +204,7 @@ interface PageAgentConfig {
|
|||||||
- Single-page application only (cannot navigate across pages)
|
- Single-page application only (cannot navigate across pages)
|
||||||
- No visual recognition (relies on DOM structure)
|
- No visual recognition (relies on DOM structure)
|
||||||
- Limited interaction support (no hover, drag-drop, canvas operations)
|
- Limited interaction support (no hover, drag-drop, canvas operations)
|
||||||
- See [Limitations](https://alibaba.github.io/page-agent/#/docs/introduction/limitations) for details
|
- See [Limitations](https://alibaba.github.io/page-agent/docs/introduction/limitations) for details
|
||||||
|
|
||||||
### Acknowledgments
|
### Acknowledgments
|
||||||
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Page Agent
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
|
||||||
|
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [](https://bundlephobia.com/package/page-agent) [](https://www.npmjs.com/package/page-agent) [](https://github.com/alibaba/page-agent)
|
||||||
|
|
||||||
|
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
||||||
|
|
||||||
|
🌐 [English](../README.md) | **中文**
|
||||||
|
|
||||||
|
<a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
|
||||||
|
|
||||||
|
<video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🎯 轻松集成**
|
||||||
|
- 无需 `浏览器插件` / `Python` / `无头浏览器`。
|
||||||
|
- 纯页面内 JavaScript,一切都在你的网页中完成。
|
||||||
|
- **📖 基于文本的 DOM 操作**
|
||||||
|
- 无需截图,无需多模态模型或特殊权限。
|
||||||
|
- **🧠 用你自己的 LLM**
|
||||||
|
- **🎨 精美 UI,支持人机协同**
|
||||||
|
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
|
||||||
|
|
||||||
|
## 💡 应用场景
|
||||||
|
|
||||||
|
- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
|
||||||
|
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
|
||||||
|
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
|
||||||
|
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Agent 跨标签页工作。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 一行代码集成
|
||||||
|
|
||||||
|
通过我们免费的 Demo LLM 快速体验 PageAgent:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="{URL}" crossorigin="true"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
|
||||||
|
|
||||||
|
| Mirrors | URL |
|
||||||
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.5.9/dist/iife/page-agent.demo.js |
|
||||||
|
| China | https://registry.npmmirror.com/page-agent/1.5.9/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
### NPM 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install page-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { PageAgent } from 'page-agent'
|
||||||
|
|
||||||
|
const agent = new PageAgent({
|
||||||
|
model: 'qwen3.5-plus',
|
||||||
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
|
apiKey: 'YOUR_API_KEY',
|
||||||
|
language: 'zh-CN',
|
||||||
|
})
|
||||||
|
|
||||||
|
await agent.execute('点击登录按钮')
|
||||||
|
```
|
||||||
|
|
||||||
|
更多编程用法,请参阅 [📖 文档](https://alibaba.github.io/page-agent/docs/introduction/overview)。
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)。
|
||||||
|
|
||||||
|
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码,机器人账号可能被禁止参与互动。
|
||||||
|
|
||||||
|
## 👏 致谢
|
||||||
|
|
||||||
|
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
|
||||||
|
|
||||||
|
`PageAgent` 专为**客户端网页增强**设计,不是服务端自动化工具。
|
||||||
|
|
||||||
|
```
|
||||||
|
DOM processing components and prompt are derived from browser-use:
|
||||||
|
|
||||||
|
Browser Use <https://github.com/browser-use/browser-use>
|
||||||
|
Copyright (c) 2024 Gregor Zunic
|
||||||
|
Licensed under the MIT License
|
||||||
|
|
||||||
|
We gratefully acknowledge the browser-use project and its contributors for their
|
||||||
|
excellent work on web automation and DOM interaction patterns that helped make
|
||||||
|
this project possible.
|
||||||
|
|
||||||
|
Third-party dependencies and their licenses can be found in the package.json
|
||||||
|
file and in the node_modules directory after installation.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
[MIT License](../LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
||||||
|
|
||||||
|
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Terms of Use & Privacy
|
||||||
|
|
||||||
|
**Last updated:** March 2026
|
||||||
|
|
||||||
|
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). "The software" refers to Page Agent (the JavaScript library) and Page Agent Ext (the browser extension). This document covers the software itself and the testing API we provide — **not** any third-party product or service built with it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Open Source Software Privacy
|
||||||
|
|
||||||
|
The software is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and we do **not** have access to your browsing activity, page content, or task instructions through the software.
|
||||||
|
|
||||||
|
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
|
||||||
|
|
||||||
|
The project is open source under the [MIT License](https://github.com/alibaba/page-agent/blob/main/LICENSE) and can be audited at: https://github.com/alibaba/page-agent
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Testing API and Demo Disclaimer & Terms of Use
|
||||||
|
|
||||||
|
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of the software.
|
||||||
|
|
||||||
|
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this API, you agree to the following terms:
|
||||||
|
|
||||||
|
- **Permitted Use Only**: This API must be used solely for technical evaluation of the software. Any other use — including integration into other products or services, unlawful activities, violation of the underlying LLM provider's usage policies, or automated scraping at scale — is strictly prohibited.
|
||||||
|
|
||||||
|
- **No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
|
||||||
|
|
||||||
|
- **Data Processing**: We do not store or log your prompts, webpage data (HTML), or any submitted content, nor do we use such data for model training. All data is processed in-transit and immediately discarded. We perform in-memory request validation to prevent abuse of the testing API, and temporarily process IP addresses for rate-limiting purposes. No data from these processes is retained. Data is processed through Alibaba Cloud infrastructure, which is subject to its own privacy policy.
|
||||||
|
|
||||||
|
- **Independent Infrastructure**: The software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and **no built-in backend**. To facilitate easy testing, the maintainers have purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) Function Compute and BaiLian Qwen models). This project is not a product of, nor endorsed by, Alibaba Cloud.
|
||||||
|
|
||||||
|
- **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
|
||||||
|
|
||||||
|
- **"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this service.
|
||||||
|
|
||||||
|
- **Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
|
||||||
|
|
||||||
|
**Note**: This free testing LLM API processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this API.
|
||||||
|
|
||||||
|
**Age Requirement**: The software and testing API are not intended for use by individuals under the age of 13 (or the minimum age of digital consent in your jurisdiction).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Browser Extension (Page Agent Ext)
|
||||||
|
|
||||||
|
### Data Processing
|
||||||
|
|
||||||
|
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension developer.
|
||||||
|
|
||||||
|
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
|
||||||
|
|
||||||
|
- Your task instructions (natural language commands)
|
||||||
|
- Simplified page structure (cleaned HTML) of all pages under the extension's control
|
||||||
|
|
||||||
|
are sent to the LLM API endpoint configured in **your settings**.
|
||||||
|
|
||||||
|
> **Note:** The HTML cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
|
||||||
|
|
||||||
|
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
|
||||||
|
|
||||||
|
**If you use the testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` (or equivalent browser storage APIs)
|
||||||
|
- **No cloud sync**: Configuration is not synced to any external server
|
||||||
|
- **No analytics**: The extension does not include any analytics or tracking code
|
||||||
|
|
||||||
|
### Your Control
|
||||||
|
|
||||||
|
- The extension is open source and can be audited by anyone
|
||||||
|
- You choose which LLM provider to use
|
||||||
|
- You may configure your own API endpoint at any time
|
||||||
|
- You can clear all stored data by removing the extension
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
We may update these terms at our discretion.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
https://github.com/alibaba/page-agent/issues
|
||||||
Generated
+1734
-964
File diff suppressed because it is too large
Load Diff
+22
-19
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"packages/llms",
|
"packages/llms",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/page-agent",
|
"packages/page-agent",
|
||||||
|
"packages/mcp",
|
||||||
"packages/extension",
|
"packages/extension",
|
||||||
"packages/website"
|
"packages/website"
|
||||||
],
|
],
|
||||||
@@ -24,43 +25,45 @@
|
|||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf packages/*/dist",
|
|
||||||
"start": "npm run dev --workspace=@page-agent/website",
|
"start": "npm run dev --workspace=@page-agent/website",
|
||||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
"dev:ext": "npm run dev -w @page-agent/ext",
|
||||||
"build:libs": "npm run build --workspaces --if-present",
|
|
||||||
"build": "npm run build:libs && npm run build:website",
|
|
||||||
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
||||||
|
"build": "npm run build:libs && npm run build:website",
|
||||||
|
"build:libs": "npm run build --workspaces --if-present",
|
||||||
|
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||||
|
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
|
||||||
"version": "node scripts/sync-version.js",
|
"version": "node scripts/sync-version.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"cleanup": "rm -rf packages/*/dist",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.3.1",
|
"@commitlint/cli": "^20.5.0",
|
||||||
"@commitlint/config-conventional": "^20.3.1",
|
"@commitlint/config-conventional": "^20.5.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@microsoft/api-extractor": "^7.56.3",
|
"@microsoft/api-extractor": "^7.57.7",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@types/node": "^25.2.2",
|
"@types/node": "^25.5.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.3.1",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-dom": "^2.12.2",
|
"eslint-plugin-react-dom": "^2.13.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-react-x": "^2.12.2",
|
"eslint-plugin-react-x": "^2.13.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.2.4",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "^3.8.0",
|
"prettier": "^3.8.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.55.0",
|
"typescript-eslint": "^8.57.1",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0-beta.6",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
"vite-plugin-css-injected-by-js": "^4.0.1",
|
||||||
"vite-bundle-analyzer": "^1.3.6"
|
"vite-bundle-analyzer": "^1.3.6"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent-core.js",
|
"main": "./dist/esm/page-agent-core.js",
|
||||||
"module": "./dist/esm/page-agent-core.js",
|
"module": "./dist/esm/page-agent-core.js",
|
||||||
@@ -44,8 +44,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"zod": "^4.3.5",
|
"@page-agent/llms": "1.5.9",
|
||||||
"@page-agent/llms": "1.3.0",
|
"@page-agent/page-controller": "1.5.9"
|
||||||
"@page-agent/page-controller": "1.3.0"
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||||
|
* Copyright (C) 2026 SimonLuvRamen
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*/
|
*/
|
||||||
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
import { InvokeError, LLM, type Tool } from '@page-agent/llms'
|
||||||
import type { BrowserState, PageController } from '@page-agent/page-controller'
|
import type { BrowserState, PageController } from '@page-agent/page-controller'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as zod from 'zod'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import { type PageAgentConfig } from './config'
|
|
||||||
import { DEFAULT_MAX_STEPS } from './config/constants'
|
|
||||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||||
import { tools } from './tools'
|
import { tools } from './tools'
|
||||||
import type {
|
import type {
|
||||||
AgentActivity,
|
AgentActivity,
|
||||||
|
AgentConfig,
|
||||||
AgentReflection,
|
AgentReflection,
|
||||||
AgentStatus,
|
AgentStatus,
|
||||||
AgentStepEvent,
|
AgentStepEvent,
|
||||||
@@ -21,12 +21,13 @@ import type {
|
|||||||
MacroToolInput,
|
MacroToolInput,
|
||||||
MacroToolResult,
|
MacroToolResult,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { assert, normalizeResponse, uid, waitFor } from './utils'
|
import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
|
||||||
|
|
||||||
export { type PageAgentConfig }
|
|
||||||
export { tool, type PageAgentTool } from './tools'
|
export { tool, type PageAgentTool } from './tools'
|
||||||
export type * from './types'
|
export type * from './types'
|
||||||
|
|
||||||
|
export type PageAgentCoreConfig = AgentConfig & { pageController: PageController }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI agent for browser automation.
|
* AI agent for browser automation.
|
||||||
*
|
*
|
||||||
@@ -59,7 +60,7 @@ export type * from './types'
|
|||||||
*/
|
*/
|
||||||
export class PageAgentCore extends EventTarget {
|
export class PageAgentCore extends EventTarget {
|
||||||
readonly id = uid()
|
readonly id = uid()
|
||||||
readonly config: PageAgentConfig & { maxSteps: number }
|
readonly config: PageAgentCoreConfig & { maxSteps: number }
|
||||||
readonly tools: typeof tools
|
readonly tools: typeof tools
|
||||||
/** PageController for DOM operations */
|
/** PageController for DOM operations */
|
||||||
readonly pageController: PageController
|
readonly pageController: PageController
|
||||||
@@ -93,10 +94,10 @@ export class PageAgentCore extends EventTarget {
|
|||||||
browserState: null as BrowserState | null,
|
browserState: null as BrowserState | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config: PageAgentConfig & { pageController: PageController }) {
|
constructor(config: PageAgentCoreConfig) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.config = { ...config, maxSteps: config.maxSteps || DEFAULT_MAX_STEPS }
|
this.config = { ...config, maxSteps: config.maxSteps ?? 40 }
|
||||||
|
|
||||||
this.#llm = new LLM(this.config)
|
this.#llm = new LLM(this.config)
|
||||||
this.tools = new Map(tools)
|
this.tools = new Map(tools)
|
||||||
@@ -176,7 +177,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push a observation message to the history event stream.
|
* Push an observation message to the history event stream.
|
||||||
* This will be visible in <agent_history> and remain persistent in memory across steps.
|
* This will be visible in <agent_history> and remain persistent in memory across steps.
|
||||||
* @experimental @internal
|
* @experimental @internal
|
||||||
* @note history change will be emitted before next step starts
|
* @note history change will be emitted before next step starts
|
||||||
@@ -221,6 +222,7 @@ export class PageAgentCore extends EventTarget {
|
|||||||
this.history = []
|
this.history = []
|
||||||
this.#setStatus('running')
|
this.#setStatus('running')
|
||||||
this.#emitHistoryChange()
|
this.#emitHistoryChange()
|
||||||
|
this.#observations = []
|
||||||
|
|
||||||
// Reset internal states
|
// Reset internal states
|
||||||
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
|
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
|
||||||
@@ -247,16 +249,16 @@ export class PageAgentCore extends EventTarget {
|
|||||||
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
|
{ role: 'user' as const, content: await this.#assembleUserPrompt() },
|
||||||
]
|
]
|
||||||
|
|
||||||
const tools = { AgentOutput: this.#packMacroTool() }
|
const macroTool = { AgentOutput: this.#packMacroTool() }
|
||||||
|
|
||||||
// invoke LLM
|
// invoke LLM
|
||||||
|
|
||||||
console.log(chalk.blue.bold('🧠 Thinking...'))
|
console.log(chalk.blue.bold('🧠 Thinking...'))
|
||||||
this.#emitActivity({ type: 'thinking' })
|
this.#emitActivity({ type: 'thinking' })
|
||||||
|
|
||||||
const result = await this.#llm.invoke(messages, tools, this.#abortController.signal, {
|
const result = await this.#llm.invoke(messages, macroTool, this.#abortController.signal, {
|
||||||
toolChoiceName: 'AgentOutput',
|
toolChoiceName: 'AgentOutput',
|
||||||
normalizeResponse,
|
normalizeResponse: (res) => normalizeResponse(res, this.tools),
|
||||||
})
|
})
|
||||||
|
|
||||||
// assemble history
|
// assemble history
|
||||||
@@ -341,6 +343,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
await onAfterTask?.(this, result)
|
await onAfterTask?.(this, result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await waitFor(this.config.stepDelay ?? 0.4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,24 +361,22 @@ export class PageAgentCore extends EventTarget {
|
|||||||
const tools = this.tools
|
const tools = this.tools
|
||||||
|
|
||||||
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
const actionSchemas = Array.from(tools.entries()).map(([toolName, tool]) => {
|
||||||
return zod.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
return z.object({ [toolName]: tool.inputSchema }).describe(tool.description)
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionSchema = zod.union(
|
const actionSchema = z.union(actionSchemas as unknown as [z.ZodType, z.ZodType, ...z.ZodType[]])
|
||||||
actionSchemas as unknown as [zod.ZodType, zod.ZodType, ...zod.ZodType[]]
|
|
||||||
)
|
|
||||||
|
|
||||||
const macroToolSchema = zod.object({
|
const macroToolSchema = z.object({
|
||||||
// thinking: zod.string().optional(),
|
// thinking: z.string().optional(),
|
||||||
evaluation_previous_goal: zod.string().optional(),
|
evaluation_previous_goal: z.string().optional(),
|
||||||
memory: zod.string().optional(),
|
memory: z.string().optional(),
|
||||||
next_goal: zod.string().optional(),
|
next_goal: z.string().optional(),
|
||||||
action: actionSchema,
|
action: actionSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: 'You MUST call this tool every step!',
|
description: 'You MUST call this tool every step!',
|
||||||
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
|
inputSchema: macroToolSchema as z.ZodType<MacroToolInput>,
|
||||||
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
|
||||||
// abort
|
// abort
|
||||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||||
@@ -461,14 +463,13 @@ export class PageAgentCore extends EventTarget {
|
|||||||
* Get instructions from config
|
* Get instructions from config
|
||||||
*/
|
*/
|
||||||
async #getInstructions(): Promise<string> {
|
async #getInstructions(): Promise<string> {
|
||||||
const { instructions } = this.config
|
const { instructions, experimentalLlmsTxt } = this.config
|
||||||
if (!instructions) return ''
|
|
||||||
|
|
||||||
const systemInstructions = instructions.system?.trim()
|
const systemInstructions = instructions?.system?.trim()
|
||||||
let pageInstructions: string | undefined
|
let pageInstructions: string | undefined
|
||||||
|
|
||||||
const url = this.#states.browserState?.url || ''
|
const url = this.#states.browserState?.url || ''
|
||||||
if (instructions.getPageInstructions && url) {
|
if (instructions?.getPageInstructions && url) {
|
||||||
try {
|
try {
|
||||||
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
pageInstructions = instructions.getPageInstructions(url)?.trim()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -478,7 +479,10 @@ export class PageAgentCore extends EventTarget {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!systemInstructions && !pageInstructions) return ''
|
|
||||||
|
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : undefined
|
||||||
|
|
||||||
|
if (!systemInstructions && !pageInstructions && !llmsTxt) return ''
|
||||||
|
|
||||||
let result = '<instructions>\n'
|
let result = '<instructions>\n'
|
||||||
|
|
||||||
@@ -490,6 +494,10 @@ export class PageAgentCore extends EventTarget {
|
|||||||
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (llmsTxt) {
|
||||||
|
result += `<llms_txt>\n${llmsTxt}\n</llms_txt>\n`
|
||||||
|
}
|
||||||
|
|
||||||
result += '</instructions>\n\n'
|
result += '</instructions>\n\n'
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -504,7 +512,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
// Accumulated wait time warning
|
// Accumulated wait time warning
|
||||||
if (this.#states.totalWaitTime >= 3) {
|
if (this.#states.totalWaitTime >= 3) {
|
||||||
this.pushObservation(
|
this.pushObservation(
|
||||||
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
|
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
|
||||||
|
`DO NOT wait any longer unless you have a good reason.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +529,8 @@ export class PageAgentCore extends EventTarget {
|
|||||||
const remaining = this.config.maxSteps - step
|
const remaining = this.config.maxSteps - step
|
||||||
if (remaining === 5) {
|
if (remaining === 5) {
|
||||||
this.pushObservation(
|
this.pushObservation(
|
||||||
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
|
`⚠️ Only ${remaining} steps remaining. ` +
|
||||||
|
`Consider wrapping up or calling done with partial results.`
|
||||||
)
|
)
|
||||||
} else if (remaining === 2) {
|
} else if (remaining === 2) {
|
||||||
this.pushObservation(
|
this.pushObservation(
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const DEFAULT_MAX_STEPS = 20
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
|
||||||
import type { PageControllerConfig } from '@page-agent/page-controller'
|
|
||||||
|
|
||||||
import type { PageAgentCore } from '../PageAgentCore'
|
|
||||||
import type { PageAgentTool } from '../tools'
|
|
||||||
import type { ExecutionResult, HistoricalEvent } from '../types'
|
|
||||||
|
|
||||||
export type { LLMConfig }
|
|
||||||
|
|
||||||
/** Supported UI languages */
|
|
||||||
export type SupportedLanguage = 'en-US' | 'zh-CN'
|
|
||||||
|
|
||||||
export interface AgentConfig {
|
|
||||||
// theme?: 'light' | 'dark'
|
|
||||||
language?: SupportedLanguage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum number of steps the agent can take per task.
|
|
||||||
* @default 20
|
|
||||||
*/
|
|
||||||
maxSteps?: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom tools to extend PageAgent capabilities
|
|
||||||
* @experimental
|
|
||||||
* @note You can also override or remove internal tools by using the same name.
|
|
||||||
* @see PageAgentTool
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // override internal tool
|
|
||||||
* import { tool } from 'page-agent'
|
|
||||||
* 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
|
|
||||||
* },
|
|
||||||
* })
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // remove internal tool
|
|
||||||
* const customTools = {
|
|
||||||
* ask_user: null // never ask user questions
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
customTools?: Record<string, PageAgentTool | null>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instructions to guide the agent's behavior
|
|
||||||
*/
|
|
||||||
instructions?: {
|
|
||||||
/**
|
|
||||||
* Global system-level instructions, applied to all tasks
|
|
||||||
*/
|
|
||||||
system?: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dynamic page-level instructions callback
|
|
||||||
* Called before each step to get instructions for the current page
|
|
||||||
* @param url - Current page URL (window.location.href)
|
|
||||||
* @returns Instructions string, or undefined/null to skip
|
|
||||||
*/
|
|
||||||
getPageInstructions?: (url: string) => string | undefined | null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle hooks for task execution.
|
|
||||||
* @experimental API may change in future versions.
|
|
||||||
*
|
|
||||||
* All hooks receive the agent instance as first parameter.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before each step execution.
|
|
||||||
* @experimental
|
|
||||||
* @param agent - The PageAgentCore instance
|
|
||||||
* @param stepCount - Current step number (0-indexed)
|
|
||||||
*/
|
|
||||||
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after each step execution.
|
|
||||||
* @experimental
|
|
||||||
* @param agent - The PageAgentCore instance
|
|
||||||
* @param history - Current history of events
|
|
||||||
*/
|
|
||||||
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before task execution starts.
|
|
||||||
* @experimental
|
|
||||||
* @param agent - The PageAgentCore instance
|
|
||||||
*/
|
|
||||||
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after task execution completes (success or failure).
|
|
||||||
* @experimental
|
|
||||||
* @param agent - The PageAgentCore instance
|
|
||||||
* @param result - The execution result
|
|
||||||
*/
|
|
||||||
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the agent is disposed.
|
|
||||||
* @experimental
|
|
||||||
* @note This hook can block the disposal process if it's async.
|
|
||||||
* @param agent - The PageAgentCore instance
|
|
||||||
* @param reason - Optional reason for disposal
|
|
||||||
*/
|
|
||||||
onDispose?: (agent: PageAgentCore, reason?: string) => void
|
|
||||||
|
|
||||||
// page behavior hooks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
|
|
||||||
* @note Can cause unpredictable side effects.
|
|
||||||
* @note May bypass some safe guards and data-masking mechanisms.
|
|
||||||
*/
|
|
||||||
experimentalScriptExecutionTool?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform page content before sending to LLM.
|
|
||||||
* Called after DOM extraction and simplification, before LLM invocation.
|
|
||||||
* Use cases: inspect extraction results, modify page info, mask sensitive data.
|
|
||||||
*
|
|
||||||
* @param content - Simplified page content that will be sent to LLM
|
|
||||||
* @returns Transformed content
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Mask phone numbers
|
|
||||||
* transformPageContent: async (content) => {
|
|
||||||
* return content.replace(/1[3-9]\d{9}/g, '***********')
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
transformPageContent?: (content: string) => Promise<string> | string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Completely override the default system prompt.
|
|
||||||
* @experimental Use with caution - incorrect prompts may break agent behavior.
|
|
||||||
*/
|
|
||||||
customSystemPrompt?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
|
||||||
@@ -69,7 +69,7 @@ Strictly follow these rules while using the browser and navigating the web:
|
|||||||
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
|
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
|
||||||
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
|
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
|
||||||
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
|
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
|
||||||
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it.
|
- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
|
||||||
- If expected elements are missing, try scrolling, or navigating back.
|
- If expected elements are missing, try scrolling, or navigating back.
|
||||||
- If the page is not fully loaded, use the `wait` action.
|
- If the page is not fully loaded, use the `wait` action.
|
||||||
- Do not repeat one action for more than 3 times unless some conditions changed.
|
- Do not repeat one action for more than 3 times unless some conditions changed.
|
||||||
@@ -87,11 +87,11 @@ Strictly follow these rules while using the browser and navigating the web:
|
|||||||
|
|
||||||
<capability>
|
<capability>
|
||||||
- You can only handle single page app. Do not jump out of current page.
|
- You can only handle single page app. Do not jump out of current page.
|
||||||
- Do not click on link if it will open in a new page (etc. <a target="_blank">)
|
- Do not click on link if it will open in a new page (e.g., <a target="_blank">)
|
||||||
- It is ok to fail the task.
|
- It is ok to fail the task.
|
||||||
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
|
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
|
||||||
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
|
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
|
||||||
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail.
|
- Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.
|
||||||
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
|
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
|
||||||
</capability>
|
</capability>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Internal tools for PageAgent.
|
* Internal tools for PageAgent.
|
||||||
* @note Adapted from browser-use
|
* @note Adapted from browser-use
|
||||||
*/
|
*/
|
||||||
import * as zod from 'zod'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import type { PageAgentCore } from '../PageAgentCore'
|
import type { PageAgentCore } from '../PageAgentCore'
|
||||||
import { waitFor } from '../utils'
|
import { waitFor } from '../utils'
|
||||||
@@ -13,7 +13,7 @@ import { waitFor } from '../utils'
|
|||||||
export interface PageAgentTool<TParams = any> {
|
export interface PageAgentTool<TParams = any> {
|
||||||
// name: string
|
// name: string
|
||||||
description: string
|
description: string
|
||||||
inputSchema: zod.ZodType<TParams>
|
inputSchema: z.ZodType<TParams>
|
||||||
execute: (this: PageAgentCore, args: TParams) => Promise<string>
|
execute: (this: PageAgentCore, args: TParams) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
|
'Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
success: zod.boolean().default(true),
|
success: z.boolean().default(true),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
// @note main loop will handle this one
|
// @note main loop will handle this one
|
||||||
@@ -47,8 +47,8 @@ tools.set(
|
|||||||
'wait',
|
'wait',
|
||||||
tool({
|
tool({
|
||||||
description: 'Wait for x seconds. 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({
|
inputSchema: z.object({
|
||||||
seconds: zod.number().min(1).max(10).default(1),
|
seconds: z.number().min(1).max(10).default(1),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
// try to subtract LLM calling time from the actual wait time
|
// try to subtract LLM calling time from the actual wait time
|
||||||
@@ -67,8 +67,8 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
|
'Ask the user a question and wait for their answer. Use this if you need more information or clarification.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
question: zod.string(),
|
question: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
if (!this.onAskUser) {
|
if (!this.onAskUser) {
|
||||||
@@ -84,8 +84,8 @@ tools.set(
|
|||||||
'click_element_by_index',
|
'click_element_by_index',
|
||||||
tool({
|
tool({
|
||||||
description: 'Click element by index',
|
description: 'Click element by index',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.clickElement(input.index)
|
const result = await this.pageController.clickElement(input.index)
|
||||||
@@ -98,9 +98,9 @@ tools.set(
|
|||||||
'input_text',
|
'input_text',
|
||||||
tool({
|
tool({
|
||||||
description: 'Click and type text into an interactive input element',
|
description: 'Click and type text into an interactive input element',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.inputText(input.index, input.text)
|
const result = await this.pageController.inputText(input.index, input.text)
|
||||||
@@ -114,9 +114,9 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Select dropdown option for interactive element index by the text of the option you want to select',
|
'Select dropdown option for interactive element index by the text of the option you want to select',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
index: zod.int().min(0),
|
index: z.int().min(0),
|
||||||
text: zod.string(),
|
text: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.selectOption(input.index, input.text)
|
const result = await this.pageController.selectOption(input.index, input.text)
|
||||||
@@ -132,11 +132,11 @@ tools.set(
|
|||||||
'scroll',
|
'scroll',
|
||||||
tool({
|
tool({
|
||||||
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
down: zod.boolean().default(true),
|
down: z.boolean().default(true),
|
||||||
num_pages: zod.number().min(0).max(10).optional().default(0.1),
|
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
||||||
pixels: zod.number().int().min(0).optional(),
|
pixels: z.number().int().min(0).optional(),
|
||||||
index: zod.number().int().min(0).optional(),
|
index: z.number().int().min(0).optional(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scroll({
|
const result = await this.pageController.scroll({
|
||||||
@@ -156,10 +156,10 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
|
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
right: zod.boolean().default(true),
|
right: z.boolean().default(true),
|
||||||
pixels: zod.number().int().min(0),
|
pixels: z.number().int().min(0),
|
||||||
index: zod.number().int().min(0).optional(),
|
index: z.number().int().min(0).optional(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.scrollHorizontally(input)
|
const result = await this.pageController.scrollHorizontally(input)
|
||||||
@@ -173,8 +173,8 @@ tools.set(
|
|||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
|
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
script: zod.string(),
|
script: z.string(),
|
||||||
}),
|
}),
|
||||||
execute: async function (this: PageAgentCore, input) {
|
execute: async function (this: PageAgentCore, input) {
|
||||||
const result = await this.pageController.executeJavascript(input.script)
|
const result = await this.pageController.executeJavascript(input.script)
|
||||||
|
|||||||
@@ -1,3 +1,165 @@
|
|||||||
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
|
|
||||||
|
// @note circular dependency but okay
|
||||||
|
import type { PageAgentCore } from './PageAgentCore'
|
||||||
|
import type { PageAgentTool } from './tools'
|
||||||
|
|
||||||
|
/** Supported UI languages */
|
||||||
|
export type SupportedLanguage = 'en-US' | 'zh-CN'
|
||||||
|
|
||||||
|
export interface AgentConfig extends LLMConfig {
|
||||||
|
language?: SupportedLanguage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of steps the agent can take per task.
|
||||||
|
* @default 40
|
||||||
|
*/
|
||||||
|
maxSteps?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom tools to extend PageAgent capabilities
|
||||||
|
* @experimental
|
||||||
|
* @note You can also override or remove internal tools by using the same name.
|
||||||
|
* @see PageAgentTool
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // override internal tool
|
||||||
|
* import { z } from 'zod/v4'
|
||||||
|
* import { tool } from 'page-agent'
|
||||||
|
* 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: z.object({
|
||||||
|
* question: z.string(),
|
||||||
|
* }),
|
||||||
|
* execute: async function (this: PageAgent, input) {
|
||||||
|
* const answer = await do_some_thing(input.question)
|
||||||
|
* return "✅ Received user answer: " + answer
|
||||||
|
* },
|
||||||
|
* })
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // remove internal tool
|
||||||
|
* const customTools = {
|
||||||
|
* ask_user: null // never ask user questions
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
customTools?: Record<string, PageAgentTool | null>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instructions to guide the agent's behavior
|
||||||
|
*/
|
||||||
|
instructions?: {
|
||||||
|
/**
|
||||||
|
* Global system-level instructions, applied to all tasks
|
||||||
|
*/
|
||||||
|
system?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic page-level instructions callback
|
||||||
|
* Called before each step to get instructions for the current page
|
||||||
|
* @param url - Current page URL (window.location.href)
|
||||||
|
* @returns Instructions string, or undefined/null to skip
|
||||||
|
*/
|
||||||
|
getPageInstructions?: (url: string) => string | undefined | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hooks for task execution.
|
||||||
|
* @experimental API may change in future versions.
|
||||||
|
*
|
||||||
|
* All hooks receive the agent instance as first parameter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before each step execution.
|
||||||
|
* @experimental
|
||||||
|
* @param agent - The PageAgentCore instance
|
||||||
|
* @param stepCount - Current step number (0-indexed)
|
||||||
|
*/
|
||||||
|
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after each step execution.
|
||||||
|
* @experimental
|
||||||
|
* @param agent - The PageAgentCore instance
|
||||||
|
* @param history - Current history of events
|
||||||
|
*/
|
||||||
|
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before task execution starts.
|
||||||
|
* @experimental
|
||||||
|
* @param agent - The PageAgentCore instance
|
||||||
|
*/
|
||||||
|
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after task execution completes (success or failure).
|
||||||
|
* @experimental
|
||||||
|
* @param agent - The PageAgentCore instance
|
||||||
|
* @param result - The execution result
|
||||||
|
*/
|
||||||
|
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the agent is disposed.
|
||||||
|
* @experimental
|
||||||
|
* @note This hook can block the disposal process if it's async.
|
||||||
|
* @param agent - The PageAgentCore instance
|
||||||
|
* @param reason - Optional reason for disposal
|
||||||
|
*/
|
||||||
|
onDispose?: (agent: PageAgentCore, reason?: string) => void
|
||||||
|
|
||||||
|
// page behavior hooks
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
|
||||||
|
* @note Can cause unpredictable side effects.
|
||||||
|
* @note May bypass some safe guards and data-masking mechanisms.
|
||||||
|
*/
|
||||||
|
experimentalScriptExecutionTool?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
* Fetch /llms.txt from current site origin and include as context.
|
||||||
|
* Only fetched once per origin per task.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
experimentalLlmsTxt?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform page content before sending to LLM.
|
||||||
|
* Called after DOM extraction and simplification, before LLM invocation.
|
||||||
|
* Use cases: inspect extraction results, modify page info, mask sensitive data.
|
||||||
|
*
|
||||||
|
* @param content - Simplified page content that will be sent to LLM
|
||||||
|
* @returns Transformed content
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Mask phone numbers
|
||||||
|
* transformPageContent: async (content) => {
|
||||||
|
* return content.replace(/1[3-9]\d{9}/g, '***********')
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
transformPageContent?: (content: string) => Promise<string> | string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completely override the default system prompt.
|
||||||
|
* @experimental Use with caution - incorrect prompts may break agent behavior.
|
||||||
|
*/
|
||||||
|
customSystemPrompt?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delay between steps in seconds.
|
||||||
|
* @default 0.4
|
||||||
|
*/
|
||||||
|
stepDelay?: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent reflection state - the reflection-before-action model
|
* Agent reflection state - the reflection-before-action model
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
|
import type { PageAgentTool } from '../tools'
|
||||||
|
|
||||||
|
const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize LLM response and fix common format issues.
|
* Normalize LLM response and fix common format issues.
|
||||||
@@ -9,9 +15,10 @@ import chalk from 'chalk'
|
|||||||
* - Arguments wrapped as double JSON string
|
* - Arguments wrapped as double JSON string
|
||||||
* - Nested function call format
|
* - Nested function call format
|
||||||
* - Missing action field (fallback to wait)
|
* - Missing action field (fallback to wait)
|
||||||
|
* - Primitive action input for single-field tools (e.g. `{"click_element_by_index": 2}`)
|
||||||
* - etc.
|
* - etc.
|
||||||
*/
|
*/
|
||||||
export function normalizeResponse(response: any): any {
|
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
|
||||||
let resolvedArguments = null as any
|
let resolvedArguments = null as any
|
||||||
|
|
||||||
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
||||||
@@ -29,7 +36,7 @@ export function normalizeResponse(response: any): any {
|
|||||||
|
|
||||||
// case: sometimes the model only returns the action level
|
// case: sometimes the model only returns the action level
|
||||||
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
|
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #1: fixing tool_call`))
|
log(`#1: fixing tool_call`)
|
||||||
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
|
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -42,13 +49,13 @@ export function normalizeResponse(response: any): any {
|
|||||||
|
|
||||||
// case: sometimes the content json includes upper level wrapper
|
// case: sometimes the content json includes upper level wrapper
|
||||||
if (resolvedArguments?.name === 'AgentOutput') {
|
if (resolvedArguments?.name === 'AgentOutput') {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #2: fixing tool_call`))
|
log(`#2: fixing tool_call`)
|
||||||
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
|
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: sometimes even 2-levels of wrapping
|
// case: sometimes even 2-levels of wrapping
|
||||||
if (resolvedArguments?.type === 'function') {
|
if (resolvedArguments?.type === 'function') {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #3: fixing tool_call`))
|
log(`#3: fixing tool_call`)
|
||||||
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
|
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@ export function normalizeResponse(response: any): any {
|
|||||||
!resolvedArguments?.next_goal &&
|
!resolvedArguments?.next_goal &&
|
||||||
!resolvedArguments?.thinking
|
!resolvedArguments?.thinking
|
||||||
) {
|
) {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #4: fixing tool_call`))
|
log(`#4: fixing tool_call`)
|
||||||
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
|
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -78,9 +85,14 @@ export function normalizeResponse(response: any): any {
|
|||||||
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate and fix action input using tool schemas
|
||||||
|
if (resolvedArguments.action && tools) {
|
||||||
|
resolvedArguments.action = validateAction(resolvedArguments.action, tools)
|
||||||
|
}
|
||||||
|
|
||||||
// fix incomplete formats
|
// fix incomplete formats
|
||||||
if (!resolvedArguments.action) {
|
if (!resolvedArguments.action) {
|
||||||
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
|
log(`#5: fixing tool_call`)
|
||||||
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
|
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +120,53 @@ export function normalizeResponse(response: any): any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate action against tool schemas. Provides clear error messages
|
||||||
|
* instead of letting the union schema produce unreadable errors.
|
||||||
|
*
|
||||||
|
* Also coerces primitive inputs for single-field tools:
|
||||||
|
* e.g. `{"click_element_by_index": 2}` → `{"click_element_by_index": {"index": 2}}`
|
||||||
|
*/
|
||||||
|
function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
||||||
|
if (typeof action !== 'object' || action === null) return action
|
||||||
|
|
||||||
|
const toolName = Object.keys(action)[0]
|
||||||
|
if (!toolName) return action
|
||||||
|
|
||||||
|
const tool = tools.get(toolName)
|
||||||
|
if (!tool) {
|
||||||
|
const available = Array.from(tools.keys()).join(', ')
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Unknown action "${toolName}". Available: ${available}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = action[toolName]
|
||||||
|
const schema = tool.inputSchema
|
||||||
|
|
||||||
|
// coerce primitive input for single-field tools
|
||||||
|
if (schema instanceof z.ZodObject && value !== null && typeof value !== 'object') {
|
||||||
|
const requiredKey = Object.keys(schema.shape).find(
|
||||||
|
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
|
||||||
|
)
|
||||||
|
if (requiredKey) {
|
||||||
|
log(`coercing primitive action input for "${toolName}"`)
|
||||||
|
value = { [requiredKey]: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = schema.safeParse(value)
|
||||||
|
if (!result.success) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
|
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [toolName]: result.data }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely parse JSON, return original input if not json.
|
* Safely parse JSON, return original input if not json.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
|
|
||||||
export { normalizeResponse } from './autoFixer'
|
export * from './autoFixer'
|
||||||
|
|
||||||
export async function waitFor(seconds: number): Promise<void> {
|
export async function waitFor(seconds: number): Promise<void> {
|
||||||
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
||||||
@@ -31,7 +31,7 @@ export function randomID(existingIDs?: string[]): string {
|
|||||||
id = Math.random().toString(36).substring(2, 11)
|
id = Math.random().toString(36).substring(2, 11)
|
||||||
tryCount++
|
tryCount++
|
||||||
if (tryCount > MAX_TRY) {
|
if (tryCount > MAX_TRY) {
|
||||||
throw new Error('randomID: too many try')
|
throw new Error('randomID: too many tries')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,43 @@ export function uid() {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const llmsTxtCache = new Map<string, string | null>()
|
||||||
|
|
||||||
|
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
|
||||||
|
export async function fetchLlmsTxt(url: string): Promise<string | null> {
|
||||||
|
let origin: string
|
||||||
|
try {
|
||||||
|
origin = new URL(url).origin
|
||||||
|
} catch {
|
||||||
|
return null // Invalid URL
|
||||||
|
}
|
||||||
|
// about:blank, data:, file:
|
||||||
|
if (origin === 'null') return null
|
||||||
|
|
||||||
|
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!
|
||||||
|
|
||||||
|
const endpoint = `${origin}/llms.txt`
|
||||||
|
let result: string | null = null
|
||||||
|
try {
|
||||||
|
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`))
|
||||||
|
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3000) })
|
||||||
|
if (res.ok) {
|
||||||
|
result = await res.text()
|
||||||
|
console.log(chalk.green(`[llms.txt] Found (${result.length} chars)`))
|
||||||
|
if (result.length > 1000) {
|
||||||
|
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`))
|
||||||
|
result = truncate(result, 1000)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e)
|
||||||
|
}
|
||||||
|
llmsTxtCache.set(origin, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple assertion function that throws an error if the condition is falsy
|
* Simple assertion function that throws an error if the condition is falsy
|
||||||
* @param condition - The condition to assert
|
* @param condition - The condition to assert
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default defineConfig({
|
|||||||
external: [
|
external: [
|
||||||
'chalk',
|
'chalk',
|
||||||
'zod',
|
'zod',
|
||||||
|
'zod/v4',
|
||||||
// all the internal packages
|
// all the internal packages
|
||||||
/^@page-agent\//,
|
/^@page-agent\//,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,57 +1,5 @@
|
|||||||
# Privacy Policy for Page Agent Extension
|
# Privacy Policy for Page Agent Extension
|
||||||
|
|
||||||
**Last updated:** January 2026
|
This document has moved. Please see our full **[Terms of Use & Privacy](../../docs/terms-and-privacy.md)**.
|
||||||
|
|
||||||
## Overview
|
Online: https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md
|
||||||
|
|
||||||
Page Agent Extension is a browser automation tool that uses AI to help you interact with web pages. This policy explains what data is processed and under what circumstances.
|
|
||||||
|
|
||||||
## Data Processing
|
|
||||||
|
|
||||||
### Local Processing
|
|
||||||
|
|
||||||
The extension performs DOM analysis and automation actions **locally in your browser**. Your browsing history, passwords, and form data are not accessed or collected by the extension itself.
|
|
||||||
|
|
||||||
### Data Transmission
|
|
||||||
|
|
||||||
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
|
|
||||||
|
|
||||||
- Your task instructions (natural language commands)
|
|
||||||
- Sanitized page structure (simplified DOM, excluding sensitive form values)
|
|
||||||
|
|
||||||
are sent to the LLM API endpoint configured in **your settings**.
|
|
||||||
|
|
||||||
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
|
|
||||||
|
|
||||||
**If you use our testing endpoint**, your requests are proxied to [DeepSeek](https://deepseek.com) for AI processing. Regarding this test endpoint:
|
|
||||||
|
|
||||||
- This endpoint is provided for evaluation purposes only and is not recommended for production or daily use
|
|
||||||
- We do **not** store your task content, page content, or visited URLs
|
|
||||||
- Minimal logging (timestamps, request metadata, IP addresses) may occur for abuse prevention and service stability
|
|
||||||
- DeepSeek's [Privacy Policy](https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html) applies to their processing of your requests
|
|
||||||
|
|
||||||
## Data Storage
|
|
||||||
|
|
||||||
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local`
|
|
||||||
- **No cloud sync**: Configuration is not synced to any external server
|
|
||||||
- **No analytics**: The extension does not include any analytics or tracking code
|
|
||||||
|
|
||||||
## Your Control
|
|
||||||
|
|
||||||
- The extension is open source and can be audited by anyone
|
|
||||||
- You choose which LLM provider to use
|
|
||||||
- You may configure your own API endpoint at any time
|
|
||||||
- You can clear all stored data by removing the extension
|
|
||||||
|
|
||||||
## Changes to This Policy
|
|
||||||
|
|
||||||
We may update this policy as the extension evolves. Significant changes will be noted in the extension's release notes.
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
For questions about this privacy policy:
|
|
||||||
https://github.com/alibaba/page-agent/issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Source code: https://github.com/alibaba/page-agent
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/index.css",
|
"css": "src/assets/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/lib/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}.json"
|
"@magicui": "https://magicui.design/r/{name}.json"
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
# Page Agent 浏览器插件 API
|
|
||||||
|
|
||||||
在你的网页应用中接入 Page Agent 插件,并通过页面 JavaScript 发起多页面浏览器任务。
|
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
### 1. 安装浏览器插件
|
|
||||||
|
|
||||||
首选渠道:
|
|
||||||
|
|
||||||
- Chrome 应用商店:https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj
|
|
||||||
|
|
||||||
通常更快提供最新构建的渠道:
|
|
||||||
|
|
||||||
- GitHub Releases:https://github.com/alibaba/page-agent/releases
|
|
||||||
|
|
||||||
### 2. 安装类型定义(推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @page-agent/core --save-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 授权(Token)
|
|
||||||
|
|
||||||
token 用于让页面 JS 调用扩展 API(`window.PAGE_AGENT_EXT`)并执行多页面任务。
|
|
||||||
|
|
||||||
为什么必须使用 token:
|
|
||||||
|
|
||||||
- 插件具备较广的浏览器权限(页面访问、导航、多标签控制)。
|
|
||||||
- 若被滥用,可能危害用户隐私与安全。
|
|
||||||
- 用户必须主动将 token 提供给其信任的应用。
|
|
||||||
|
|
||||||
配置步骤:
|
|
||||||
|
|
||||||
1. 在扩展侧边栏中复制 auth token。
|
|
||||||
2. 在页面中设置 token:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
// 等待插件注入(最多 1 秒)
|
|
||||||
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
|
||||||
const start = Date.now()
|
|
||||||
while (Date.now() - start < timeout) {
|
|
||||||
if (window.PAGE_AGENT_EXT) return true
|
|
||||||
await new Promise((r) => setTimeout(r, 100))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用
|
|
||||||
if (await waitForExtension()) {
|
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute('点击登录按钮', {
|
|
||||||
baseURL: 'https://api.openai.com/v1',
|
|
||||||
apiKey: 'your-api-key',
|
|
||||||
model: 'gpt-5.2',
|
|
||||||
onStatusChange: (status) => console.log('状态:', status),
|
|
||||||
onActivity: (activity) => console.log('活动:', activity),
|
|
||||||
})
|
|
||||||
console.log('结果:', result)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 全局 API
|
|
||||||
|
|
||||||
token 匹配后,插件会在 `window` 上注入 API。
|
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT_VERSION`
|
|
||||||
|
|
||||||
插件版本号字符串,可用于在访问主 API 前做能力检查。
|
|
||||||
|
|
||||||
### `window.PAGE_AGENT_EXT`
|
|
||||||
|
|
||||||
主命名空间对象。
|
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.execute(task, config)`
|
|
||||||
|
|
||||||
执行 Agent 任务。
|
|
||||||
|
|
||||||
参数:
|
|
||||||
|
|
||||||
| 名称 | 类型 | 必填 | 说明 |
|
|
||||||
| ---- | ---- | ---- | ---- |
|
|
||||||
| `task` | `string` | 是 | 任务描述 |
|
|
||||||
| `config` | `ExecuteConfig` | 是 | LLM 设置、执行选项和回调 |
|
|
||||||
|
|
||||||
返回:`Promise<ExecutionResult>`
|
|
||||||
|
|
||||||
#### `PAGE_AGENT_EXT.stop()`
|
|
||||||
|
|
||||||
停止当前任务。
|
|
||||||
|
|
||||||
## 类型定义
|
|
||||||
|
|
||||||
安装 `@page-agent/core` 获取完整类型:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
export interface ExecuteConfig {
|
|
||||||
baseURL: string
|
|
||||||
apiKey: string
|
|
||||||
model: string
|
|
||||||
|
|
||||||
// 是否包含启动脚本所在标签页。默认 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[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 基础执行
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
|
||||||
'在邮箱输入框填入 test@example.com 然后点击提交',
|
|
||||||
{
|
|
||||||
baseURL: 'https://api.openai.com/v1',
|
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
|
||||||
model: 'gpt-5.2',
|
|
||||||
includeInitialTab: false, // 可选:排除当前标签页
|
|
||||||
onStatusChange: (status) => console.log(status),
|
|
||||||
onActivity: (activity) => console.log(activity),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 停止当前任务
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
window.PAGE_AGENT_EXT!.stop()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Window 类型声明
|
|
||||||
|
|
||||||
如果你不直接引入 `@page-agent/core`,可添加以下声明:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type {
|
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
interface ExecuteConfig {
|
|
||||||
baseURL: string
|
|
||||||
apiKey: string
|
|
||||||
model: string
|
|
||||||
includeInitialTab?: boolean
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
PAGE_AGENT_EXT_VERSION?: string
|
|
||||||
PAGE_AGENT_EXT?: {
|
|
||||||
version: string
|
|
||||||
execute: Execute
|
|
||||||
stop: () => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.5",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
@@ -17,32 +17,35 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/chrome": "^0.1.34",
|
"@types/chrome": "^0.1.37",
|
||||||
"@types/react": "^19.2.13",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.1",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@wxt-dev/module-react": "^1.1.5",
|
"@wxt-dev/module-react": "^1.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.577.0",
|
||||||
"motion": "^12.34.0",
|
"motion": "^12.37.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.8.0",
|
"simple-icons": "^16.12.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wxt": "^0.20.14"
|
"wxt": "^0.20.19"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.3.0",
|
"@page-agent/core": "1.5.9",
|
||||||
"@page-agent/llms": "1.3.0",
|
"@page-agent/llms": "1.5.9",
|
||||||
"@page-agent/page-controller": "1.3.0",
|
"@page-agent/page-controller": "1.5.9",
|
||||||
"@page-agent/ui": "1.3.0",
|
"@page-agent/ui": "1.5.9",
|
||||||
"chalk": "^5.6.2",
|
"ai-motion": "^0.4.8",
|
||||||
"zod": "^4.3.5"
|
"chalk": "^5.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type PageAgentConfig, PageAgentCore } from '@page-agent/core'
|
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
|
||||||
|
|
||||||
import { RemotePageController } from './RemotePageController'
|
import { RemotePageController } from './RemotePageController'
|
||||||
import { TabsController } from './TabsController'
|
import { TabsController } from './TabsController'
|
||||||
@@ -17,7 +17,7 @@ function detectLanguage(): 'en-US' | 'zh-CN' {
|
|||||||
* - can be used from a side panel or a content script
|
* - can be used from a side panel or a content script
|
||||||
*/
|
*/
|
||||||
export class MultiPageAgent extends PageAgentCore {
|
export class MultiPageAgent extends PageAgentCore {
|
||||||
constructor(config: Omit<PageAgentConfig, 'pageController'> & { includeInitialTab?: boolean }) {
|
constructor(config: AgentConfig & { includeInitialTab?: boolean }) {
|
||||||
// multi page controller
|
// multi page controller
|
||||||
const tabsController = new TabsController()
|
const tabsController = new TabsController()
|
||||||
const pageController = new RemotePageController(tabsController)
|
const pageController = new RemotePageController(tabsController)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function handleTabControlMessage(
|
|||||||
.create({ url: payload.url, active: false })
|
.create({ url: payload.url, active: false })
|
||||||
.then((newTab) => {
|
.then((newTab) => {
|
||||||
debug('open_new_tab: success', newTab)
|
debug('open_new_tab: success', newTab)
|
||||||
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId })
|
sendResponse({ success: true, tabId: newTab.id })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
sendResponse({ error: error instanceof Error ? error.message : String(error) })
|
||||||
@@ -63,7 +63,7 @@ export function handleTabControlMessage(
|
|||||||
case 'create_tab_group': {
|
case 'create_tab_group': {
|
||||||
debug('create_tab_group', payload)
|
debug('create_tab_group', payload)
|
||||||
chrome.tabs
|
chrome.tabs
|
||||||
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } })
|
.group({ tabIds: payload.tabIds })
|
||||||
.then((groupId) => {
|
.then((groupId) => {
|
||||||
debug('create_tab_group: success', groupId)
|
debug('create_tab_group: success', groupId)
|
||||||
sendResponse({ success: true, groupId })
|
sendResponse({ success: true, groupId })
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export class TabsController extends EventTarget {
|
|||||||
private initialTabId: number | null = null
|
private initialTabId: number | null = null
|
||||||
private tabGroupId: number | null = null
|
private tabGroupId: number | null = null
|
||||||
private task: string = ''
|
private task: string = ''
|
||||||
private windowId: number | null = null
|
|
||||||
|
|
||||||
async init(task: string, includeInitialTab: boolean = true) {
|
async init(task: string, includeInitialTab: boolean = true) {
|
||||||
debug('init', task, includeInitialTab)
|
debug('init', task, includeInitialTab)
|
||||||
@@ -37,7 +36,6 @@ export class TabsController extends EventTarget {
|
|||||||
this.currentTabId = null
|
this.currentTabId = null
|
||||||
this.tabGroupId = null
|
this.tabGroupId = null
|
||||||
this.initialTabId = null
|
this.initialTabId = null
|
||||||
this.windowId = null
|
|
||||||
|
|
||||||
const result = await sendMessage({
|
const result = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
@@ -53,7 +51,6 @@ export class TabsController extends EventTarget {
|
|||||||
if (includeInitialTab) {
|
if (includeInitialTab) {
|
||||||
this.currentTabId = this.initialTabId
|
this.currentTabId = this.initialTabId
|
||||||
|
|
||||||
// update tab status immediately
|
|
||||||
const info = await sendMessage({
|
const info = await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
action: 'get_tab_info',
|
action: 'get_tab_info',
|
||||||
@@ -67,6 +64,8 @@ export class TabsController extends EventTarget {
|
|||||||
title: info.title,
|
title: info.title,
|
||||||
status: info.status,
|
status: info.status,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await this.createTabGroup([this.initialTabId])
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateCurrentTabId(this.currentTabId)
|
await this.updateCurrentTabId(this.currentTabId)
|
||||||
@@ -132,9 +131,6 @@ export class TabsController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabId = result.tabId as number
|
const tabId = result.tabId as number
|
||||||
const windowId = result.windowId as number
|
|
||||||
|
|
||||||
this.windowId = windowId
|
|
||||||
|
|
||||||
this.tabs.push({
|
this.tabs.push({
|
||||||
id: tabId,
|
id: tabId,
|
||||||
@@ -144,32 +140,7 @@ export class TabsController extends EventTarget {
|
|||||||
await this.switchToTab(tabId)
|
await this.switchToTab(tabId)
|
||||||
|
|
||||||
if (!this.tabGroupId) {
|
if (!this.tabGroupId) {
|
||||||
const result = await sendMessage({
|
await this.createTabGroup([tabId])
|
||||||
type: 'TAB_CONTROL',
|
|
||||||
action: 'create_tab_group',
|
|
||||||
payload: { tabIds: [tabId], windowId: this.windowId },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(`Failed to create tab group: ${result.error}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupId = result.groupId as number
|
|
||||||
|
|
||||||
this.tabGroupId = groupId
|
|
||||||
|
|
||||||
await sendMessage({
|
|
||||||
type: 'TAB_CONTROL',
|
|
||||||
action: 'update_tab_group',
|
|
||||||
payload: {
|
|
||||||
groupId: this.tabGroupId,
|
|
||||||
properties: {
|
|
||||||
title: `PageAgent(${this.task})`,
|
|
||||||
color: randomColor(),
|
|
||||||
collapsed: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
await sendMessage({
|
await sendMessage({
|
||||||
type: 'TAB_CONTROL',
|
type: 'TAB_CONTROL',
|
||||||
@@ -230,6 +201,33 @@ export class TabsController extends EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createTabGroup(tabIds: number[]) {
|
||||||
|
const result = await sendMessage({
|
||||||
|
type: 'TAB_CONTROL',
|
||||||
|
action: 'create_tab_group',
|
||||||
|
payload: { tabIds },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
throw new Error(`Failed to create tab group: ${result?.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabGroupId = result.groupId as number
|
||||||
|
|
||||||
|
await sendMessage({
|
||||||
|
type: 'TAB_CONTROL',
|
||||||
|
action: 'update_tab_group',
|
||||||
|
payload: {
|
||||||
|
groupId: this.tabGroupId,
|
||||||
|
properties: {
|
||||||
|
title: `PageAgent(${this.task})`,
|
||||||
|
color: randomColor(),
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async updateCurrentTabId(tabId: number | null) {
|
async updateCurrentTabId(tabId: number | null) {
|
||||||
debug('updateCurrentTabId', tabId)
|
debug('updateCurrentTabId', tabId)
|
||||||
|
|
||||||
@@ -309,16 +307,7 @@ interface TabMeta {
|
|||||||
status?: 'loading' | 'unloaded' | 'complete'
|
status?: 'loading' | 'unloaded' | 'complete'
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAB_GROUP_COLORS = [
|
const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const
|
||||||
'grey',
|
|
||||||
'blue',
|
|
||||||
'red',
|
|
||||||
'yellow',
|
|
||||||
'green',
|
|
||||||
'pink',
|
|
||||||
'purple',
|
|
||||||
'cyan',
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
|
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
|
|
||||||
// Demo LLM for testing
|
// Demo LLM for testing
|
||||||
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
export const DEMO_MODEL = 'qwen3.5-plus'
|
||||||
export const DEMO_BASE_URL =
|
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
export const DEMO_API_KEY = 'NA'
|
||||||
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
|
||||||
|
|
||||||
export const DEMO_CONFIG: LLMConfig = {
|
export const DEMO_CONFIG: LLMConfig = {
|
||||||
apiKey: DEMO_API_KEY,
|
apiKey: DEMO_API_KEY,
|
||||||
baseURL: DEMO_BASE_URL,
|
baseURL: DEMO_BASE_URL,
|
||||||
model: DEMO_MODEL,
|
model: DEMO_MODEL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
|
||||||
|
export const LEGACY_TESTING_ENDPOINTS = [
|
||||||
|
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function isTestingEndpoint(url: string): boolean {
|
||||||
|
const normalized = url.replace(/\/+$/, '')
|
||||||
|
return normalized === DEMO_BASE_URL || LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateLegacyEndpoint(config: LLMConfig): LLMConfig {
|
||||||
|
const normalized = config.baseURL.replace(/\/+$/, '')
|
||||||
|
if (LEGACY_TESTING_ENDPOINTS.some((ep) => normalized === ep)) {
|
||||||
|
return { ...DEMO_CONFIG }
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ Strictly follow these rules while using the browser and navigating the web:
|
|||||||
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
|
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
|
||||||
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
|
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
|
||||||
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
|
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
|
||||||
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it.
|
- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
|
||||||
- If expected elements are missing, try scrolling, or navigating back.
|
- If expected elements are missing, try scrolling, or navigating back.
|
||||||
- If the page is not fully loaded, use the `wait` action.
|
- If the page is not fully loaded, use the `wait` action.
|
||||||
- Do not repeat one action for more than 3 times unless some conditions changed.
|
- Do not repeat one action for more than 3 times unless some conditions changed.
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
* - switch_to_tab: Switch to an existing tab
|
* - switch_to_tab: Switch to an existing tab
|
||||||
* - close_tab: Close a tab (optionally switch to another)
|
* - close_tab: Close a tab (optionally switch to another)
|
||||||
*/
|
*/
|
||||||
import * as zod from 'zod'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import type { TabsController } from './TabsController'
|
import type { TabsController } from './TabsController'
|
||||||
|
|
||||||
/** Tool definition compatible with PageAgentCore customTools */
|
/** Tool definition compatible with PageAgentCore customTools */
|
||||||
interface TabTool {
|
interface TabTool {
|
||||||
description: string
|
description: string
|
||||||
inputSchema: zod.ZodType
|
inputSchema: z.ZodType
|
||||||
execute: (input: unknown) => Promise<string>
|
execute: (input: unknown) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
|||||||
open_new_tab: {
|
open_new_tab: {
|
||||||
description:
|
description:
|
||||||
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
|
'Open a new browser tab with the specified URL. The new tab becomes the current tab for all subsequent page operations.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
url: zod.string().describe('The URL to open in the new tab'),
|
url: z.string().describe('The URL to open in the new tab'),
|
||||||
}),
|
}),
|
||||||
execute: async (input: unknown) => {
|
execute: async (input: unknown) => {
|
||||||
const { url } = input as { url: string }
|
const { url } = input as { url: string }
|
||||||
@@ -42,8 +42,8 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
|||||||
switch_to_tab: {
|
switch_to_tab: {
|
||||||
description:
|
description:
|
||||||
'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',
|
'Switch to an existing tab by its ID. After switching, all page operations will target the new current tab. You can only switch to tabs in the tab list shown in browser state.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
tab_id: zod.number().int().describe('The tab ID to switch to'),
|
tab_id: z.number().int().describe('The tab ID to switch to'),
|
||||||
}),
|
}),
|
||||||
execute: async (input: unknown) => {
|
execute: async (input: unknown) => {
|
||||||
const { tab_id } = input as { tab_id: number }
|
const { tab_id } = input as { tab_id: number }
|
||||||
@@ -58,8 +58,8 @@ export function createTabTools(tabsController: TabsController): Record<string, T
|
|||||||
close_tab: {
|
close_tab: {
|
||||||
description:
|
description:
|
||||||
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
|
'Close a tab by its ID. Cannot close the initial tab. Optionally specify which tab to switch to after closing.',
|
||||||
inputSchema: zod.object({
|
inputSchema: z.object({
|
||||||
tab_id: zod.number().int().describe('The tab ID to close'),
|
tab_id: z.number().int().describe('The tab ID to close'),
|
||||||
}),
|
}),
|
||||||
execute: async (input: unknown) => {
|
execute: async (input: unknown) => {
|
||||||
const { tab_id } = input as { tab_id: number }
|
const { tab_id } = input as { tab_id: number }
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
/**
|
/**
|
||||||
* React hook for using AgentController
|
* React hook for using AgentController
|
||||||
*/
|
*/
|
||||||
import type { AgentActivity, AgentStatus, HistoricalEvent } from '@page-agent/core'
|
import type {
|
||||||
|
AgentActivity,
|
||||||
|
AgentStatus,
|
||||||
|
ExecutionResult,
|
||||||
|
HistoricalEvent,
|
||||||
|
SupportedLanguage,
|
||||||
|
} from '@page-agent/core'
|
||||||
import type { LLMConfig } from '@page-agent/llms'
|
import type { LLMConfig } from '@page-agent/llms'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { MultiPageAgent } from './MultiPageAgent'
|
import { MultiPageAgent } from './MultiPageAgent'
|
||||||
import { DEMO_CONFIG } from './constants'
|
import { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'
|
||||||
|
|
||||||
|
/** Language preference: undefined means follow system */
|
||||||
|
export type LanguagePreference = SupportedLanguage | undefined
|
||||||
|
|
||||||
|
export interface AdvancedConfig {
|
||||||
|
maxSteps?: number
|
||||||
|
systemInstruction?: string
|
||||||
|
experimentalLlmsTxt?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtConfig extends LLMConfig, AdvancedConfig {
|
||||||
|
language?: LanguagePreference
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseAgentResult {
|
export interface UseAgentResult {
|
||||||
status: AgentStatus
|
status: AgentStatus
|
||||||
history: HistoricalEvent[]
|
history: HistoricalEvent[]
|
||||||
activity: AgentActivity | null
|
activity: AgentActivity | null
|
||||||
currentTask: string
|
currentTask: string
|
||||||
config: LLMConfig | null
|
config: ExtConfig | null
|
||||||
execute: (task: string) => Promise<void>
|
execute: (task: string) => Promise<ExecutionResult>
|
||||||
stop: () => void
|
stop: () => void
|
||||||
configure: (config: LLMConfig) => Promise<void>
|
configure: (config: ExtConfig) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAgent(): UseAgentResult {
|
export function useAgent(): UseAgentResult {
|
||||||
@@ -25,23 +44,35 @@ export function useAgent(): UseAgentResult {
|
|||||||
const [history, setHistory] = useState<HistoricalEvent[]>([])
|
const [history, setHistory] = useState<HistoricalEvent[]>([])
|
||||||
const [activity, setActivity] = useState<AgentActivity | null>(null)
|
const [activity, setActivity] = useState<AgentActivity | null>(null)
|
||||||
const [currentTask, setCurrentTask] = useState('')
|
const [currentTask, setCurrentTask] = useState('')
|
||||||
const [config, setConfig] = useState<LLMConfig | null>(null)
|
const [config, setConfig] = useState<ExtConfig | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
chrome.storage.local.get('llmConfig').then((result) => {
|
chrome.storage.local.get(['llmConfig', 'language', 'advancedConfig']).then((result) => {
|
||||||
if (result.llmConfig) {
|
let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
|
||||||
setConfig(result.llmConfig as LLMConfig)
|
const language = (result.language as SupportedLanguage) || undefined
|
||||||
} else {
|
const advancedConfig = (result.advancedConfig as AdvancedConfig) ?? {}
|
||||||
|
|
||||||
|
// Auto-migrate legacy testing endpoints
|
||||||
|
const migrated = migrateLegacyEndpoint(llmConfig)
|
||||||
|
if (migrated !== llmConfig) {
|
||||||
|
llmConfig = migrated
|
||||||
|
chrome.storage.local.set({ llmConfig: migrated })
|
||||||
|
} else if (!result.llmConfig) {
|
||||||
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
|
chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
|
||||||
setConfig(DEMO_CONFIG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setConfig({ ...llmConfig, ...advancedConfig, language })
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config) return
|
if (!config) return
|
||||||
|
|
||||||
const agent = new MultiPageAgent(config)
|
const { systemInstruction, ...agentConfig } = config
|
||||||
|
const agent = new MultiPageAgent({
|
||||||
|
...agentConfig,
|
||||||
|
instructions: systemInstruction ? { system: systemInstruction } : undefined,
|
||||||
|
})
|
||||||
agentRef.current = agent
|
agentRef.current = agent
|
||||||
|
|
||||||
const handleStatusChange = (e: Event) => {
|
const handleStatusChange = (e: Event) => {
|
||||||
@@ -80,17 +111,33 @@ export function useAgent(): UseAgentResult {
|
|||||||
|
|
||||||
setCurrentTask(task)
|
setCurrentTask(task)
|
||||||
setHistory([])
|
setHistory([])
|
||||||
await agent.execute(task)
|
return agent.execute(task)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const stop = useCallback(() => {
|
const stop = useCallback(() => {
|
||||||
agentRef.current?.stop()
|
agentRef.current?.stop()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const configure = useCallback(async (newConfig: LLMConfig) => {
|
const configure = useCallback(
|
||||||
await chrome.storage.local.set({ llmConfig: newConfig })
|
async ({
|
||||||
setConfig(newConfig)
|
language,
|
||||||
}, [])
|
maxSteps,
|
||||||
|
systemInstruction,
|
||||||
|
experimentalLlmsTxt,
|
||||||
|
...llmConfig
|
||||||
|
}: ExtConfig) => {
|
||||||
|
await chrome.storage.local.set({ llmConfig })
|
||||||
|
if (language) {
|
||||||
|
await chrome.storage.local.set({ language })
|
||||||
|
} else {
|
||||||
|
await chrome.storage.local.remove('language')
|
||||||
|
}
|
||||||
|
const advancedConfig: AdvancedConfig = { maxSteps, systemInstruction, experimentalLlmsTxt }
|
||||||
|
await chrome.storage.local.set({ advancedConfig })
|
||||||
|
setConfig({ ...llmConfig, ...advancedConfig, language })
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -111,6 +111,41 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%,
|
||||||
|
49% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-a {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-b {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|||||||
+121
-19
@@ -1,15 +1,26 @@
|
|||||||
import type { LLMConfig } from '@page-agent/llms'
|
import {
|
||||||
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react'
|
ChevronDown,
|
||||||
|
Copy,
|
||||||
|
CornerUpLeft,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
HatGlasses,
|
||||||
|
Home,
|
||||||
|
Loader2,
|
||||||
|
Scale,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { siGithub } from 'simple-icons'
|
import { siGithub } from 'simple-icons'
|
||||||
|
|
||||||
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/agent/constants'
|
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
|
||||||
|
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
|
||||||
interface ConfigPanelProps {
|
interface ConfigPanelProps {
|
||||||
config: LLMConfig | null
|
config: ExtConfig | null
|
||||||
onSave: (config: LLMConfig) => Promise<void>
|
onSave: (config: ExtConfig) => Promise<void>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,17 +28,27 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
|
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
|
||||||
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
|
||||||
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
const [model, setModel] = useState(config?.model || DEMO_MODEL)
|
||||||
|
const [language, setLanguage] = useState<LanguagePreference>(config?.language)
|
||||||
|
const [maxSteps, setMaxSteps] = useState<number | undefined>(config?.maxSteps)
|
||||||
|
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
|
||||||
|
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
|
||||||
|
config?.experimentalLlmsTxt ?? false
|
||||||
|
)
|
||||||
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [userAuthToken, setUserAuthToken] = useState<string>('')
|
const [userAuthToken, setUserAuthToken] = useState<string>('')
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [showToken, setShowToken] = useState(false)
|
const [showToken, setShowToken] = useState(false)
|
||||||
const [showApiKey, setShowApiKey] = useState(false)
|
const [showApiKey, setShowApiKey] = useState(false)
|
||||||
|
|
||||||
// Update local state when config prop changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setApiKey(config?.apiKey || DEMO_API_KEY)
|
setApiKey(config?.apiKey || DEMO_API_KEY)
|
||||||
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
||||||
setModel(config?.model || DEMO_MODEL)
|
setModel(config?.model || DEMO_MODEL)
|
||||||
|
setLanguage(config?.language)
|
||||||
|
setMaxSteps(config?.maxSteps)
|
||||||
|
setSystemInstruction(config?.systemInstruction ?? '')
|
||||||
|
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
// Poll for user auth token every second until found
|
// Poll for user auth token every second until found
|
||||||
@@ -65,7 +86,15 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await onSave({ apiKey, baseURL, model })
|
await onSave({
|
||||||
|
apiKey,
|
||||||
|
baseURL,
|
||||||
|
model,
|
||||||
|
language,
|
||||||
|
maxSteps: maxSteps || undefined,
|
||||||
|
systemInstruction: systemInstruction || undefined,
|
||||||
|
experimentalLlmsTxt,
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
@@ -89,7 +118,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
||||||
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
||||||
<p className="text-[10px] text-muted-foreground mb-1">
|
<p className="text-[10px] text-muted-foreground mb-1">
|
||||||
Add this token to a website's localStorage to give it authorization to call this extension
|
Give a website the ability to call this extension.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
@@ -134,6 +163,23 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Testing API notice */}
|
||||||
|
{isTestingEndpoint(baseURL) && (
|
||||||
|
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
|
||||||
|
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
|
||||||
|
You are using the free testing API. By using this service you agree to the{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-foreground"
|
||||||
|
>
|
||||||
|
Terms of Use & Privacy Policy
|
||||||
|
</a>
|
||||||
|
. No sensitive data. No guaranteed availability.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Model</label>
|
<label className="text-xs text-muted-foreground">Model</label>
|
||||||
<Input
|
<Input
|
||||||
@@ -165,6 +211,65 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-muted-foreground">Language</label>
|
||||||
|
<select
|
||||||
|
value={language ?? ''}
|
||||||
|
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
|
||||||
|
className="h-8 text-xs rounded-md border border-input bg-background px-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="">System</option>
|
||||||
|
<option value="en-US">English</option>
|
||||||
|
<option value="zh-CN">中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Config */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAdvancedOpen(!advancedOpen)}
|
||||||
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
|
||||||
|
>
|
||||||
|
Advanced
|
||||||
|
<ChevronDown
|
||||||
|
className="size-3 transition-transform"
|
||||||
|
style={{ transform: advancedOpen ? 'rotate(0deg)' : 'rotate(90deg)' }}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{advancedOpen && (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-muted-foreground">Max Steps</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="40"
|
||||||
|
min={1}
|
||||||
|
max={200}
|
||||||
|
value={maxSteps ?? ''}
|
||||||
|
onChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}
|
||||||
|
className="text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-xs text-muted-foreground">System Instruction</label>
|
||||||
|
<textarea
|
||||||
|
placeholder="Additional instructions for the agent..."
|
||||||
|
value={systemInstruction}
|
||||||
|
onChange={(e) => setSystemInstruction(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
|
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
|
||||||
|
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2 mt-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
|
||||||
Cancel
|
Cancel
|
||||||
@@ -181,6 +286,10 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
{/* Footer */}
|
{/* 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="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">
|
<div className="flex flex-col justify-between">
|
||||||
|
<span>
|
||||||
|
Version <span className="font-mono">v{__VERSION__}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/alibaba/page-agent"
|
href="https://github.com/alibaba/page-agent"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -192,7 +301,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Source Code</span>
|
<span>Source Code</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
<a
|
<a
|
||||||
href="https://alibaba.github.io/page-agent/"
|
href="https://alibaba.github.io/page-agent/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -204,24 +315,15 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://github.com/alibaba/page-agent/blob/main/packages/extension/PRIVACY.md"
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center gap-1 hover:text-foreground"
|
className="flex items-center gap-1 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<HatGlasses className="size-3" />
|
<HatGlasses className="size-3" />
|
||||||
<span>Privacy Policy</span>
|
<span>Privacy</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* attribute */}
|
{/* attribute */}
|
||||||
+16
-5
@@ -1,4 +1,4 @@
|
|||||||
import { AlertTriangle, RotateCcw } from 'lucide-react'
|
import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'
|
||||||
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -27,6 +27,11 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleResetConfig = async () => {
|
||||||
|
await chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.hasError) {
|
if (!this.state.hasError) {
|
||||||
return this.props.children
|
return this.props.children
|
||||||
@@ -39,10 +44,16 @@ export class ErrorBoundary extends Component<Props, State> {
|
|||||||
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
|
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
|
||||||
{this.state.error?.message || 'An unexpected error occurred'}
|
{this.state.error?.message || 'An unexpected error occurred'}
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" size="sm" onClick={this.handleReload}>
|
<div className="flex gap-2">
|
||||||
<RotateCcw className="size-3.5 mr-2" />
|
<Button variant="outline" size="sm" onClick={this.handleResetConfig}>
|
||||||
Reload Panel
|
<Eraser className="size-3.5 mr-2" />
|
||||||
</Button>
|
Reset Config
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={this.handleReload}>
|
||||||
|
<RotateCcw className="size-3.5 mr-2" />
|
||||||
|
Reload Panel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
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
|
||||||
|
export function StatusDot({ status }: { status: AgentStatus }) {
|
||||||
|
const colorClass = {
|
||||||
|
idle: 'bg-muted-foreground',
|
||||||
|
running: 'bg-blue-500',
|
||||||
|
completed: 'bg-green-500',
|
||||||
|
error: 'bg-destructive',
|
||||||
|
}[status]
|
||||||
|
|
||||||
|
const label = {
|
||||||
|
idle: 'Ready',
|
||||||
|
running: 'Running',
|
||||||
|
completed: 'Done',
|
||||||
|
error: 'Error',
|
||||||
|
}[status]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 mr-2">
|
||||||
|
<span
|
||||||
|
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">{label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Logo({ className }: { className?: string }) {
|
||||||
|
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(() => {
|
||||||
|
try {
|
||||||
|
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!)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[MotionOverlay] Motion unavailable:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
motionRef.current?.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-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-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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,8 +21,9 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|||||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||||
|
|
||||||
// Focus state.
|
// Focus state — soft multi-color glow matching ai-motion palette
|
||||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]',
|
'has-[[data-slot=input-group-control]:focus-visible]:border-blue-400/60',
|
||||||
|
'has-[[data-slot=input-group-control]:focus-visible]:shadow-[0_0_0_1px_rgba(57,182,255,0.2),0_0_8px_rgba(57,182,255,0.15),0_0_16px_rgba(189,69,251,0.1)]',
|
||||||
|
|
||||||
// Error state.
|
// Error state.
|
||||||
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
'has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { MotionProps, motion, useInView } from 'motion/react'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children?: string
|
||||||
|
words?: string[]
|
||||||
|
className?: string
|
||||||
|
duration?: number
|
||||||
|
typeSpeed?: number
|
||||||
|
deleteSpeed?: number
|
||||||
|
delay?: number
|
||||||
|
pauseDelay?: number
|
||||||
|
loop?: boolean
|
||||||
|
as?: React.ElementType
|
||||||
|
startOnView?: boolean
|
||||||
|
showCursor?: boolean
|
||||||
|
blinkCursor?: boolean
|
||||||
|
cursorStyle?: 'line' | 'block' | 'underscore'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypingAnimation({
|
||||||
|
children,
|
||||||
|
words,
|
||||||
|
className,
|
||||||
|
duration = 100,
|
||||||
|
typeSpeed,
|
||||||
|
deleteSpeed,
|
||||||
|
delay = 0,
|
||||||
|
pauseDelay = 1000,
|
||||||
|
loop = false,
|
||||||
|
as: Component = 'span',
|
||||||
|
startOnView = true,
|
||||||
|
showCursor = true,
|
||||||
|
blinkCursor = true,
|
||||||
|
cursorStyle = 'line',
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) {
|
||||||
|
const MotionComponent = motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>('')
|
||||||
|
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||||
|
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||||
|
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null)
|
||||||
|
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
|
||||||
|
const hasMultipleWords = wordsToAnimate.length > 1
|
||||||
|
|
||||||
|
const typingSpeed = typeSpeed || duration
|
||||||
|
const deletingSpeed = deleteSpeed || typingSpeed / 2
|
||||||
|
|
||||||
|
const shouldStart = startOnView ? isInView : true
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldStart || wordsToAnimate.length === 0) return
|
||||||
|
|
||||||
|
const timeoutDelay =
|
||||||
|
delay > 0 && displayedText === ''
|
||||||
|
? delay
|
||||||
|
: phase === 'typing'
|
||||||
|
? typingSpeed
|
||||||
|
: phase === 'deleting'
|
||||||
|
? deletingSpeed
|
||||||
|
: pauseDelay
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const currentWord = wordsToAnimate[currentWordIndex] || ''
|
||||||
|
const graphemes = Array.from(currentWord)
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case 'typing':
|
||||||
|
if (currentCharIndex < graphemes.length) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex + 1)
|
||||||
|
} else {
|
||||||
|
if (hasMultipleWords || loop) {
|
||||||
|
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
|
||||||
|
if (!isLastWord || loop) {
|
||||||
|
setPhase('pause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
setPhase('deleting')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deleting':
|
||||||
|
if (currentCharIndex > 0) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex - 1)
|
||||||
|
} else {
|
||||||
|
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||||
|
setCurrentWordIndex(nextIndex)
|
||||||
|
setPhase('typing')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, timeoutDelay)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [
|
||||||
|
shouldStart,
|
||||||
|
phase,
|
||||||
|
currentCharIndex,
|
||||||
|
currentWordIndex,
|
||||||
|
displayedText,
|
||||||
|
wordsToAnimate,
|
||||||
|
hasMultipleWords,
|
||||||
|
loop,
|
||||||
|
typingSpeed,
|
||||||
|
deletingSpeed,
|
||||||
|
pauseDelay,
|
||||||
|
delay,
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
|
||||||
|
const isComplete =
|
||||||
|
!loop &&
|
||||||
|
currentWordIndex === wordsToAnimate.length - 1 &&
|
||||||
|
currentCharIndex >= currentWordGraphemes.length &&
|
||||||
|
phase !== 'deleting'
|
||||||
|
|
||||||
|
const shouldShowCursor =
|
||||||
|
showCursor &&
|
||||||
|
!isComplete &&
|
||||||
|
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
|
||||||
|
|
||||||
|
const getCursorChar = () => {
|
||||||
|
switch (cursorStyle) {
|
||||||
|
case 'block':
|
||||||
|
return '▌'
|
||||||
|
case 'underscore':
|
||||||
|
return '_'
|
||||||
|
case 'line':
|
||||||
|
default:
|
||||||
|
return '|'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
{shouldShowCursor && (
|
||||||
|
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
|
||||||
|
{getCursorChar()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MotionComponent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,7 +30,34 @@ export default defineBackground(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// external messages (from localhost launcher page via externally_connectable)
|
||||||
|
|
||||||
|
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type === 'OPEN_HUB') {
|
||||||
|
openOrFocusHubTab(message.wsPort).then(() => {
|
||||||
|
if (sender.tab?.id) chrome.tabs.remove(sender.tab.id)
|
||||||
|
sendResponse({ ok: true })
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// setup
|
// setup
|
||||||
|
|
||||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function openOrFocusHubTab(wsPort: number) {
|
||||||
|
const hubUrl = chrome.runtime.getURL('hub.html')
|
||||||
|
const existing = await chrome.tabs.query({ url: `${hubUrl}*` })
|
||||||
|
|
||||||
|
if (existing.length > 0 && existing[0].id) {
|
||||||
|
await chrome.tabs.update(existing[0].id, {
|
||||||
|
active: true,
|
||||||
|
url: `${hubUrl}?ws=${wsPort}`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.tabs.create({ url: `${hubUrl}?ws=${wsPort}`, pinned: true })
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { Plug, PlugZap, Square, Unplug } from 'lucide-react'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { useAgent } from '@/agent/useAgent'
|
||||||
|
import { ActivityCard, EventCard } from '@/components/cards'
|
||||||
|
import { Logo, MotionOverlay, StatusDot } from '@/components/misc'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
import { useHubWs } from './hub-ws'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
|
||||||
|
const { wsState } = useHubWs(execute, stop, configure, config)
|
||||||
|
|
||||||
|
const historyRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (historyRef.current) {
|
||||||
|
historyRef.current.scrollTop = historyRef.current.scrollHeight
|
||||||
|
}
|
||||||
|
}, [history, activity])
|
||||||
|
|
||||||
|
const isRunning = status === 'running'
|
||||||
|
const WsIcon = wsState === 'connected' ? PlugZap : wsState === 'connecting' ? Plug : Unplug
|
||||||
|
const wsLabel = {
|
||||||
|
connected: 'Connected',
|
||||||
|
connecting: 'Connecting…',
|
||||||
|
disconnected: new URLSearchParams(location.search).get('ws') ? 'Disconnected' : 'Standalone',
|
||||||
|
}[wsState]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-background">
|
||||||
|
<MotionOverlay active={isRunning} />
|
||||||
|
|
||||||
|
{/* Left — Protocol docs */}
|
||||||
|
<aside className="w-80 shrink-0 border-r flex flex-col bg-muted/20">
|
||||||
|
<div className="flex items-center gap-2 px-5 py-4 border-b">
|
||||||
|
<Logo className="size-5" />
|
||||||
|
<span className="text-sm font-semibold tracking-tight">Page Agent Hub</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||||
|
<ProtocolDocs />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t px-5 py-3 text-[11px] text-muted-foreground/60">
|
||||||
|
Connect via <code className="text-[10px]">hub.html?ws=PORT</code>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Right — Live session */}
|
||||||
|
<main className="flex-1 flex flex-col min-w-0">
|
||||||
|
{/* Header bar */}
|
||||||
|
<header className="flex items-center justify-between border-b px-5 py-3">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<WsIcon className="size-3.5" />
|
||||||
|
<span>{wsLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<StatusDot status={status} />
|
||||||
|
{isRunning && (
|
||||||
|
<Button variant="destructive" size="sm" onClick={stop} className="h-7 text-xs">
|
||||||
|
<Square className="size-3 mr-1" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Task banner */}
|
||||||
|
{currentTask && (
|
||||||
|
<div className="border-b px-5 py-2 bg-muted/30">
|
||||||
|
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">
|
||||||
|
Current Task
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-medium truncate" title={currentTask}>
|
||||||
|
{currentTask}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Event stream */}
|
||||||
|
<div ref={historyRef} className="flex-1 overflow-y-auto p-5 space-y-2">
|
||||||
|
{!currentTask && history.length === 0 && !isRunning && (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
||||||
|
<WsIcon className="size-10 opacity-30" />
|
||||||
|
<p className="text-sm">
|
||||||
|
{wsState === 'connected'
|
||||||
|
? 'Waiting for task from external caller…'
|
||||||
|
: 'No active session'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{history.map((event, index) => (
|
||||||
|
// eslint-disable-next-line react-x/no-array-index-key
|
||||||
|
<EventCard key={index} event={event} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{activity && <ActivityCard activity={activity} />}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProtocolDocs() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-5 text-xs text-muted-foreground">
|
||||||
|
<section>
|
||||||
|
<h3 className="text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2">
|
||||||
|
Caller → Hub
|
||||||
|
</h3>
|
||||||
|
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
|
||||||
|
{`{ type: "execute", task: string, config?: object }
|
||||||
|
{ type: "stop" }`}
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2">
|
||||||
|
Hub → Caller
|
||||||
|
</h3>
|
||||||
|
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
|
||||||
|
{`{ type: "ready" }
|
||||||
|
{ type: "result", success: boolean, data: string }
|
||||||
|
{ type: "error", message: string }`}
|
||||||
|
</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2">
|
||||||
|
Flow
|
||||||
|
</h3>
|
||||||
|
<ol className="list-decimal list-inside space-y-1 text-[11px] leading-relaxed">
|
||||||
|
<li>Hub opens WS to caller's server</li>
|
||||||
|
<li>
|
||||||
|
Sends <code className="text-[10px]">ready</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Caller sends <code className="text-[10px]">execute</code> with task
|
||||||
|
</li>
|
||||||
|
<li>Hub runs agent, streams events</li>
|
||||||
|
<li>
|
||||||
|
Hub sends <code className="text-[10px]">result</code> or{' '}
|
||||||
|
<code className="text-[10px]">error</code>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* Hub WebSocket Protocol
|
||||||
|
*
|
||||||
|
* Hub connects as WS client to `ws://localhost:{port}`.
|
||||||
|
* All messages are JSON. One task at a time.
|
||||||
|
*
|
||||||
|
* Inbound (Caller → Hub):
|
||||||
|
* { type: "execute", task: string, config?: object }
|
||||||
|
* { type: "stop" }
|
||||||
|
*
|
||||||
|
* Outbound (Hub → Caller):
|
||||||
|
* { type: "ready" }
|
||||||
|
* { type: "result", success: boolean, data: string }
|
||||||
|
* { type: "error", message: string }
|
||||||
|
*/
|
||||||
|
import type { ExecutionResult } from '@page-agent/core'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import type { ExtConfig } from '@/agent/useAgent'
|
||||||
|
|
||||||
|
// --- Protocol types ---
|
||||||
|
|
||||||
|
interface ExecuteMessage {
|
||||||
|
type: 'execute'
|
||||||
|
task: string
|
||||||
|
config?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StopMessage {
|
||||||
|
type: 'stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundMessage = ExecuteMessage | StopMessage
|
||||||
|
|
||||||
|
interface ReadyMessage {
|
||||||
|
type: 'ready'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultMessage {
|
||||||
|
type: 'result'
|
||||||
|
success: boolean
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorMessage {
|
||||||
|
type: 'error'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundMessage = ReadyMessage | ResultMessage | ErrorMessage
|
||||||
|
|
||||||
|
export type HubWsState = 'connecting' | 'connected' | 'disconnected'
|
||||||
|
|
||||||
|
// --- HubWs class ---
|
||||||
|
|
||||||
|
export interface HubWsHandlers {
|
||||||
|
onExecute: (
|
||||||
|
task: string,
|
||||||
|
config?: Record<string, unknown>
|
||||||
|
) => Promise<{ success: boolean; data: string }>
|
||||||
|
onStop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Framework-agnostic WebSocket client for Hub.
|
||||||
|
* Connects to an external WS server, receives tasks, dispatches to handlers,
|
||||||
|
* and sends results back. No React, no DOM.
|
||||||
|
*/
|
||||||
|
export class HubWs {
|
||||||
|
#ws: WebSocket | null = null
|
||||||
|
#state: HubWsState = 'disconnected'
|
||||||
|
#busy = false
|
||||||
|
#handlers: HubWsHandlers
|
||||||
|
#port: number
|
||||||
|
#onStateChange: (state: HubWsState) => void
|
||||||
|
|
||||||
|
constructor(port: number, handlers: HubWsHandlers, onStateChange: (state: HubWsState) => void) {
|
||||||
|
this.#port = port
|
||||||
|
this.#handlers = handlers
|
||||||
|
this.#onStateChange = onStateChange
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
return this.#state
|
||||||
|
}
|
||||||
|
|
||||||
|
get busy() {
|
||||||
|
return this.#busy
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.#ws) return
|
||||||
|
this.#setState('connecting')
|
||||||
|
|
||||||
|
const ws = new WebSocket(`ws://localhost:${this.#port}`)
|
||||||
|
this.#ws = ws
|
||||||
|
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
this.#setState('connected')
|
||||||
|
this.#send({ type: 'ready' })
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
this.#ws = null
|
||||||
|
this.#busy = false
|
||||||
|
this.#setState('disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.addEventListener('message', (event) => {
|
||||||
|
this.#handleMessage(event.data as string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.#ws?.close()
|
||||||
|
this.#ws = null
|
||||||
|
this.#busy = false
|
||||||
|
this.#setState('disconnected')
|
||||||
|
}
|
||||||
|
|
||||||
|
#setState(state: HubWsState) {
|
||||||
|
if (this.#state === state) return
|
||||||
|
this.#state = state
|
||||||
|
this.#onStateChange(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#send(msg: OutboundMessage) {
|
||||||
|
if (this.#ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.#ws.send(JSON.stringify(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleMessage(raw: string) {
|
||||||
|
let msg: InboundMessage
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(raw)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'execute':
|
||||||
|
this.#handleExecute(msg)
|
||||||
|
break
|
||||||
|
case 'stop':
|
||||||
|
this.#handlers.onStop()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #handleExecute(msg: ExecuteMessage) {
|
||||||
|
if (this.#busy) {
|
||||||
|
this.#send({ type: 'error', message: 'Hub is busy with another task' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#busy = true
|
||||||
|
try {
|
||||||
|
const result = await this.#handlers.onExecute(msg.task, msg.config)
|
||||||
|
this.#send({ type: 'result', success: result.success, data: result.data })
|
||||||
|
} catch (err) {
|
||||||
|
this.#send({ type: 'error', message: err instanceof Error ? err.message : String(err) })
|
||||||
|
} finally {
|
||||||
|
this.#busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- React hook ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook that bridges HubWs to the agent's execute/stop/configure.
|
||||||
|
* Handles the config-before-execute dance internally.
|
||||||
|
*/
|
||||||
|
export function useHubWs(
|
||||||
|
execute: (task: string) => Promise<ExecutionResult>,
|
||||||
|
stop: () => void,
|
||||||
|
configure: (config: ExtConfig) => Promise<void>,
|
||||||
|
config: ExtConfig | null
|
||||||
|
): { wsState: HubWsState } {
|
||||||
|
const wsPort = new URLSearchParams(location.search).get('ws')
|
||||||
|
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
|
||||||
|
const hubWsRef = useRef<HubWs | null>(null)
|
||||||
|
|
||||||
|
const latest = useRef({ execute, stop, configure, config })
|
||||||
|
useEffect(() => {
|
||||||
|
latest.current = { execute, stop, configure, config }
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wsPort) return
|
||||||
|
|
||||||
|
const hubWs = new HubWs(
|
||||||
|
Number(wsPort),
|
||||||
|
{
|
||||||
|
onExecute: async (task, incomingConfig) => {
|
||||||
|
const { execute, configure, config } = latest.current
|
||||||
|
if (incomingConfig) {
|
||||||
|
await configure({ ...config, ...incomingConfig } as ExtConfig)
|
||||||
|
}
|
||||||
|
const result = await execute(task)
|
||||||
|
return { success: result.success, data: result.data }
|
||||||
|
},
|
||||||
|
onStop: () => latest.current.stop(),
|
||||||
|
},
|
||||||
|
setWsState
|
||||||
|
)
|
||||||
|
|
||||||
|
hubWs.connect()
|
||||||
|
hubWsRef.current = hubWs
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
hubWs.disconnect()
|
||||||
|
hubWsRef.current = null
|
||||||
|
}
|
||||||
|
}, [wsPort])
|
||||||
|
|
||||||
|
return { wsState }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/png" href="/assets/page-agent-64.png" />
|
||||||
|
<title>Page Agent Hub</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
|
||||||
|
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
import '@/assets/index.css'
|
||||||
|
|
||||||
|
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>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@@ -110,9 +110,9 @@ export default defineUnlistedScript(() => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
;(window as any).PAGE_AGENT_EXT_VERSION = __EXT_VERSION__
|
;(window as any).PAGE_AGENT_EXT_VERSION = __VERSION__
|
||||||
;(window as any).PAGE_AGENT_EXT = {
|
;(window as any).PAGE_AGENT_EXT = {
|
||||||
version: __EXT_VERSION__,
|
version: __VERSION__,
|
||||||
execute,
|
execute,
|
||||||
stop,
|
stop,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { History, Send, Settings, Square } from 'lucide-react'
|
import { History, Send, Settings, Square } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { ConfigPanel } from '@/components/ConfigPanel'
|
||||||
|
import { HistoryDetail } from '@/components/HistoryDetail'
|
||||||
|
import { HistoryList } from '@/components/HistoryList'
|
||||||
|
import { ActivityCard, EventCard } from '@/components/cards'
|
||||||
|
import { EmptyState, Logo, MotionOverlay, StatusDot } from '@/components/misc'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
@@ -11,11 +16,6 @@ import {
|
|||||||
import { saveSession } from '@/lib/db'
|
import { saveSession } from '@/lib/db'
|
||||||
|
|
||||||
import { useAgent } from '../../agent/useAgent'
|
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'
|
|
||||||
|
|
||||||
type View =
|
type View =
|
||||||
| { name: 'chat' }
|
| { name: 'chat' }
|
||||||
@@ -117,14 +117,15 @@ export default function App() {
|
|||||||
const showEmptyState = !currentTask && history.length === 0 && !isRunning
|
const showEmptyState = !currentTask && history.length === 0 && !isRunning
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-background">
|
<div className="relative flex flex-col h-screen bg-background">
|
||||||
|
<MotionOverlay active={isRunning} />
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center justify-between border-b px-3 py-2">
|
<header className="flex items-center justify-between border-b px-3 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Logo className="size-5" />
|
<Logo className="size-5" />
|
||||||
<span className="text-sm font-medium">Page Agent Ext</span>
|
<span className="text-sm font-medium">Page Agent Ext</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-1">
|
||||||
<StatusDot status={status} />
|
<StatusDot status={status} />
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import type { AgentStatus } from '@page-agent/core'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
// Status dot indicator
|
|
||||||
export function StatusDot({ status }: { status: AgentStatus }) {
|
|
||||||
const colorClass = {
|
|
||||||
idle: 'bg-muted-foreground',
|
|
||||||
running: 'bg-blue-500',
|
|
||||||
completed: 'bg-green-500',
|
|
||||||
error: 'bg-destructive',
|
|
||||||
}[status]
|
|
||||||
|
|
||||||
const label = {
|
|
||||||
idle: 'Ready',
|
|
||||||
running: 'Running',
|
|
||||||
completed: 'Done',
|
|
||||||
error: 'Error',
|
|
||||||
}[status]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground">{label}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Logo({ className }: { className?: string }) {
|
|
||||||
return <img src="/assets/page-agent-256.webp" alt="Page Agent" className={cn('', className)} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty state with logo
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
|
||||||
|
import { ErrorBoundary } from '@/components/ErrorBoundary'
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary'
|
|
||||||
|
|
||||||
import '@/assets/index.css'
|
import '@/assets/index.css'
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
declare const __EXT_VERSION__: string
|
declare const __VERSION__: string
|
||||||
declare const __CORE_VERSION__: string
|
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export default defineConfig({
|
|||||||
vite: () => ({
|
vite: () => ({
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
define: {
|
define: {
|
||||||
__EXT_VERSION__: JSON.stringify(pkg.version),
|
__VERSION__: JSON.stringify(pkg.version),
|
||||||
__CORE_VERSION__: JSON.stringify(pkg.dependencies['@page-agent/core']),
|
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
force: true,
|
force: true,
|
||||||
@@ -37,10 +36,14 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
zip: {
|
||||||
|
artifactTemplate: 'page-agent-ext-{{version}}-{{browser}}.zip',
|
||||||
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Page Agent Ext',
|
key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbzT0iTYeYlnCvDJIGDnGU8oarJgZILDzSfLi/ufuSxXEPDKuMyD892GhvrMCZNVHS11Sh6NYUOc/PcUOhtaR2urHtcNkrpSJNV10zUamY7fxBdVEkOucfyLu8INVy+teis62MoRWYPaUPkfZUjrLGW8MsZ9aFzARfu9GGDEp2EAYsWDN6w6vyz9LJ82pm542EWnVT4MjmDPgvYFCWGBtaU/dfHD+GAX6URJFapsCvryVURKJ+76c/GO9/I3EX1IBfbY6dec78bLCMvVxiTmiv36KyGPwX1OpakW8IiCpXWdbAxjm+plbYlp5t5zTyyoE3sOSFeXsBH0Kg27o8GcvQIDAQAB',
|
||||||
description:
|
default_locale: 'en',
|
||||||
'AI-powered browser automation assistant. Control web pages with natural language.',
|
name: '__MSG_extName__',
|
||||||
|
description: '__MSG_extDescription__',
|
||||||
homepage_url: 'https://alibaba.github.io/page-agent/',
|
homepage_url: 'https://alibaba.github.io/page-agent/',
|
||||||
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
||||||
host_permissions: ['<all_urls>'],
|
host_permissions: ['<all_urls>'],
|
||||||
@@ -48,7 +51,7 @@ export default defineConfig({
|
|||||||
64: 'assets/page-agent-64.png',
|
64: 'assets/page-agent-64.png',
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
default_title: 'Open Page Agent',
|
default_title: '__MSG_extActionTitle__',
|
||||||
},
|
},
|
||||||
web_accessible_resources: [
|
web_accessible_resources: [
|
||||||
{
|
{
|
||||||
@@ -59,5 +62,8 @@ export default defineConfig({
|
|||||||
side_panel: {
|
side_panel: {
|
||||||
default_path: 'sidepanel/index.html',
|
default_path: 'sidepanel/index.html',
|
||||||
},
|
},
|
||||||
|
externally_connectable: {
|
||||||
|
matches: ['http://localhost/*'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
@@ -37,7 +37,12 @@
|
|||||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* OpenAI Client implementation
|
* OpenAI Client implementation
|
||||||
*/
|
*/
|
||||||
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
import { modelPatch, zodToOpenAITool } from './utils'
|
import { modelPatch, zodToOpenAITool } from './utils'
|
||||||
@@ -182,7 +184,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
// Validate with schema
|
// Validate with schema
|
||||||
const validation = tool.inputSchema.safeParse(parsedArgs)
|
const validation = tool.inputSchema.safeParse(parsedArgs)
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
console.error(validation.error)
|
console.error(z.prettifyError(validation.error))
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorType.INVALID_TOOL_ARGS,
|
||||||
'Tool arguments validation failed',
|
'Tool arguments validation failed',
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
||||||
import { InvokeError } from './errors'
|
import { InvokeError, InvokeErrorType } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
|
|
||||||
export type { InvokeError, InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export { InvokeError, InvokeErrorType }
|
||||||
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||||
// Runtime validation as defensive programming (types already guarantee these)
|
// Runtime validation as defensive programming (types already guarantee these)
|
||||||
if (!config.baseURL || !config.apiKey || !config.model) {
|
if (!config.baseURL || !config.apiKey || !config.model) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
|
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
|
||||||
'See: https://alibaba.github.io/page-agent/#/docs/features/models'
|
'See: https://alibaba.github.io/page-agent/docs/features/models'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Core types for LLM integration
|
* Core types for LLM integration
|
||||||
*/
|
*/
|
||||||
import type { z } from 'zod'
|
import type * as z from 'zod/v4'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message format - OpenAI standard (industry standard)
|
* Message format - OpenAI standard (industry standard)
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
* Utility functions for LLM integration
|
* Utility functions for LLM integration
|
||||||
*/
|
*/
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import type { Tool } from './types'
|
import type { Tool } from './types'
|
||||||
|
|
||||||
function debug(message: string) {
|
const debug = console.debug.bind(console, chalk.gray('[LLM]'))
|
||||||
console.debug(chalk.gray('[LLM]'), message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert Zod schema to OpenAI tool format
|
* Convert Zod schema to OpenAI tool format
|
||||||
@@ -38,6 +36,7 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
if (modelName.startsWith('qwen')) {
|
if (modelName.startsWith('qwen')) {
|
||||||
debug('Applying Qwen patch: use higher temperature for auto fixing')
|
debug('Applying Qwen patch: use higher temperature for auto fixing')
|
||||||
body.temperature = Math.max(body.temperature || 0, 1.0)
|
body.temperature = Math.max(body.temperature || 0, 1.0)
|
||||||
|
body.enable_thinking = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('claude')) {
|
if (modelName.startsWith('claude')) {
|
||||||
@@ -74,6 +73,11 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
} else if (modelName.startsWith('gpt-51')) {
|
} else if (modelName.startsWith('gpt-51')) {
|
||||||
debug('Applying GPT-51 patch: disable reasoning')
|
debug('Applying GPT-51 patch: disable reasoning')
|
||||||
body.reasoning_effort = 'none'
|
body.reasoning_effort = 'none'
|
||||||
|
} else if (modelName.startsWith('gpt-54')) {
|
||||||
|
debug(
|
||||||
|
'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'
|
||||||
|
)
|
||||||
|
delete body.reasoning_effort
|
||||||
} else if (modelName.startsWith('gpt-5-mini')) {
|
} else if (modelName.startsWith('gpt-5-mini')) {
|
||||||
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
||||||
body.reasoning_effort = 'low'
|
body.reasoning_effort = 'low'
|
||||||
@@ -89,6 +93,15 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
body.reasoning_effort = 'minimal'
|
body.reasoning_effort = 'minimal'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('minimax')) {
|
||||||
|
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
||||||
|
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
||||||
|
body.temperature = Math.max(body.temperature || 0, 0.01)
|
||||||
|
if (body.temperature > 1) body.temperature = 1
|
||||||
|
// MiniMax does not support parallel_tool_calls
|
||||||
|
delete body.parallel_tool_calls
|
||||||
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['chalk', 'zod'],
|
external: ['chalk', 'zod', 'zod/v4'],
|
||||||
},
|
},
|
||||||
minify: false,
|
minify: false,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# @page-agent/mcp
|
||||||
|
|
||||||
|
MCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js >= 20
|
||||||
|
- [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome
|
||||||
|
- An LLM API key (OpenAI-compatible)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"page-agent": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@page-agent/mcp"],
|
||||||
|
"env": {
|
||||||
|
"LLM_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
|
"LLM_API_KEY": "sk-xxx",
|
||||||
|
"LLM_MODEL_NAME": "qwen3.5-plus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor / Copilot
|
||||||
|
|
||||||
|
Same format — add the config to the MCP settings of your client.
|
||||||
|
|
||||||
|
## MCP Tools
|
||||||
|
|
||||||
|
| Tool | Input | Description |
|
||||||
|
| -------------- | ------------------ | ---------------------------------------------------- |
|
||||||
|
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
||||||
|
| `get_status` | — | Returns `{ connected, busy }` |
|
||||||
|
| `stop_task` | — | Stop the currently running task. |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ---------------- | ------- | --------------------- |
|
||||||
|
| `LLM_BASE_URL` | — | LLM API base URL |
|
||||||
|
| `LLM_API_KEY` | — | LLM API key |
|
||||||
|
| `LLM_MODEL_NAME` | — | Model name |
|
||||||
|
| `PORT` | `38401` | HTTP + WebSocket port |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ stdio ┌──────────────────┐ WebSocket ┌──────────────┐
|
||||||
|
│ Claude / │◄────────►│ @page-agent/mcp │◄────────────►│ Hub tab │
|
||||||
|
│ Copilot │ (MCP) │ (Node.js) │ (localhost) │ (extension) │
|
||||||
|
└──────────────┘ └──────────────────┘ └──────┬───────┘
|
||||||
|
│ │
|
||||||
|
│ HTTP │ useAgent
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────┐ ┌──────────────┐
|
||||||
|
│ Launcher page │ │ MultiPage │
|
||||||
|
│ (localhost:PORT) │ │ Agent │
|
||||||
|
└──────────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Agent client starts the MCP server via stdio (`npx @page-agent/mcp`).
|
||||||
|
2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.
|
||||||
|
3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).
|
||||||
|
4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.
|
||||||
|
|
||||||
|
The hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Pure JS ESM, no build step. Source files are the published artifacts.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.js # CLI entry: MCP server (stdio) + opens launcher
|
||||||
|
├── hub-bridge.js # HTTP server + WebSocket bridge to hub tab
|
||||||
|
└── launcher.html # Bootstrap page: detects extension, triggers hub open
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:libs
|
||||||
|
npm run dev:ext
|
||||||
|
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
||||||
|
```
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@page-agent/mcp",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.5.9",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"page-agent-mcp": "src/index.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/"
|
||||||
|
],
|
||||||
|
"description": "MCP server for controlling the browser via Page Agent extension",
|
||||||
|
"keywords": [
|
||||||
|
"page-agent",
|
||||||
|
"mcp",
|
||||||
|
"browser-automation",
|
||||||
|
"chrome-extension"
|
||||||
|
],
|
||||||
|
"author": "Simon<gaomeng1900>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/alibaba/page-agent.git",
|
||||||
|
"directory": "packages/mcp"
|
||||||
|
},
|
||||||
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
|
"ws": "^8.19.0",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import http from 'node:http'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { WebSocketServer } from 'ws'
|
||||||
|
|
||||||
|
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
|
||||||
|
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
|
||||||
|
|
||||||
|
const launcherTemplate = readFileSync(
|
||||||
|
fileURLToPath(new URL('./launcher.html', import.meta.url)),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP + WebSocket bridge to the hub.html extension tab.
|
||||||
|
* - HTTP serves the launcher page (triggers extension to open hub)
|
||||||
|
* - WS carries execute/stop commands and result/error responses
|
||||||
|
*/
|
||||||
|
export class HubBridge {
|
||||||
|
/** @type {number} */
|
||||||
|
port
|
||||||
|
|
||||||
|
/** @type {http.Server} */
|
||||||
|
#httpServer
|
||||||
|
|
||||||
|
/** @type {WebSocketServer} */
|
||||||
|
#wss
|
||||||
|
|
||||||
|
/** @type {import('ws').WebSocket | null} */
|
||||||
|
#hub = null
|
||||||
|
|
||||||
|
/** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */
|
||||||
|
#pendingTask = null
|
||||||
|
|
||||||
|
/** @param {number} port */
|
||||||
|
constructor(port) {
|
||||||
|
this.port = port
|
||||||
|
this.#httpServer = http.createServer((_req, res) => {
|
||||||
|
const html = launcherTemplate
|
||||||
|
.replaceAll('__EXT_ID__', EXT_ID)
|
||||||
|
.replaceAll('__STORE_URL__', STORE_URL)
|
||||||
|
.replaceAll('__WS_PORT__', String(port))
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
||||||
|
res.end(html)
|
||||||
|
})
|
||||||
|
this.#wss = new WebSocketServer({ server: this.#httpServer })
|
||||||
|
this.#wss.on('connection', (ws) => this.#onConnection(ws))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<void>} */
|
||||||
|
async start() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
|
||||||
|
if (err.code === 'EADDRINUSE') {
|
||||||
|
reject(
|
||||||
|
new Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.#httpServer.listen(this.port, () => {
|
||||||
|
console.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get connected() {
|
||||||
|
return this.#hub?.readyState === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get busy() {
|
||||||
|
return this.#pendingTask !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} task
|
||||||
|
* @param {Record<string, unknown>} [config]
|
||||||
|
* @returns {Promise<{success: boolean, data: string}>}
|
||||||
|
*/
|
||||||
|
async executeTask(task, config) {
|
||||||
|
if (!this.connected) throw new Error('Hub is not connected. Is the extension running?')
|
||||||
|
if (this.#pendingTask) throw new Error('Agent is already running a task.')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.#pendingTask = { resolve, reject }
|
||||||
|
this.#hub.send(JSON.stringify({ type: 'execute', task, config }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTask() {
|
||||||
|
if (this.connected) {
|
||||||
|
this.#hub.send(JSON.stringify({ type: 'stop' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add version checking
|
||||||
|
|
||||||
|
/** @param {import('ws').WebSocket} ws */
|
||||||
|
#onConnection(ws) {
|
||||||
|
if (this.#hub && this.#hub.readyState === 1) {
|
||||||
|
ws.close(4000, 'Another hub is already connected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#hub = ws
|
||||||
|
console.error('[page-agent-mcp] Hub connected')
|
||||||
|
|
||||||
|
ws.on('message', (/** @type {Buffer} */ rawData) => {
|
||||||
|
/** @type {{ type: string, success?: boolean, data?: string, message?: string }} */
|
||||||
|
let msg
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(rawData.toString('utf-8'))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'result') {
|
||||||
|
this.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })
|
||||||
|
this.#pendingTask = null
|
||||||
|
} else if (msg.type === 'error') {
|
||||||
|
this.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))
|
||||||
|
this.#pendingTask = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.error('[page-agent-mcp] Hub disconnected')
|
||||||
|
if (this.#hub === ws) this.#hub = null
|
||||||
|
if (this.#pendingTask) {
|
||||||
|
this.#pendingTask.reject(new Error('Hub disconnected while task was running'))
|
||||||
|
this.#pendingTask = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
|
import { exec } from 'node:child_process'
|
||||||
|
import { platform } from 'node:os'
|
||||||
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
|
import { HubBridge } from './hub-bridge.js'
|
||||||
|
|
||||||
|
const env = process.env
|
||||||
|
const port = parseInt(env.PORT || '38401')
|
||||||
|
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const llmConfig = {}
|
||||||
|
if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
|
||||||
|
if (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL
|
||||||
|
if (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME
|
||||||
|
|
||||||
|
// --- Hub bridge (HTTP + WebSocket) ---
|
||||||
|
|
||||||
|
const hub = new HubBridge(port)
|
||||||
|
await hub.start()
|
||||||
|
|
||||||
|
// Open launcher in default browser
|
||||||
|
const url = `http://localhost:${port}`
|
||||||
|
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start ""' : 'xdg-open'
|
||||||
|
exec(`${cmd} "${url}"`, (err) => {
|
||||||
|
if (err) console.error(`[page-agent-mcp] Could not open browser: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- MCP server (stdio) ---
|
||||||
|
|
||||||
|
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
|
||||||
|
|
||||||
|
mcpServer.registerTool(
|
||||||
|
'execute_task',
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Execute a browser automation task described in natural language. ' +
|
||||||
|
'The Page Agent extension will control the browser to complete the task. ' +
|
||||||
|
'Blocks until the task is complete.',
|
||||||
|
inputSchema: { task: z.string().describe('Task description in natural language') },
|
||||||
|
},
|
||||||
|
async ({ task }) => {
|
||||||
|
try {
|
||||||
|
const config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined
|
||||||
|
const result = await hub.executeTask(task, config)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: result.success
|
||||||
|
? `Task completed successfully.\n\n${result.data}`
|
||||||
|
: `Task failed.\n\n${result.data}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
mcpServer.registerTool(
|
||||||
|
'get_status',
|
||||||
|
{
|
||||||
|
description: 'Check the current status of the Page Agent hub connection and agent.',
|
||||||
|
},
|
||||||
|
async () => ({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
mcpServer.registerTool(
|
||||||
|
'stop_task',
|
||||||
|
{
|
||||||
|
description: 'Stop the currently running browser automation task.',
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
hub.stopTask()
|
||||||
|
return { content: [{ type: 'text', text: 'Stop signal sent.' }] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await mcpServer.connect(transport)
|
||||||
|
console.error('[page-agent-mcp] MCP server ready (stdio)')
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Page Agent – Connecting</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #a3a3a3;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid #333;
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.install {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.install.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div id="connecting">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<h1>Connecting to Page Agent</h1>
|
||||||
|
<p>Opening the hub in your browser...</p>
|
||||||
|
</div>
|
||||||
|
<div id="install" class="install">
|
||||||
|
<h1>Extension Not Found</h1>
|
||||||
|
<p>The Page Agent browser extension is required.<br />Install it and try again.</p>
|
||||||
|
<a class="btn" href="__STORE_URL__" target="_blank">Install Extension</a>
|
||||||
|
<p style="margin-top: 1.5rem; font-size: 0.75rem">
|
||||||
|
After installing, restart the MCP server or refresh this page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
;(function () {
|
||||||
|
var EXT_ID = '__EXT_ID__'
|
||||||
|
var wsPort = __WS_PORT__
|
||||||
|
|
||||||
|
function showInstall() {
|
||||||
|
document.getElementById('connecting').style.display = 'none'
|
||||||
|
document.getElementById('install').classList.add('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof chrome === 'undefined' || !chrome.runtime || !chrome.runtime.sendMessage) {
|
||||||
|
showInstall()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
EXT_ID,
|
||||||
|
{ type: 'OPEN_HUB', wsPort: wsPort },
|
||||||
|
function (response) {
|
||||||
|
if (chrome.runtime.lastError || !response || !response.ok) {
|
||||||
|
showInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
showInstall()
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./dist/esm/page-agent.js",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"module": "./dist/esm/page-agent.js",
|
||||||
@@ -44,11 +44,16 @@
|
|||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.3.0",
|
"@page-agent/core": "1.5.9",
|
||||||
"@page-agent/llms": "1.3.0",
|
"@page-agent/llms": "1.5.9",
|
||||||
"@page-agent/page-controller": "1.3.0",
|
"@page-agent/page-controller": "1.5.9",
|
||||||
"@page-agent/ui": "1.3.0",
|
"@page-agent/ui": "1.5.9",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||||
* All rights reserved.
|
* All rights reserved.
|
||||||
*/
|
*/
|
||||||
import { type PageAgentConfig, PageAgentCore, type PageAgentTool, tool } from '@page-agent/core'
|
import { type AgentConfig, PageAgentCore } from '@page-agent/core'
|
||||||
import { PageController } from '@page-agent/page-controller'
|
import { PageController, type PageControllerConfig } from '@page-agent/page-controller'
|
||||||
import { Panel } from '@page-agent/ui'
|
import { Panel } from '@page-agent/ui'
|
||||||
|
|
||||||
export type { PageAgentConfig, PageAgentTool }
|
export * from '@page-agent/core'
|
||||||
export { tool }
|
|
||||||
|
export type PageAgentConfig = AgentConfig & PageControllerConfig
|
||||||
|
|
||||||
export class PageAgent extends PageAgentCore {
|
export class PageAgent extends PageAgentCore {
|
||||||
panel: Panel
|
panel: Panel
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ window.PageAgent = PageAgent
|
|||||||
|
|
||||||
console.log('🚀 page-agent.js loaded!')
|
console.log('🚀 page-agent.js loaded!')
|
||||||
|
|
||||||
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_MODEL = 'qwen3.5-plus'
|
||||||
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
|
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
|
const DEMO_API_KEY = 'NA'
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default defineConfig({
|
|||||||
external: [
|
external: [
|
||||||
'chalk',
|
'chalk',
|
||||||
'zod',
|
'zod',
|
||||||
|
'zod/v4',
|
||||||
// all the internal packages
|
// all the internal packages
|
||||||
/^@page-agent\//,
|
/^@page-agent\//,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
|||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
// Load .env from repo root
|
// Load .env from repo root
|
||||||
dotenvConfig({ path: resolve(__dirname, '../../.env') })
|
dotenvConfig({ path: resolve(__dirname, '../../.env'), quiet: true })
|
||||||
|
|
||||||
// UMD Bundle for CDN
|
// UMD Bundle for CDN
|
||||||
// - alias all local packages so that they can be build in
|
// - alias all local packages so that they can be build in
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./dist/lib/page-controller.js",
|
||||||
"module": "./dist/lib/page-controller.js",
|
"module": "./dist/lib/page-controller.js",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
scrollVertically,
|
scrollVertically,
|
||||||
selectOptionElement,
|
selectOptionElement,
|
||||||
} from './actions'
|
} from './actions'
|
||||||
import { VIEWPORT_EXPANSION } from './constants'
|
|
||||||
import * as dom from './dom'
|
import * as dom from './dom'
|
||||||
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
||||||
import { getPageInfo } from './dom/getPageInfo'
|
import { getPageInfo } from './dom/getPageInfo'
|
||||||
@@ -24,7 +23,6 @@ import { patchReact } from './patches/react'
|
|||||||
* Configuration for PageController
|
* Configuration for PageController
|
||||||
*/
|
*/
|
||||||
export interface PageControllerConfig extends dom.DomConfig {
|
export interface PageControllerConfig extends dom.DomConfig {
|
||||||
viewportExpansion?: number
|
|
||||||
/** Enable visual mask overlay during operations (default: false) */
|
/** Enable visual mask overlay during operations (default: false) */
|
||||||
enableMask?: boolean
|
enableMask?: boolean
|
||||||
}
|
}
|
||||||
@@ -131,7 +129,7 @@ export class PageController extends EventTarget {
|
|||||||
const url = window.location.href
|
const url = window.location.href
|
||||||
const title = document.title
|
const title = document.title
|
||||||
const pi = getPageInfo()
|
const pi = getPageInfo()
|
||||||
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
|
const viewportExpansion = dom.resolveViewportExpansion(this.config.viewportExpansion)
|
||||||
|
|
||||||
await this.updateTree()
|
await this.updateTree()
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export async function clickElement(element: HTMLElement) {
|
|||||||
// dispatch a click event
|
// dispatch a click event
|
||||||
// element.click()
|
// element.click()
|
||||||
|
|
||||||
await waitFor(0.1) // Wait to ensure click event processing completes
|
await waitFor(0.2) // Wait to ensure click event processing completes
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
@@ -101,41 +101,93 @@ const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
|||||||
'value'
|
'value'
|
||||||
)!.set!
|
)!.set!
|
||||||
|
|
||||||
/**
|
|
||||||
* create a synthetic keyboard event
|
|
||||||
* with key keycode code
|
|
||||||
*/
|
|
||||||
export async function createSyntheticInputEvent(elem: HTMLElement, key: string) {
|
|
||||||
elem.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key }))
|
|
||||||
await waitFor(0.01)
|
|
||||||
|
|
||||||
if (elem instanceof HTMLInputElement || elem instanceof HTMLTextAreaElement) {
|
|
||||||
elem.dispatchEvent(new Event('beforeinput', { bubbles: true }))
|
|
||||||
await waitFor(0.01)
|
|
||||||
elem.dispatchEvent(new Event('input', { bubbles: true }))
|
|
||||||
await waitFor(0.01)
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key }))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function inputTextElement(element: HTMLElement, text: string) {
|
export async function inputTextElement(element: HTMLElement, text: string) {
|
||||||
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
const isContentEditable = element.isContentEditable
|
||||||
throw new Error('Element is not an input or textarea')
|
if (
|
||||||
|
!(element instanceof HTMLInputElement) &&
|
||||||
|
!(element instanceof HTMLTextAreaElement) &&
|
||||||
|
!isContentEditable
|
||||||
|
) {
|
||||||
|
throw new Error('Element is not an input, textarea, or contenteditable')
|
||||||
}
|
}
|
||||||
|
|
||||||
await clickElement(element)
|
await clickElement(element)
|
||||||
|
|
||||||
if (element instanceof HTMLTextAreaElement) {
|
if (isContentEditable) {
|
||||||
|
// Contenteditable support (partial)
|
||||||
|
// Not supported:
|
||||||
|
// - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain.
|
||||||
|
// - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained.
|
||||||
|
//
|
||||||
|
// Plan A: Dispatch synthetic events
|
||||||
|
// Works: LinkedIn, React contenteditable, Quill.
|
||||||
|
// Fails: Slate.js
|
||||||
|
// Sequence: beforeinput -> mutation -> input -> change -> blur
|
||||||
|
|
||||||
|
// Dispatch beforeinput + mutation + input for clearing
|
||||||
|
if (
|
||||||
|
element.dispatchEvent(
|
||||||
|
new InputEvent('beforeinput', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
inputType: 'deleteContent',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
element.innerText = ''
|
||||||
|
element.dispatchEvent(
|
||||||
|
new InputEvent('input', {
|
||||||
|
bubbles: true,
|
||||||
|
inputType: 'deleteContent',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch beforeinput + mutation + input for insertion (important for React apps)
|
||||||
|
if (
|
||||||
|
element.dispatchEvent(
|
||||||
|
new InputEvent('beforeinput', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
inputType: 'insertText',
|
||||||
|
data: text,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
element.innerText = text
|
||||||
|
element.dispatchEvent(
|
||||||
|
new InputEvent('input', {
|
||||||
|
bubbles: true,
|
||||||
|
inputType: 'insertText',
|
||||||
|
data: text,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch change event (for good measure)
|
||||||
|
element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
|
|
||||||
|
// Trigger blur for validation
|
||||||
|
element.blur()
|
||||||
|
|
||||||
|
// Plan B: execCommand (deprecated but works better for some editors)
|
||||||
|
// Works: LinkedIn, Quill, Slate.js, react contenteditable components
|
||||||
|
//
|
||||||
|
// document.execCommand('selectAll')
|
||||||
|
// document.execCommand('delete')
|
||||||
|
// document.execCommand('insertText', false, text)
|
||||||
|
} else if (element instanceof HTMLTextAreaElement) {
|
||||||
nativeTextAreaValueSetter.call(element, text)
|
nativeTextAreaValueSetter.call(element, text)
|
||||||
} else {
|
} else {
|
||||||
nativeInputValueSetter.call(element, text)
|
nativeInputValueSetter.call(element, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputEvent = new Event('input', { bubbles: true })
|
// Only dispatch shared input event for non-contenteditable (contenteditable has its own)
|
||||||
element.dispatchEvent(inputEvent)
|
if (!isContentEditable) {
|
||||||
|
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
|
||||||
await waitFor(0.1) // Wait to ensure input event processing completes
|
await waitFor(0.1)
|
||||||
|
|
||||||
blurLastClickedElement()
|
blurLastClickedElement()
|
||||||
}
|
}
|
||||||
@@ -161,14 +213,18 @@ export async function selectOptionElement(selectElement: HTMLSelectElement, opti
|
|||||||
await waitFor(0.1) // Wait to ensure change event processing completes
|
await waitFor(0.1) // Wait to ensure change event processing completes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScrollableElement extends HTMLElement {
|
||||||
|
scrollIntoViewIfNeeded?: (centerIfNeeded?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
export async function scrollIntoViewIfNeeded(element: HTMLElement) {
|
export async function scrollIntoViewIfNeeded(element: HTMLElement) {
|
||||||
const el = element as any
|
const el = element as ScrollableElement
|
||||||
if (el.scrollIntoViewIfNeeded) {
|
if (typeof el.scrollIntoViewIfNeeded === 'function') {
|
||||||
el.scrollIntoViewIfNeeded()
|
el.scrollIntoViewIfNeeded()
|
||||||
// await waitFor(0.5) // Animation playback
|
// await waitFor(0.5) // Animation playback
|
||||||
} else {
|
} else {
|
||||||
// @todo visibility check
|
// @todo visibility check
|
||||||
el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
|
element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
|
||||||
// await waitFor(0.5) // Animation playback
|
// await waitFor(0.5) // Animation playback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,11 +237,6 @@ export async function scrollVertically(
|
|||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Starting direct container scroll for element:',
|
|
||||||
targetElement.tagName
|
|
||||||
)
|
|
||||||
|
|
||||||
let currentElement = targetElement as HTMLElement | null
|
let currentElement = targetElement as HTMLElement | null
|
||||||
let scrollSuccess = false
|
let scrollSuccess = false
|
||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
@@ -198,19 +249,6 @@ export async function scrollVertically(
|
|||||||
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
||||||
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Checking element:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'hasScrollableY:',
|
|
||||||
hasScrollableY,
|
|
||||||
'canScrollVertically:',
|
|
||||||
canScrollVertically,
|
|
||||||
'scrollHeight:',
|
|
||||||
currentElement.scrollHeight,
|
|
||||||
'clientHeight:',
|
|
||||||
currentElement.clientHeight
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasScrollableY && canScrollVertically) {
|
if (hasScrollableY && canScrollVertically) {
|
||||||
const beforeScroll = currentElement.scrollTop
|
const beforeScroll = currentElement.scrollTop
|
||||||
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
|
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
|
||||||
@@ -228,27 +266,10 @@ export async function scrollVertically(
|
|||||||
const afterScroll = currentElement.scrollTop
|
const afterScroll = currentElement.scrollTop
|
||||||
const actualScrollDelta = afterScroll - beforeScroll
|
const actualScrollDelta = afterScroll - beforeScroll
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Scroll attempt:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'before:',
|
|
||||||
beforeScroll,
|
|
||||||
'after:',
|
|
||||||
afterScroll,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Math.abs(actualScrollDelta) > 0.5) {
|
if (Math.abs(actualScrollDelta) > 0.5) {
|
||||||
scrollSuccess = true
|
scrollSuccess = true
|
||||||
scrolledElement = currentElement
|
scrolledElement = currentElement
|
||||||
scrollDelta = actualScrollDelta
|
scrollDelta = actualScrollDelta
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Successfully scrolled container:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,11 +365,6 @@ export async function scrollHorizontally(
|
|||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Starting direct container scroll for element:',
|
|
||||||
targetElement.tagName
|
|
||||||
)
|
|
||||||
|
|
||||||
let currentElement = targetElement as HTMLElement | null
|
let currentElement = targetElement as HTMLElement | null
|
||||||
let scrollSuccess = false
|
let scrollSuccess = false
|
||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
@@ -361,19 +377,6 @@ export async function scrollHorizontally(
|
|||||||
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
||||||
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Checking element:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'hasScrollableX:',
|
|
||||||
hasScrollableX,
|
|
||||||
'canScrollHorizontally:',
|
|
||||||
canScrollHorizontally,
|
|
||||||
'scrollWidth:',
|
|
||||||
currentElement.scrollWidth,
|
|
||||||
'clientWidth:',
|
|
||||||
currentElement.clientWidth
|
|
||||||
)
|
|
||||||
|
|
||||||
if (hasScrollableX && canScrollHorizontally) {
|
if (hasScrollableX && canScrollHorizontally) {
|
||||||
const beforeScroll = currentElement.scrollLeft
|
const beforeScroll = currentElement.scrollLeft
|
||||||
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
|
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
|
||||||
@@ -391,27 +394,10 @@ export async function scrollHorizontally(
|
|||||||
const afterScroll = currentElement.scrollLeft
|
const afterScroll = currentElement.scrollLeft
|
||||||
const actualScrollDelta = afterScroll - beforeScroll
|
const actualScrollDelta = afterScroll - beforeScroll
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Scroll attempt:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'before:',
|
|
||||||
beforeScroll,
|
|
||||||
'after:',
|
|
||||||
afterScroll,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
|
|
||||||
if (Math.abs(actualScrollDelta) > 0.5) {
|
if (Math.abs(actualScrollDelta) > 0.5) {
|
||||||
scrollSuccess = true
|
scrollSuccess = true
|
||||||
scrolledElement = currentElement
|
scrolledElement = currentElement
|
||||||
scrollDelta = actualScrollDelta
|
scrollDelta = actualScrollDelta
|
||||||
console.log(
|
|
||||||
'[SCROLL DEBUG] Successfully scrolled container:',
|
|
||||||
currentElement.tagName,
|
|
||||||
'delta:',
|
|
||||||
actualScrollDelta
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
|
||||||
* All rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Viewport expansion for DOM tree extraction.
|
|
||||||
* -1 means full page (no viewport restriction)
|
|
||||||
* 0 means viewport only
|
|
||||||
* positive values expand the viewport by that many pixels
|
|
||||||
*
|
|
||||||
* @note Since isTopElement depends on elementFromPoint,
|
|
||||||
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
|
|
||||||
*/
|
|
||||||
// export const VIEWPORT_EXPANSION = 100
|
|
||||||
export const VIEWPORT_EXPANSION = -1
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
* @edit add `data-browser-use-ignore` attribute
|
* @edit add `data-browser-use-ignore` attribute
|
||||||
* @edit improve `sampleRect`, filter out rects with 0 area
|
* @edit improve `sampleRect`, filter out rects with 0 area
|
||||||
* @edit exclude aria-hidden elements
|
* @edit exclude aria-hidden elements
|
||||||
|
* @edit make sure attributes exist for interactive candidates.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
@@ -1446,7 +1447,7 @@ export default (
|
|||||||
/**
|
/**
|
||||||
* @edit add `data-browser-use-ignore` attribute
|
* @edit add `data-browser-use-ignore` attribute
|
||||||
*/
|
*/
|
||||||
if (node.dataset?.browserUseIgnore === 'true') {
|
if (node.dataset?.browserUseIgnore === 'true' || node.dataset?.pageAgentIgnore === 'true') {
|
||||||
return null // Skip this node and its children
|
return null // Skip this node and its children
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1608,6 +1609,18 @@ export default (
|
|||||||
* @edit direct dom ref
|
* @edit direct dom ref
|
||||||
*/
|
*/
|
||||||
nodeData.ref = node
|
nodeData.ref = node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @edit make sure attributes exist for interactive candidates.
|
||||||
|
* @note if the element failed the isInteractiveCandidate, attributes would be empty.
|
||||||
|
*/
|
||||||
|
if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
|
||||||
|
const attributeNames = node.getAttributeNames?.() || []
|
||||||
|
for (const name of attributeNames) {
|
||||||
|
const value = node.getAttribute(name)
|
||||||
|
nodeData.attributes[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { VIEWPORT_EXPANSION } from '../constants'
|
|
||||||
import domTree from './dom_tree/index.js'
|
import domTree from './dom_tree/index.js'
|
||||||
import {
|
import {
|
||||||
ElementDomNode,
|
ElementDomNode,
|
||||||
@@ -7,7 +6,23 @@ import {
|
|||||||
TextDomNode,
|
TextDomNode,
|
||||||
} from './dom_tree/type'
|
} from './dom_tree/type'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Viewport expansion for DOM tree extraction.
|
||||||
|
* -1 means full page (no viewport restriction)
|
||||||
|
* 0 means viewport only
|
||||||
|
* positive values expand the viewport by that many pixels
|
||||||
|
*
|
||||||
|
* @note Since isTopElement depends on elementFromPoint,
|
||||||
|
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
|
||||||
|
*/
|
||||||
|
const DEFAULT_VIEWPORT_EXPANSION = -1
|
||||||
|
|
||||||
|
export function resolveViewportExpansion(viewportExpansion?: number): number {
|
||||||
|
return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomConfig {
|
export interface DomConfig {
|
||||||
|
viewportExpansion?: number
|
||||||
interactiveBlacklist?: (Element | (() => Element))[]
|
interactiveBlacklist?: (Element | (() => Element))[]
|
||||||
interactiveWhitelist?: (Element | (() => Element))[]
|
interactiveWhitelist?: (Element | (() => Element))[]
|
||||||
includeAttributes?: string[]
|
includeAttributes?: string[]
|
||||||
@@ -21,6 +36,8 @@ export interface DomConfig {
|
|||||||
const newElementsCache = new WeakMap<HTMLElement, string>()
|
const newElementsCache = new WeakMap<HTMLElement, string>()
|
||||||
|
|
||||||
export function getFlatTree(config: DomConfig): FlatDomTree {
|
export function getFlatTree(config: DomConfig): FlatDomTree {
|
||||||
|
const viewportExpansion = resolveViewportExpansion(config.viewportExpansion)
|
||||||
|
|
||||||
const interactiveBlacklist = [] as Element[]
|
const interactiveBlacklist = [] as Element[]
|
||||||
for (const item of config.interactiveBlacklist || []) {
|
for (const item of config.interactiveBlacklist || []) {
|
||||||
if (typeof item === 'function') {
|
if (typeof item === 'function') {
|
||||||
@@ -43,7 +60,7 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
|
|||||||
doHighlightElements: true,
|
doHighlightElements: true,
|
||||||
debugMode: true,
|
debugMode: true,
|
||||||
focusHighlightIndex: -1,
|
focusHighlightIndex: -1,
|
||||||
viewportExpansion: VIEWPORT_EXPANSION,
|
viewportExpansion,
|
||||||
interactiveBlacklist,
|
interactiveBlacklist,
|
||||||
interactiveWhitelist,
|
interactiveWhitelist,
|
||||||
highlightOpacity: config.highlightOpacity ?? 0.0,
|
highlightOpacity: config.highlightOpacity ?? 0.0,
|
||||||
@@ -74,6 +91,43 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
|
|||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globRegexCache = new Map<string, RegExp>()
|
||||||
|
|
||||||
|
function globToRegex(pattern: string): RegExp {
|
||||||
|
let regex = globRegexCache.get(pattern)
|
||||||
|
if (!regex) {
|
||||||
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||||
|
regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`)
|
||||||
|
globRegexCache.set(pattern, regex)
|
||||||
|
}
|
||||||
|
return regex
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchAttributes(
|
||||||
|
attrs: Record<string, string>,
|
||||||
|
patterns: string[]
|
||||||
|
): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.includes('*')) {
|
||||||
|
const regex = globToRegex(pattern)
|
||||||
|
for (const key of Object.keys(attrs)) {
|
||||||
|
if (regex.test(key) && attrs[key].trim()) {
|
||||||
|
result[key] = attrs[key].trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const value = attrs[pattern]
|
||||||
|
if (value && value.trim()) {
|
||||||
|
result[pattern] = value.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* elementsToString 内部使用的类型
|
* elementsToString 内部使用的类型
|
||||||
*/
|
*/
|
||||||
@@ -140,10 +194,13 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
// for jump check
|
// for jump check
|
||||||
'target',
|
'target',
|
||||||
|
|
||||||
// absolute 定位的下拉菜单
|
// absolute position dropdown menu
|
||||||
'aria-haspopup',
|
'aria-haspopup',
|
||||||
'aria-controls',
|
'aria-controls',
|
||||||
'aria-owns',
|
'aria-owns',
|
||||||
|
|
||||||
|
// content editable
|
||||||
|
'contenteditable',
|
||||||
]
|
]
|
||||||
|
|
||||||
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
|
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
|
||||||
@@ -245,23 +302,15 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
let attributesHtmlStr = ''
|
let attributesHtmlStr = ''
|
||||||
|
|
||||||
if (includeAttrs.length > 0 && node.attributes) {
|
if (includeAttrs.length > 0 && node.attributes) {
|
||||||
const attributesToInclude: Record<string, string> = {}
|
const attributesToInclude = matchAttributes(node.attributes, includeAttrs)
|
||||||
|
|
||||||
// Filter attributes
|
|
||||||
for (const key of includeAttrs) {
|
|
||||||
const value = node.attributes[key]
|
|
||||||
if (value && value.trim() !== '') {
|
|
||||||
attributesToInclude[key] = value.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicate values (for attributes longer than 5 chars)
|
// Remove duplicate values (for attributes longer than 5 chars)
|
||||||
const orderedKeys = includeAttrs.filter((key) => key in attributesToInclude)
|
const keys = Object.keys(attributesToInclude)
|
||||||
if (orderedKeys.length > 1) {
|
if (keys.length > 1) {
|
||||||
const keysToRemove = new Set<string>()
|
const keysToRemove = new Set<string>()
|
||||||
const seenValues: Record<string, string> = {}
|
const seenValues: Record<string, string> = {}
|
||||||
|
|
||||||
for (const key of orderedKeys) {
|
for (const key of keys) {
|
||||||
const value = attributesToInclude[key]
|
const value = attributesToInclude[key]
|
||||||
if (value.length > 5) {
|
if (value.length > 5) {
|
||||||
if (value in seenValues) {
|
if (value in seenValues) {
|
||||||
|
|||||||
@@ -7,3 +7,7 @@
|
|||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import cursorStyles from './cursor.module.css'
|
|||||||
export class SimulatorMask {
|
export class SimulatorMask {
|
||||||
shown: boolean = false
|
shown: boolean = false
|
||||||
wrapper = document.createElement('div')
|
wrapper = document.createElement('div')
|
||||||
motion = new Motion({
|
motion: Motion | null = null
|
||||||
mode: isPageDark() ? 'dark' : 'light',
|
|
||||||
styles: {
|
|
||||||
position: 'absolute',
|
|
||||||
inset: '0',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
#cursor = document.createElement('div')
|
#cursor = document.createElement('div')
|
||||||
|
|
||||||
@@ -28,9 +22,19 @@ export class SimulatorMask {
|
|||||||
this.wrapper.id = 'page-agent-runtime_simulator-mask'
|
this.wrapper.id = 'page-agent-runtime_simulator-mask'
|
||||||
this.wrapper.className = styles.wrapper
|
this.wrapper.className = styles.wrapper
|
||||||
this.wrapper.setAttribute('data-browser-use-ignore', 'true')
|
this.wrapper.setAttribute('data-browser-use-ignore', 'true')
|
||||||
|
this.wrapper.setAttribute('data-page-agent-ignore', 'true')
|
||||||
|
|
||||||
this.wrapper.appendChild(this.motion.element)
|
try {
|
||||||
this.motion.autoResize(this.wrapper)
|
const motion = new Motion({
|
||||||
|
mode: isPageDark() ? 'dark' : 'light',
|
||||||
|
styles: { position: 'absolute', inset: '0' },
|
||||||
|
})
|
||||||
|
this.motion = motion
|
||||||
|
this.wrapper.appendChild(motion.element)
|
||||||
|
motion.autoResize(this.wrapper)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[SimulatorMask] Motion overlay unavailable:', e)
|
||||||
|
}
|
||||||
|
|
||||||
// Capture all mouse, keyboard, and wheel events
|
// Capture all mouse, keyboard, and wheel events
|
||||||
this.wrapper.addEventListener('click', (e) => {
|
this.wrapper.addEventListener('click', (e) => {
|
||||||
@@ -144,10 +148,10 @@ export class SimulatorMask {
|
|||||||
if (this.shown) return
|
if (this.shown) return
|
||||||
|
|
||||||
this.shown = true
|
this.shown = true
|
||||||
this.motion.start()
|
this.motion?.start()
|
||||||
this.motion.fadeIn()
|
this.motion?.fadeIn()
|
||||||
|
|
||||||
this.wrapper.style.display = 'block'
|
this.wrapper.classList.add(styles.visible)
|
||||||
|
|
||||||
// Initialize cursor position
|
// Initialize cursor position
|
||||||
this.#currentCursorX = window.innerWidth / 2
|
this.#currentCursorX = window.innerWidth / 2
|
||||||
@@ -162,18 +166,18 @@ export class SimulatorMask {
|
|||||||
if (!this.shown) return
|
if (!this.shown) return
|
||||||
|
|
||||||
this.shown = false
|
this.shown = false
|
||||||
this.motion.fadeOut()
|
this.motion?.fadeOut()
|
||||||
this.motion.pause()
|
this.motion?.pause()
|
||||||
|
|
||||||
this.#cursor.classList.remove(cursorStyles.clicking)
|
this.#cursor.classList.remove(cursorStyles.clicking)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.wrapper.style.display = 'none'
|
this.wrapper.classList.remove(styles.visible)
|
||||||
}, 800) // Match the animation duration
|
}, 800) // Match the animation duration
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.motion.dispose()
|
this.motion?.dispose()
|
||||||
this.wrapper.remove()
|
this.wrapper.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const enUS = {
|
|||||||
close: 'Close',
|
close: 'Close',
|
||||||
expand: 'Expand history',
|
expand: 'Expand history',
|
||||||
collapse: 'Collapse history',
|
collapse: 'Collapse history',
|
||||||
step: 'Step {{number}} · {{time}}{{duration}}',
|
step: 'Step {{number}}',
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
clicking: 'Clicking element [{{index}}]...',
|
clicking: 'Clicking element [{{index}}]...',
|
||||||
@@ -63,7 +63,7 @@ const zhCN = {
|
|||||||
close: '关闭',
|
close: '关闭',
|
||||||
expand: '展开历史',
|
expand: '展开历史',
|
||||||
collapse: '收起历史',
|
collapse: '收起历史',
|
||||||
step: '步骤 {{number}} · {{time}}{{duration}}',
|
step: '步骤 {{number}}',
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
clicking: '正在点击元素 [{{index}}]...',
|
clicking: '正在点击元素 [{{index}}]...',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { I18n, type SupportedLanguage } from '../i18n'
|
import { I18n, type SupportedLanguage } from '../i18n'
|
||||||
import { truncate } from '../utils'
|
import { truncate } from '../utils'
|
||||||
import { createCard, createReflectionLines, formatTime } from './cards'
|
import { createCard, createReflectionLines } from './cards'
|
||||||
import type { AgentActivity, PanelAgentAdapter } from './types'
|
import type { AgentActivity, PanelAgentAdapter } from './types'
|
||||||
|
|
||||||
import styles from './Panel.module.css'
|
import styles from './Panel.module.css'
|
||||||
@@ -187,7 +187,6 @@ export class Panel {
|
|||||||
tempCard.innerHTML = createCard({
|
tempCard.innerHTML = createCard({
|
||||||
icon: '❓',
|
icon: '❓',
|
||||||
content: `Question: ${question}`,
|
content: `Question: ${question}`,
|
||||||
meta: formatTime(this.#config.language ?? 'en-US'),
|
|
||||||
type: 'question',
|
type: 'question',
|
||||||
})
|
})
|
||||||
const cardElement = tempCard.firstElementChild as HTMLElement
|
const cardElement = tempCard.firstElementChild as HTMLElement
|
||||||
@@ -374,6 +373,7 @@ export class Panel {
|
|||||||
wrapper.id = 'page-agent-runtime_agent-panel'
|
wrapper.id = 'page-agent-runtime_agent-panel'
|
||||||
wrapper.className = styles.wrapper
|
wrapper.className = styles.wrapper
|
||||||
wrapper.setAttribute('data-browser-use-ignore', 'true')
|
wrapper.setAttribute('data-browser-use-ignore', 'true')
|
||||||
|
wrapper.setAttribute('data-page-agent-ignore', 'true')
|
||||||
|
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="${styles.background}"></div>
|
<div class="${styles.background}"></div>
|
||||||
@@ -591,15 +591,12 @@ export class Panel {
|
|||||||
/** Create cards for a history event */
|
/** Create cards for a history event */
|
||||||
#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {
|
#createHistoryCards(event: PanelAgentAdapter['history'][number]): string[] {
|
||||||
const cards: string[] = []
|
const cards: string[] = []
|
||||||
const time = formatTime(this.#config.language ?? 'en-US')
|
|
||||||
const meta =
|
const meta =
|
||||||
event.type === 'step' && event.stepIndex !== undefined
|
event.type === 'step' && event.stepIndex !== undefined
|
||||||
? this.#i18n.t('ui.panel.step', {
|
? this.#i18n.t('ui.panel.step', {
|
||||||
number: (event.stepIndex + 1).toString(),
|
number: (event.stepIndex + 1).toString(),
|
||||||
time,
|
|
||||||
duration: '',
|
|
||||||
})
|
})
|
||||||
: time
|
: undefined
|
||||||
|
|
||||||
if (event.type === 'step') {
|
if (event.type === 'step') {
|
||||||
// Reflection card
|
// Reflection card
|
||||||
@@ -636,7 +633,7 @@ export class Panel {
|
|||||||
/** Create cards for an action */
|
/** Create cards for an action */
|
||||||
#createActionCards(
|
#createActionCards(
|
||||||
action: { name: string; input: unknown; output: string },
|
action: { name: string; input: unknown; output: string },
|
||||||
meta: string
|
meta?: string
|
||||||
): string[] {
|
): string[] {
|
||||||
const cards: string[] = []
|
const cards: string[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface CardOptions {
|
|||||||
export function createCard({ icon, content, meta, type }: CardOptions): string {
|
export function createCard({ icon, content, meta, type }: CardOptions): string {
|
||||||
const typeClass = type ? styles[type] : ''
|
const typeClass = type ? styles[type] : ''
|
||||||
const contentHtml = Array.isArray(content)
|
const contentHtml = Array.isArray(content)
|
||||||
? `<div class="${styles.reflectionLines}">${content.join('')}</div>`
|
? `<div class="${styles.reflectionLines}">${content.map((line) => `<span>${escapeHtml(line)}</span>`).join('')}</div>`
|
||||||
: `<span>${escapeHtml(content)}</span>`
|
: `<span>${escapeHtml(content)}</span>`
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -32,16 +32,6 @@ export function createCard({ icon, content, meta, type }: CardOptions): string {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format timestamp for cards */
|
|
||||||
export function formatTime(locale: string = 'en-US'): string {
|
|
||||||
return new Date().toLocaleTimeString(locale, {
|
|
||||||
hour12: false,
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Create reflection lines from reflection object */
|
/** Create reflection lines from reflection object */
|
||||||
export function createReflectionLines(reflection: {
|
export function createReflectionLines(reflection: {
|
||||||
evaluation_previous_goal?: string
|
evaluation_previous_goal?: string
|
||||||
@@ -50,13 +40,13 @@ export function createReflectionLines(reflection: {
|
|||||||
}): string[] {
|
}): string[] {
|
||||||
const lines: string[] = []
|
const lines: string[] = []
|
||||||
if (reflection.evaluation_previous_goal) {
|
if (reflection.evaluation_previous_goal) {
|
||||||
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`)
|
lines.push(`🔍 ${reflection.evaluation_previous_goal}`)
|
||||||
}
|
}
|
||||||
if (reflection.memory) {
|
if (reflection.memory) {
|
||||||
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`)
|
lines.push(`💾 ${reflection.memory}`)
|
||||||
}
|
}
|
||||||
if (reflection.next_goal) {
|
if (reflection.next_goal) {
|
||||||
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`)
|
lines.push(`🎯 ${reflection.next_goal}`)
|
||||||
}
|
}
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-24
@@ -5,9 +5,9 @@
|
|||||||
- **React** with TypeScript
|
- **React** with TypeScript
|
||||||
- **Vite** for dev server and build
|
- **Vite** for dev server and build
|
||||||
- **Tailwind CSS** for styling
|
- **Tailwind CSS** for styling
|
||||||
- **shadcn/ui** (new-york style) for UI components
|
- **shadcn/ui** (new-york style) for UI components — **do NOT hand-edit `src/components/ui/` files**
|
||||||
- **Magic UI** for animations and effects
|
- **Magic UI** for animations and effects
|
||||||
- **wouter** with hash routing for static hosting
|
- **wouter** with browser routing (`base: "/page-agent"`)
|
||||||
- **lucide-react** for icons
|
- **lucide-react** for icons
|
||||||
|
|
||||||
## Component Guidelines
|
## Component Guidelines
|
||||||
@@ -53,58 +53,74 @@ Located in `src/components/ui/`:
|
|||||||
### Styling Rules
|
### Styling Rules
|
||||||
|
|
||||||
1. **Prefer Tailwind classes** over custom CSS
|
1. **Prefer Tailwind classes** over custom CSS
|
||||||
2. Use CSS modules only for complex component-specific styles
|
2. Support dark mode via `dark:` classes
|
||||||
3. Support dark mode via `dark:` classes
|
3. Use CSS variables from `src/index.css` for theme colors
|
||||||
4. Use CSS variables from `src/index.css` for theme colors
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── pages/
|
├── pages/
|
||||||
│ ├── Home.tsx # Homepage
|
│ ├── home/
|
||||||
|
│ │ ├── index.tsx # Homepage
|
||||||
|
│ │ └── ...Section.tsx
|
||||||
│ └── docs/
|
│ └── docs/
|
||||||
│ ├── Layout.tsx # Documentation sidebar
|
│ ├── index.tsx # Docs route switch
|
||||||
|
│ ├── Layout.tsx # Sidebar navigation
|
||||||
│ └── [section]/[topic]/page.tsx
|
│ └── [section]/[topic]/page.tsx
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── ui/ # shadcn/ui + Magic UI components
|
│ ├── ui/ # shadcn/ui + Magic UI (DO NOT hand-edit)
|
||||||
|
│ ├── Heading.tsx # Anchor heading for doc pages
|
||||||
│ ├── Header.tsx # Site header
|
│ ├── Header.tsx # Site header
|
||||||
│ └── Footer.tsx # Site footer
|
│ └── Footer.tsx # Site footer
|
||||||
├── i18n/ # Internationalization
|
├── i18n/ # Internationalization
|
||||||
├── router.tsx # Central routing
|
├── router.tsx # Root layout + routing
|
||||||
└── main.tsx # App entry
|
└── main.tsx # App entry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
Uses wouter browser routing with base path for GitHub Pages deployment at `https://alibaba.github.io/page-agent/`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// main.tsx
|
||||||
|
<Router base="/page-agent">
|
||||||
|
<PagesRouter />
|
||||||
|
</Router>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
|
||||||
|
- Header and Footer live in `router.tsx` **outside** `<Switch>`, so they always see the root router context (`base="/page-agent"`)
|
||||||
|
- Docs pages are nested via `<Route path="/docs" nest>`, which creates a child context (`base="/page-agent/docs"`)
|
||||||
|
- Inside the docs nest, Link hrefs are relative to `/docs` (e.g. `href="/features/models"`, NOT `href="/docs/features/models"`)
|
||||||
|
- **Never use `~` prefix** in Link hrefs — it bypasses the base path entirely
|
||||||
|
- Doc page headings use `<Heading id="slug" level={2}>` for anchor links
|
||||||
|
|
||||||
|
### SPA on GitHub Pages
|
||||||
|
|
||||||
|
Instead of `404.html` redirects, the build copies `index.html` into every route directory. Add new routes to the `SPA_ROUTES` array in `vite.config.js`.
|
||||||
|
|
||||||
## Adding New Pages
|
## Adding New Pages
|
||||||
|
|
||||||
### Documentation Page
|
### Documentation Page
|
||||||
|
|
||||||
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
|
1. Create `src/pages/docs/<section>/<slug>/page.tsx`
|
||||||
2. Add route to `src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
2. Add route in `src/pages/docs/index.tsx`
|
||||||
3. Add navigation item to `pages/docs/Layout.tsx`
|
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
||||||
|
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
||||||
## Routing
|
|
||||||
|
|
||||||
Uses hash-based routing for static hosting:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { Router } from 'wouter'
|
|
||||||
import { useHashLocation } from 'wouter/use-hash-location'
|
|
||||||
|
|
||||||
;<Router hook={useHashLocation}>{/* routes */}</Router>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
| ----------------- | ----------------------- |
|
| ----------------- | ----------------------- |
|
||||||
| `components.json` | shadcn/ui configuration |
|
| `components.json` | shadcn/ui configuration |
|
||||||
| `vite.config.js` | Vite build settings |
|
| `vite.config.js` | Vite build + SPA routes |
|
||||||
| `tsconfig.json` | TypeScript config |
|
| `tsconfig.json` | TypeScript config |
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # Dev server (from root)
|
npm start # Dev server (from root)
|
||||||
npm run build:website # Build website (from root)
|
npm run build:website # Build website (from root)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -45,13 +45,24 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<div id="sk">
|
||||||
|
<p class="sk-text" id="sk-text">Loading...</p>
|
||||||
|
<style>
|
||||||
|
#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}
|
||||||
|
@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}
|
||||||
|
.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}
|
||||||
|
@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script type="module" src="./src/main.tsx"></script>
|
<script type="module" src="./src/main.tsx"></script>
|
||||||
<script>
|
<script>
|
||||||
// Dynamically update html lang attribute based on i18n detection
|
|
||||||
const updateHtmlLang = () => {
|
const updateHtmlLang = () => {
|
||||||
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
||||||
document.documentElement.lang = lang
|
document.documentElement.lang = lang
|
||||||
|
const el = document.getElementById('sk-text')
|
||||||
|
if (el) el.textContent = lang.startsWith('zh') ? '加载中...' : 'Loading...'
|
||||||
}
|
}
|
||||||
updateHtmlLang()
|
updateHtmlLang()
|
||||||
window.addEventListener('storage', updateHtmlLang)
|
window.addEventListener('storage', updateHtmlLang)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.5.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
@@ -15,19 +15,19 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/react": "^19.2.13",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.1",
|
"@types/react-dom": "^19.2.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.577.0",
|
||||||
"motion": "^12.34.0",
|
"motion": "^12.37.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.8.0",
|
"simple-icons": "^16.12.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.9.0"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://alibaba.github.io/page-agent/sitemap.xml
|
||||||
+14
-6
@@ -5,10 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { Badge } from './badge'
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -35,6 +34,8 @@ export interface APIReferenceProps {
|
|||||||
description?: React.ReactNode
|
description?: React.ReactNode
|
||||||
/** Property definitions */
|
/** Property definitions */
|
||||||
properties: PropDefinition[]
|
properties: PropDefinition[]
|
||||||
|
/** Display variant: 'properties' for fields, 'methods' for methods */
|
||||||
|
variant?: 'properties' | 'methods'
|
||||||
/** Additional CSS classes */
|
/** Additional CSS classes */
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,14 @@ export interface APIReferenceProps {
|
|||||||
// Component
|
// Component
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export function APIReference({ title, description, properties, className }: APIReferenceProps) {
|
export function APIReference({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
properties,
|
||||||
|
variant = 'properties',
|
||||||
|
className,
|
||||||
|
}: APIReferenceProps) {
|
||||||
|
const isMethodsVariant = variant === 'methods'
|
||||||
return (
|
return (
|
||||||
<div className={cn('my-6', className)}>
|
<div className={cn('my-6', className)}>
|
||||||
{title && (
|
{title && (
|
||||||
@@ -58,10 +66,10 @@ export function APIReference({ title, description, properties, className }: APIR
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-gray-50 dark:bg-gray-800/50">
|
<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">
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
Property
|
{isMethodsVariant ? 'Method' : 'Property'}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
|
||||||
Type
|
{isMethodsVariant ? 'Return Type' : 'Type'}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell">
|
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell">
|
||||||
Default
|
Default
|
||||||
@@ -120,7 +128,7 @@ function PropRow({ name, type, required, defaultValue, description, status }: Pr
|
|||||||
|
|
||||||
{/* Type */}
|
{/* Type */}
|
||||||
<td className="px-4 py-3 align-top">
|
<td className="px-4 py-3 align-top">
|
||||||
<code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded whitespace-nowrap">
|
<code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded wrap-break-word">
|
||||||
{type}
|
{type}
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</td>
|
||||||
@@ -42,7 +42,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group relative ${containerClasses} rounded-xl border shadow-2xl overflow-hidden ${className}`}
|
className={`group relative ${containerClasses} rounded-xl border shadow-2xl my-4 overflow-hidden ${className}`}
|
||||||
>
|
>
|
||||||
{/* 编辑器顶部栏 */}
|
{/* 编辑器顶部栏 */}
|
||||||
{showHeader && (
|
{showHeader && (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { siGithub } from 'simple-icons'
|
import { siGithub, siX } from 'simple-icons'
|
||||||
|
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
@@ -12,10 +12,47 @@ export default function Footer() {
|
|||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
<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">
|
<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">
|
<div className="text-gray-600 dark:text-gray-300 text-sm text-center md:text-left">
|
||||||
© 2026 page-agent. All rights reserved.
|
<p>
|
||||||
</p>
|
<a
|
||||||
<div className="flex items-center space-x-6">
|
href="https://x.com/simonluvramen"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block bg-[linear-gradient(60deg,#39b6ff_0%,#bd45fb_33%,#ff5733_66%,#ffd600_100%)] bg-clip-text text-xs leading-none text-transparent font-mono transition-opacity duration-200 hover:opacity-85"
|
||||||
|
>
|
||||||
|
Simon.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-xs mt-0.5">
|
||||||
|
© 2026 page-agent. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 text-sm mr-4"
|
||||||
|
>
|
||||||
|
{isZh ? '使用条款与隐私' : 'Terms & Privacy'}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://x.com/simonluvramen"
|
||||||
|
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 mr-4"
|
||||||
|
aria-label="X (Twitter)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="w-4 h-4 fill-current"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d={siX.path} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/alibaba/page-agent"
|
href="https://github.com/alibaba/page-agent"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useState } from 'react'
|
|||||||
import { siGithub } from 'simple-icons'
|
import { siGithub } from 'simple-icons'
|
||||||
import { Link } from 'wouter'
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
|
import { formatStars, useGitHubStars } from '@/hooks/useGitHubStars'
|
||||||
import { useLanguage } from '@/i18n/context'
|
import { useLanguage } from '@/i18n/context'
|
||||||
|
|
||||||
import LanguageSwitcher from './LanguageSwitcher'
|
import LanguageSwitcher from './LanguageSwitcher'
|
||||||
@@ -12,6 +13,7 @@ import { HyperText } from './ui/hyper-text'
|
|||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { isZh } = useLanguage()
|
const { isZh } = useLanguage()
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
const stars = useGitHubStars()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -23,7 +25,7 @@ export default function Header() {
|
|||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link
|
<Link
|
||||||
href="~/"
|
href="/"
|
||||||
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
||||||
aria-label={isZh ? 'page-agent 首页' : 'page-agent home'}
|
aria-label={isZh ? 'page-agent 首页' : 'page-agent home'}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
@@ -34,8 +36,11 @@ export default function Header() {
|
|||||||
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
|
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
|
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white leading-tight flex items-baseline gap-1.5">
|
||||||
page-agent
|
page-agent
|
||||||
|
<span className="hidden sm:inline text-[10px] font-mono font-normal text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
|
||||||
|
{import.meta.env.VERSION}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<HyperText
|
<HyperText
|
||||||
as="p"
|
as="p"
|
||||||
@@ -44,7 +49,7 @@ export default function Header() {
|
|||||||
animateOnHover={true}
|
animateOnHover={true}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
GUI Agent in your webpage
|
AI Agent In Your Webpage
|
||||||
</HyperText>
|
</HyperText>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -56,7 +61,7 @@ export default function Header() {
|
|||||||
aria-label="Mobile navigation"
|
aria-label="Mobile navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||||
aria-label={isZh ? '文档' : 'Docs'}
|
aria-label={isZh ? '文档' : 'Docs'}
|
||||||
>
|
>
|
||||||
@@ -66,7 +71,7 @@ export default function Header() {
|
|||||||
href="https://github.com/alibaba/page-agent"
|
href="https://github.com/alibaba/page-agent"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
className="flex items-center gap-1 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="GitHub"
|
aria-label="GitHub"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -77,6 +82,9 @@ export default function Header() {
|
|||||||
>
|
>
|
||||||
<path d={siGithub.path} />
|
<path d={siGithub.path} />
|
||||||
</svg>
|
</svg>
|
||||||
|
{stars !== null && (
|
||||||
|
<span className="text-sm tabular-nums">★ {formatStars(stars)}</span>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -86,11 +94,8 @@ export default function Header() {
|
|||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label={isZh ? '文档' : '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}
|
|
||||||
</span>
|
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||||
>
|
>
|
||||||
<BookOpen className="w-4 h-4" />
|
<BookOpen className="w-4 h-4" />
|
||||||
@@ -112,6 +117,9 @@ export default function Header() {
|
|||||||
<path d={siGithub.path} />
|
<path d={siGithub.path} />
|
||||||
</svg>
|
</svg>
|
||||||
GitHub
|
GitHub
|
||||||
|
{stars !== null && (
|
||||||
|
<span className="text-sm font-medium tabular-nums ">★ {formatStars(stars)}</span>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
@@ -138,7 +146,7 @@ export default function Header() {
|
|||||||
role="navigation"
|
role="navigation"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href="~/docs/introduction/overview"
|
href="/docs/introduction/overview"
|
||||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
>
|
>
|
||||||
@@ -161,6 +169,11 @@ export default function Header() {
|
|||||||
<path d={siGithub.path} />
|
<path d={siGithub.path} />
|
||||||
</svg>
|
</svg>
|
||||||
GitHub
|
GitHub
|
||||||
|
{stars !== null && (
|
||||||
|
<span className="text-xs tabular-nums text-gray-400 dark:text-gray-500">
|
||||||
|
★ {formatStars(stars)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
<div className="flex items-center gap-3 px-3 py-2">
|
<div className="flex items-center gap-3 px-3 py-2">
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
type Level = 2 | 3
|
||||||
|
|
||||||
|
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
||||||
|
id: string
|
||||||
|
level?: Level
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelStyles = {
|
||||||
|
2: { tag: 'h2', className: 'text-2xl font-semibold mb-4' },
|
||||||
|
3: { tag: 'h3', className: 'text-xl font-semibold mb-3' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
||||||
|
const ref = useRef<HTMLHeadingElement>(null)
|
||||||
|
const { tag: Tag, className: defaultClassName } = levelStyles[level]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.location.hash === `#${id}`) {
|
||||||
|
ref.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
className={cn('group relative scroll-mt-20', defaultClassName, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`#${id}`}
|
||||||
|
className="absolute -left-5 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-blue-500 transition-opacity no-underline"
|
||||||
|
aria-label={`Link to ${id}`}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</a>
|
||||||
|
{children}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ export default function LanguageSwitcher() {
|
|||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
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"
|
className="flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||||
aria-label={isZh ? '切换语言' : 'Switch language'}
|
aria-label={isZh ? '切换语言' : 'Switch language'}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
@@ -81,7 +81,7 @@ export default function LanguageSwitcher() {
|
|||||||
<button
|
<button
|
||||||
key={lang.code}
|
key={lang.code}
|
||||||
onClick={() => handleLanguageChange(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 ${
|
className={`flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
||||||
language === lang.code
|
language === lang.code
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||||
: 'text-gray-700 dark:text-gray-300'
|
: 'text-gray-700 dark:text-gray-300'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user