Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49d6163a70 | |||
| 20f33ac478 | |||
| 683602bb6b | |||
| ad19a26a57 | |||
| 51f81fc44c | |||
| 71905a6ca0 | |||
| 9ada66bb43 | |||
| 2d66915570 | |||
| 25098f8772 | |||
| 8a67b96fdb | |||
| 682a08e0dc | |||
| b53d4dd8a5 | |||
| a4e48d32a4 | |||
| cdd1602f79 | |||
| adec9d8197 | |||
| 1b9970da14 | |||
| aa8f5ae235 | |||
| 60a6b5e5b6 | |||
| 7ae8fe753a | |||
| a1dfe879c1 | |||
| 6e7619760f | |||
| efc85da91c | |||
| 8e11fcb918 | |||
| 4a9bd9aed0 | |||
| 0f697c21bd | |||
| e0d6d0d51b | |||
| d92ba7d882 | |||
| 3e8e584065 | |||
| 9bdd565feb | |||
| b0a31e777b | |||
| 78a54d0d3d | |||
| 7f04f3564a | |||
| 22a25b17f5 | |||
| 8a63c1b716 | |||
| ab710bcaf5 | |||
| df449166d5 | |||
| da822c5fe1 | |||
| 78412323a7 | |||
| ad06cd1869 | |||
| 86682c3a1e | |||
| fee3bbdf85 | |||
| 4d3c878809 | |||
| e704398c43 | |||
| 56d20904b9 | |||
| bdddcb860e | |||
| e6e8ebf4e3 | |||
| 9b1131ce13 | |||
| 462d76dbb6 | |||
| 3791d1e157 | |||
| 2e3355f52c |
@@ -6,32 +6,11 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please follow our community communication guidelines when filing issues.
|
||||
- Be polite and respectful.
|
||||
- Prefer constructive, actionable feedback over venting.
|
||||
- Include clear reproduction steps and environment details.
|
||||
- Consider contributing a fix if possible — PRs are welcome.
|
||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||
|
||||
提交 Issue 时请遵守社区沟通规范:
|
||||
- 保持礼貌与尊重。
|
||||
- 提供建设性、可行动的反馈,避免情绪化表达。
|
||||
- 请包含清晰的复现步骤与环境信息。
|
||||
- 如果可以,欢迎直接提交修复 PR。
|
||||
|
||||
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
|
||||
|
||||
- type: checkboxes
|
||||
id: community
|
||||
attributes:
|
||||
label: Community Communication / 社区沟通
|
||||
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
|
||||
options:
|
||||
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
||||
required: true
|
||||
- label: I will share constructive, actionable details. / 我会提供建设性、可行动的细节。
|
||||
required: true
|
||||
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
||||
required: true
|
||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
@@ -48,7 +27,7 @@ body:
|
||||
render: typescript
|
||||
placeholder: Minimal reproduction code
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: browser
|
||||
@@ -56,7 +35,7 @@ body:
|
||||
label: Browser
|
||||
placeholder: 'Chrome 120, Firefox 119, etc.'
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
@@ -64,4 +43,19 @@ body:
|
||||
label: version
|
||||
placeholder: '0.0.0'
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
id: community
|
||||
attributes:
|
||||
label: Community Communication / 社区沟通
|
||||
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
|
||||
options:
|
||||
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
||||
required: true
|
||||
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
|
||||
required: true
|
||||
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
@@ -12,6 +12,3 @@ contact_links:
|
||||
- name: Code of Conduct / 行为准则
|
||||
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md
|
||||
about: Community expectations and standards. 社区行为期望与标准。
|
||||
- name: 行为准则(中文)
|
||||
url: https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT_zh.md
|
||||
about: 中文版社区行为准则。
|
||||
|
||||
@@ -6,19 +6,30 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please follow our community communication guidelines when proposing features.
|
||||
- Be polite and respectful.
|
||||
- Explain the problem first, then propose solutions.
|
||||
- Prefer constructive, actionable suggestions.
|
||||
- Contributions are encouraged — consider opening a PR.
|
||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
||||
|
||||
提交功能建议时请遵守社区沟通规范:
|
||||
- 保持礼貌与尊重。
|
||||
- 先阐述问题,再提出解决方案。
|
||||
- 提供建设性、可行动的建议。
|
||||
- 鼓励代码贡献,欢迎直接提交 PR。
|
||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/CODE_OF_CONDUCT.md)
|
||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||
|
||||
See: CODE_OF_CONDUCT.md / CODE_OF_CONDUCT_zh.md, CONTRIBUTING.md
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Feature Description / 功能描述
|
||||
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
|
||||
placeholder: |
|
||||
**Problem**:
|
||||
What problem does this solve?
|
||||
|
||||
**Solution**:
|
||||
How should this work?
|
||||
|
||||
**Proposed API**:
|
||||
```typescript
|
||||
// code here
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: community
|
||||
@@ -32,27 +43,5 @@ body:
|
||||
required: true
|
||||
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
placeholder: What problem does this solve?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
placeholder: How should this work?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: api
|
||||
attributes:
|
||||
label: Proposed API
|
||||
render: typescript
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
@@ -5,9 +5,12 @@ Brief description of changes.
|
||||
## Type
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Feature / Improvement
|
||||
- [ ] Refactor
|
||||
- [ ] Documentation
|
||||
- [ ] Website
|
||||
- [ ] Demo / Testing
|
||||
- [ ] Breaking change
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -19,10 +22,4 @@ Closes #(issue)
|
||||
|
||||
## Requirements / 要求
|
||||
|
||||
- [ ] I will be polite and respectful. / 我会保持礼貌与尊重。
|
||||
- [ ] My comments and replies are constructive and actionable. / 我的评论与回复具有建设性。
|
||||
- [ ] I have read and follow the [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing Guide](CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
||||
|
||||
## Contributing / 贡献
|
||||
|
||||
Constructive suggestions and code contributions are encouraged. If this PR originated from a discussion or issue, please link it above. 欢迎建设性意见与代码贡献;如源自讨论或 Issue,请在上方关联链接。
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
node-version: [20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: './dist'
|
||||
path: './packages/website/dist'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build:lib
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish
|
||||
- name: Publish all public packages
|
||||
run: npm publish --workspaces --access public
|
||||
+6
-1
@@ -26,4 +26,9 @@ dist-ssr
|
||||
|
||||
|
||||
#
|
||||
.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
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.fontLigatures": true,
|
||||
"cSpell.words": ["HITL", "opensource", "retryable"],
|
||||
"cSpell.words": ["HITL", "innerhtml", "opensource", "retryable", "wouter"],
|
||||
"markdownlint.config": {
|
||||
// "comment": "Relaxed rules",
|
||||
"default": true,
|
||||
|
||||
@@ -2,43 +2,111 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a dual-architecture project with **two separate parts**:
|
||||
This is a **monorepo** with npm workspaces containing **two main packages**:
|
||||
|
||||
1. **Core Library** (`src/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation
|
||||
2. **Demo&docs Website** (`pages/`) - React documentation and landing page
|
||||
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
|
||||
2. **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
||||
|
||||
And other internal packages. Such as:
|
||||
|
||||
- **Page Controller** (`packages/page-controller/`) - DOM operations and element interactions module. Independent of LLM, can be tested in unit tests.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Core Commands
|
||||
|
||||
```bash
|
||||
npm start # Start React website development server
|
||||
npm run build # Build both library AND website
|
||||
npm run build:lib # Build pure JS library only (src/ → dist/lib/)
|
||||
npm run build:lib:watch # Library development with auto-rebuild
|
||||
npm start # Start website dev server
|
||||
npm run dev # Same as start
|
||||
npm run build # Build all packages
|
||||
npm run build:lib # Build page-agent library only
|
||||
npm run lint # ESLint with TypeScript strict rules
|
||||
```
|
||||
|
||||
### Package-specific Commands
|
||||
|
||||
```bash
|
||||
# Core library
|
||||
npm run build --workspace=page-agent
|
||||
npm run build:watch --workspace=page-agent
|
||||
|
||||
# Website
|
||||
npm run dev --workspace=@page-agent/website
|
||||
npm run build --workspace=@page-agent/website
|
||||
```
|
||||
|
||||
## Architecture & Critical Patterns
|
||||
|
||||
### Dual Build System
|
||||
### Monorepo Structure
|
||||
|
||||
- **Website build**: `vite.config.js` → React SPA with hash routing → `dist/`
|
||||
- **Library build**: `vite.lib.config.js` → UMD/ES modules → `dist/lib/`
|
||||
- **Entry points**: `src/entry.ts` (library), `pages/main.tsx` (website)
|
||||
We adopt a very simple monorepo solution: ts reference + vite alias.
|
||||
|
||||
You must update tsconfig and vite config if you add/remove/rename a package.
|
||||
|
||||
```bash
|
||||
packages/
|
||||
├── page-agent/ # npm: "page-agent" ⭐ MAIN
|
||||
│ ├── src/ # AI agent source
|
||||
│ │ ├── PageAgent.ts # Main AI agent class
|
||||
│ │ ├── tools/ # LLM tool definitions
|
||||
│ │ ├── llms/ # LLM integration
|
||||
│ │ └── ui/ # UI components
|
||||
│ ├── vite.config.js # Library build (ES + UMD)
|
||||
│ └── package.json
|
||||
├── website/ # npm: "@page-agent/website" (private) ⭐ MAIN
|
||||
│ ├── src/ # Website source
|
||||
│ └── index.html # Entry of vite webpage
|
||||
│
|
||||
│ # ...internal packages below...
|
||||
│
|
||||
└── page-controller/ # npm: "@page-agent/page-controller"
|
||||
└── src/ # DOM operations source
|
||||
├── PageController.ts # Main controller class
|
||||
├── actions.ts # Element interaction actions
|
||||
└── dom/ # DOM tree extraction
|
||||
```
|
||||
|
||||
`workspaces` must be written in topological order to guarantee build order.
|
||||
|
||||
```json
|
||||
"workspaces": [
|
||||
// internal deps ...
|
||||
"packages/page-controller",
|
||||
"packages/page-agent",
|
||||
"packages/website"
|
||||
],
|
||||
```
|
||||
|
||||
### Module Boundaries (Critical)
|
||||
|
||||
- **Core library** (`src/`): NEVER import from `pages/` - must remain pure JavaScript
|
||||
- **Website** (`pages/`): CAN import from `src/` via `@/` alias for demos
|
||||
- **Import aliases**: `@/` → `src/`, `@pages/` → `pages/`
|
||||
- **Website** (`packages/website/`): CAN import from `page-agent` for demos. Alias `@/` → `website/src/`
|
||||
- **Page Agent** (`packages/page-agent/`): The core lib. Imports from all internal packages. Never import from website.
|
||||
- **Page Controller** (`packages/page-controller/`): Internal lib. Pure DOM operations, NO LLM dependency. Never import from page-agent.
|
||||
|
||||
### PageController ↔ PageAgent Communication
|
||||
|
||||
All communication between PageAgent and PageController is async and isolated:
|
||||
|
||||
```typescript
|
||||
// PageAgent delegates DOM operations to PageController
|
||||
await this.pageController.updateTree() // Refresh DOM state
|
||||
await this.pageController.clickElement(index) // Click by index
|
||||
await this.pageController.inputText(index, text)
|
||||
await this.pageController.scroll({ down: true, numPages: 1 })
|
||||
|
||||
// PageController exposes state via async methods
|
||||
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
|
||||
const pageInfo = await this.pageController.getPageInfo()
|
||||
```
|
||||
|
||||
DOM element references and internal state (selectorMap, elementTextMap) are encapsulated in PageController.
|
||||
|
||||
### DOM Pipeline
|
||||
|
||||
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `src/dom/dom_tree/`
|
||||
1. **DOM Extraction**: Convert live DOM to `FlatDomTree` via `page-controller/src/dom/dom_tree/`
|
||||
2. **Dehydration**: DOM tree → simplified text for LLM processing
|
||||
3. **LLM Processing**: AI model returns action plans
|
||||
4. **Indexed Operations**: Map LLM responses back to specific DOM elements
|
||||
3. **LLM Processing**: AI model returns action plans (in page-agent)
|
||||
4. **Indexed Operations**: PageAgent calls PageController methods by element index
|
||||
|
||||
### Event Bus Communication
|
||||
|
||||
@@ -71,47 +139,66 @@ Library auto-initializes when injected via script tag:
|
||||
|
||||
Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
||||
|
||||
## File Organization
|
||||
## Key Files Reference
|
||||
|
||||
### Core Library (`src/`)
|
||||
### Page Agent (`packages/page-agent/`)
|
||||
|
||||
- `entry.ts` - CDN/UMD entry point with auto-initialization
|
||||
- `PageAgent.ts` - **Main AI agent class** orchestrating DOM operations
|
||||
- `tools/` - Agent tool implementations for web actions
|
||||
- `ui/` - UI components (Panel, SimulatorMask) with CSS modules
|
||||
- `utils/bus.ts` - **Type-safe event bus** for decoupled communication
|
||||
- `patches/` - Framework-specific optimizations (React, Antd compatibility)
|
||||
- `llms/` - LLM integration and communication layer
|
||||
- `dom/` - HTML serialization and page analysis utilities
|
||||
- `config/` - Configuration constants and settings
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
|
||||
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
|
||||
| `src/tools/` | Tool definitions that call PageController methods |
|
||||
| `src/utils/bus.ts` | Type-safe event bus for decoupled communication |
|
||||
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
|
||||
| `src/llms/` | LLM integration and communication layer |
|
||||
| `vite.config.js` | Library build configuration (ES + UMD) |
|
||||
|
||||
### Website (`pages/`)
|
||||
### Page Controller (`packages/page-controller/`)
|
||||
|
||||
- `main.tsx` - Site entry with hash routing setup
|
||||
- `router.tsx` - **Manual route definitions** (requires explicit registration)
|
||||
- `components/DocsLayout.tsx` - Navigation structure (hardcoded nav items)
|
||||
- `docs/[section]/[topic]/page.tsx` - Documentation pages
|
||||
- `test-pages/` - Library integration test pages
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/PageController.ts` | ⭐ Main controller class managing DOM state and actions |
|
||||
| `src/actions.ts` | Element interaction implementations (click, input, scroll) |
|
||||
| `src/dom/dom_tree/index.js` | Core DOM extraction engine (ported from browser-use) |
|
||||
| `src/dom/getPageInfo.ts` | Page scroll/size information |
|
||||
| `src/patches/` | Framework-specific optimizations (React, Antd) |
|
||||
| `src/types.ts` | TypeScript interfaces for controller |
|
||||
|
||||
### Website (`packages/website/`)
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `src/router.tsx` | ⭐ Central routing (manual registration required) |
|
||||
| `src/components/DocsLayout.tsx` | Navigation structure (hardcoded nav items) |
|
||||
| `src/main.tsx` | Site entry with hash routing setup |
|
||||
| `src/docs/[section]/[topic]/page.tsx` | Documentation pages |
|
||||
| `src/test-pages/` | Library integration test pages |
|
||||
| `vite.config.js` | Website build configuration |
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### New Documentation Page
|
||||
|
||||
1. Create `pages/docs/<section>/<slug>/page.tsx`
|
||||
2. Add route to `pages/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||
1. Create `packages/website/src/docs/<section>/<slug>/page.tsx`
|
||||
2. Add route to `packages/website/src/router.tsx` with `<Header /> + <DocsLayout>` wrapper
|
||||
3. Add navigation item to `DocsLayout.tsx`
|
||||
|
||||
### New Agent Tool
|
||||
|
||||
1. Implement under `src/tools/`
|
||||
2. Export via `src/tools/index.ts`
|
||||
3. Wire into `PageAgent.ts` if needed
|
||||
1. Implement tool in `packages/page-agent/src/tools/index.ts`
|
||||
2. If tool needs DOM operations, add method to PageController first
|
||||
3. Tool calls `this.pageController.methodName()` for DOM interactions
|
||||
|
||||
### New PageController Action
|
||||
|
||||
1. Add action implementation in `packages/page-controller/src/actions.ts`
|
||||
2. Expose via async method in `PageController.ts`
|
||||
3. Export from `packages/page-controller/src/index.ts`
|
||||
|
||||
### New UI Component
|
||||
|
||||
1. Create in `src/ui/` with colocated CSS modules
|
||||
1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
|
||||
2. Use event bus for PageAgent communication
|
||||
3. Test via `pages/test-pages/`
|
||||
|
||||
## Code Standards
|
||||
|
||||
@@ -134,28 +221,18 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
||||
- Relative imports last
|
||||
- Blank lines between groups
|
||||
|
||||
## Critical Files to Understand
|
||||
|
||||
- `pages/router.tsx` - Central routing definition (manual registration required)
|
||||
- `pages/components/DocsLayout.tsx` - Navigation structure
|
||||
- `src/PageAgent.ts` - Core AI agent class with DOM manipulation
|
||||
- `src/dom/dom_tree/index.js` - DOM extraction engine
|
||||
- `src/utils/bus.ts` - Type-safe event bus system
|
||||
- `src/entry.ts` - Library entry point for CDN usage
|
||||
- `vite.config.js` / `vite.lib.config.js` - Dual build configuration
|
||||
|
||||
## Debugging Common Issues
|
||||
|
||||
### Blank Documentation Pages
|
||||
|
||||
1. Verify route exists in `pages/router.tsx`
|
||||
1. Verify route exists in `packages/website/src/router.tsx`
|
||||
2. Check component import path
|
||||
3. Verify CSS isn't hiding content (check dark mode classes)
|
||||
4. Test with minimal component first
|
||||
|
||||
### Library Integration Issues
|
||||
|
||||
1. Check `dist/lib/page-agent.umd.js` builds correctly
|
||||
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
|
||||
2. Test CDN injection with query params
|
||||
3. Verify event bus communications are properly typed
|
||||
4. Use `pages/test-pages/` for isolated testing
|
||||
4. Use `packages/website/src/test-pages/` for isolated testing
|
||||
|
||||
+5
-3
@@ -22,10 +22,12 @@ Thank you for your interest in contributing to Page-Agent! We welcome contributi
|
||||
|
||||
### Project Structure
|
||||
|
||||
This project has **two separate parts**:
|
||||
This is a **monorepo** with npm workspaces containing **two main packages**:
|
||||
|
||||
- **Core Library** (`src/`) - Pure JavaScript AI agent library
|
||||
- **Documentation Website** (`pages/`) - React web app for landing page and docs
|
||||
1. **Core Library** (`packages/page-agent/`) - Pure JavaScript/TypeScript AI agent library for browser DOM automation, published as `page-agent` on npm
|
||||
2. **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. No fancy tooling. Hoisting is required.
|
||||
|
||||
## 🤝 How to Contribute
|
||||
|
||||
|
||||
+14
-13
@@ -4,13 +4,13 @@
|
||||
|
||||
[](https://badge.fury.io/js/page-agent) [](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)
|
||||
|
||||
**让你的网页支持 AI 自动化。**
|
||||
|
||||
运行在页面内的 UI agent. 使用自然语言操作 Web 应用。
|
||||
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
|
||||
|
||||
🌐 [English](./README.md) | **中文**
|
||||
|
||||
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview)
|
||||
👉 <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/141bbb01-8022-4d1f-919d-9efc9a1dc1cf" width="640" crossorigin muted autoplay loop></video>
|
||||
|
||||
---
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
```html
|
||||
<!-- 临时 CDN URL. 未来会变更 -->
|
||||
<script
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
|
||||
crossorigin="true"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
@@ -67,16 +67,17 @@ await agent.execute('点击登录按钮')
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
PageAgent 采用清晰的模块化架构:
|
||||
PageAgent adopts a simplified monorepo structure:
|
||||
|
||||
```
|
||||
src/
|
||||
├── PageAgent.ts # Agent 主流程
|
||||
├── dom/ # DOM 理解
|
||||
├── tools/ # 代理交互工具
|
||||
├── ui/ # UI 组件和面板
|
||||
├── llms/ # LLM 集成层
|
||||
└── utils/ # 事件总线和工具
|
||||
packages/
|
||||
├── page-agent/ # AI agent (npm: page-agent)
|
||||
│ ├── PageAgent # Agent main loop
|
||||
│ ├── tools/ # LLM tool definitions
|
||||
│ ├── ui/ # UI components & panels
|
||||
│ └── llms/ # LLM integration layer
|
||||
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||
└── website/ # Documentation site
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
|
||||
[](https://badge.fury.io/js/page-agent) [](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)
|
||||
|
||||
**Transform your webpage into an AI-powered application with a single script tag.**
|
||||
|
||||
An in-page UI agent in javascript. Control web interfaces with natural language.
|
||||
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
|
||||
|
||||
🌐 **English** | [中文](./README-zh.md)
|
||||
|
||||
👉 [🚀 **Demo**](https://alibaba.github.io/page-agent/) | [📖 **Documentation**](https://alibaba.github.io/page-agent/#/docs/introduction/overview)
|
||||
👉 <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/de8d1964-8bde-494f-a52f-2975469557a5" width="640" crossorigin muted autoplay loop></video>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🎯 Easy Integration**
|
||||
- **🎯 Easy Integration** - Transform your webpage into an agent with a single script tag.
|
||||
- **🔐 Client-Side Processing**
|
||||
- **🧠 DOM Extraction**
|
||||
- **💬 Natural Language Interface**
|
||||
@@ -33,7 +33,7 @@ An in-page UI agent in javascript. Control web interfaces with natural language.
|
||||
```html
|
||||
<!-- temporary CDN URL. May change in the future -->
|
||||
<script
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js"
|
||||
src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js"
|
||||
crossorigin="true"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
@@ -67,16 +67,17 @@ await agent.execute('Click the login button')
|
||||
|
||||
## 🏗️ Structure
|
||||
|
||||
PageAgent follows a clean, modular architecture:
|
||||
PageAgent adopts a simplified monorepo structure:
|
||||
|
||||
```
|
||||
src/
|
||||
├── PageAgent.ts # Agent main loop
|
||||
├── dom/ # DOM processing
|
||||
├── tools/ # Agent tools
|
||||
├── ui/ # UI components & panels
|
||||
├── llms/ # LLM integration layer
|
||||
└── utils/ # Event bus & utilities
|
||||
packages/
|
||||
├── page-agent/ # AI agent (npm: page-agent)
|
||||
│ ├── PageAgent # Agent main loop
|
||||
│ ├── tools/ # LLM tool definitions
|
||||
│ ├── ui/ # UI components & panels
|
||||
│ └── llms/ # LLM integration layer
|
||||
├── page-controller/ # DOM operations (npm: @page-agent/page-controller)
|
||||
└── website/ # Demo & Documentation site
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -30,6 +30,12 @@ The development progress and future plans for PageAgent.
|
||||
- [ ] **Custom knowledge base and instructions**
|
||||
- [ ] **Black/white-list safeguard**
|
||||
- [ ] **Data-masking**
|
||||
- [ ] **Improve Memory**
|
||||
- Current phrasing can cause logic-loop for some models.
|
||||
- Test adding `Action` to memory.
|
||||
- [ ] **Tools for more complex tasks**
|
||||
- todo list
|
||||
- file sys
|
||||
- [ ] **Optimize for popular UI frameworks**
|
||||
- [x] **i18n of the website**
|
||||
- [x] Chinese version
|
||||
|
||||
+8
-3
@@ -8,7 +8,7 @@ import globals from 'globals'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist', 'test-pages']),
|
||||
globalIgnores(['**/dist', '**/test-pages', '**/node_modules']),
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
@@ -37,8 +37,10 @@ export default defineConfig([
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
// project: ['./tsconfig.json'],
|
||||
// project: ['./packages/*/tsconfig.json'],
|
||||
// tsconfigRootDir: import.meta.dirname,
|
||||
projectService: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
@@ -67,6 +69,9 @@ export default defineConfig([
|
||||
'react-dom/no-missing-button-type': 'off',
|
||||
'react-x/no-nested-component-definitions': 'off',
|
||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||
|
||||
// 'require-await': 'off',
|
||||
'@typescript-eslint/require-await': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="https://img.alicdn.com/imgextra/i3/O1CN01uo9VaC20mamWNM2Zn_!!6000000006892-55-tps-64-64.svg"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PageAgent - AI-powered UI Agent in Your Webpage</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="PageAgent.js: One-line CDN import, add intelligent UI Agent to any website. Modern web AI automation with minimal integration."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="PageAgent, AI Agent, Web Automation, UI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
|
||||
/>
|
||||
<meta name="theme-color" content="#58c0fc" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="author" content="PageAgent.js Team" />
|
||||
<meta property="og:title" content="PageAgent.js - AI-powered UI Agent" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="One-line CDN import, add intelligent UI Agent to any website."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:locale:alternate" content="zh_CN" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./pages/main.tsx"></script>
|
||||
<script>
|
||||
// Dynamically update html lang attribute based on i18n detection
|
||||
const updateHtmlLang = () => {
|
||||
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
||||
document.documentElement.lang = lang
|
||||
}
|
||||
updateHtmlLang()
|
||||
window.addEventListener('storage', updateHtmlLang)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+921
-963
File diff suppressed because it is too large
Load Diff
+26
-63
@@ -1,35 +1,14 @@
|
||||
{
|
||||
"name": "page-agent",
|
||||
"private": false,
|
||||
"version": "0.0.4",
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent.js",
|
||||
"module": "./dist/lib/page-agent.js",
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"import": "./dist/lib/page-agent.js",
|
||||
"default": "./dist/lib/page-agent.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/lib/",
|
||||
"README.md",
|
||||
"LICENSE",
|
||||
"NOTICE"
|
||||
],
|
||||
"description": "AI-powered UI agent for web applications - add intelligent automation to any webpage with a single script tag",
|
||||
"keywords": [
|
||||
"ai",
|
||||
"automation",
|
||||
"ui-agent",
|
||||
"browser-automation",
|
||||
"web-agent",
|
||||
"llm",
|
||||
"dom-interaction",
|
||||
"intelligent-ui"
|
||||
"workspaces": [
|
||||
"packages/page-controller",
|
||||
"packages/page-agent",
|
||||
"packages/website"
|
||||
],
|
||||
"description": "AI-powered UI agent for web applications",
|
||||
"author": "Simon<gaomeng1900>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -38,56 +17,40 @@
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "tsc -b && vite build && npm run build:lib && npm run build:umd",
|
||||
"build:lib": "MODE=lib vite build",
|
||||
"build:lib:watch": "MODE=lib vite build --watch",
|
||||
"build:umd": "MODE=umd vite build",
|
||||
"dev": "npm run dev --workspace=@page-agent/website",
|
||||
"start": "npm run dev --workspace=@page-agent/website",
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"build:lib": "npm run build --workspace=page-agent",
|
||||
"version": "node scripts/sync-version.js",
|
||||
"lint": "eslint .",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai-motion": "^0.4.7",
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@microsoft/api-extractor": "^7.53.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-dom": "^2.0.6",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"eslint-plugin-react-x": "^2.0.6",
|
||||
"eslint-plugin-react-dom": "^2.3.9",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"eslint-plugin-react-x": "^2.3.9",
|
||||
"globals": "^16.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lint-staged": "^16.2.4",
|
||||
"prettier": "^3.6.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.1.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6",
|
||||
"@microsoft/api-extractor": "^7.55.1",
|
||||
"unplugin-dts": "^1.0.0-beta.6",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||
"wouter": "^3.7.1"
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "page-agent",
|
||||
"private": false,
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-agent.js",
|
||||
"module": "./dist/lib/page-agent.js",
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib/PageAgent.d.ts",
|
||||
"import": "./dist/lib/page-agent.js",
|
||||
"default": "./dist/lib/page-agent.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"description": "GUI agent for web applications - add intelligent automation to any webpage with a single script",
|
||||
"keywords": [
|
||||
"ai",
|
||||
"automation",
|
||||
"ui-agent",
|
||||
"GUI-agent",
|
||||
"browser-automation",
|
||||
"web-agent",
|
||||
"llm",
|
||||
"dom-interaction",
|
||||
"web-automation",
|
||||
"GUI-simulation"
|
||||
],
|
||||
"author": "Simon<gaomeng1900>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alibaba/page-agent.git"
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "npm run build:lib && npm run build:umd",
|
||||
"build:lib": "MODE=lib vite build",
|
||||
"build:umd": "MODE=umd vite build",
|
||||
"build:watch": "MODE=umd vite build --watch",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
},
|
||||
"dependencies": {
|
||||
"ai-motion": "^0.4.7",
|
||||
"chalk": "^5.6.2",
|
||||
"zod": "^4.1.12",
|
||||
"@page-agent/page-controller": "0.0.7"
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,14 @@
|
||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||
* All rights reserved.
|
||||
*/
|
||||
import { PageController } from '@page-agent/page-controller'
|
||||
import chalk from 'chalk'
|
||||
import zod from 'zod'
|
||||
|
||||
import type { PageAgentConfig } from './config'
|
||||
import { MAX_STEPS, VIEWPORT_EXPANSION } from './config/constants'
|
||||
import * as dom from './dom'
|
||||
import { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
||||
import { getPageInfo } from './dom/getPageInfo'
|
||||
import { MAX_STEPS } from './config/constants'
|
||||
import { I18n } from './i18n'
|
||||
import { LLM, type Tool } from './llms'
|
||||
import { patchReact } from './patches/react'
|
||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||
import { tools } from './tools'
|
||||
import { Panel, getToolCompletedText, getToolExecutingText } from './ui/Panel'
|
||||
@@ -87,19 +84,8 @@ export class PageAgent extends EventTarget {
|
||||
#totalWaitTime = 0
|
||||
#abortController = new AbortController()
|
||||
|
||||
/** Corresponds to eval_page in browser-use */
|
||||
flatTree: FlatDomTree | null = null
|
||||
/**
|
||||
* All highlighted index-mapped interactive elements
|
||||
* Corresponds to DOMState.selector_map in browser-use
|
||||
*/
|
||||
selectorMap = new Map<number, InteractiveElementDomNode>()
|
||||
/** highlight index -> element text */
|
||||
elementTextMap = new Map<number, string>()
|
||||
/** Corresponds to clickable_elements_to_string in browser-use */
|
||||
simplifiedHTML = '<EMPTY>'
|
||||
/** last time the tree was updated */
|
||||
lastTimeUpdate = 0
|
||||
/** PageController for DOM operations */
|
||||
pageController: PageController
|
||||
|
||||
/** Fullscreen mask */
|
||||
mask = new SimulatorMask()
|
||||
@@ -115,6 +101,9 @@ export class PageAgent extends EventTarget {
|
||||
this.panel = new Panel(this)
|
||||
this.tools = new Map(tools)
|
||||
|
||||
// Initialize PageController with config
|
||||
this.pageController = new PageController(this.config)
|
||||
|
||||
if (this.config.customTools) {
|
||||
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
||||
if (tool === null) {
|
||||
@@ -125,7 +114,9 @@ export class PageAgent extends EventTarget {
|
||||
}
|
||||
}
|
||||
|
||||
patchReact(this)
|
||||
if (!this.config.experimentalScriptExecutionTool) {
|
||||
this.tools.delete('execute_javascript')
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (!this.disposed) this.dispose('PAGE_UNLOADING')
|
||||
@@ -171,7 +162,7 @@ export class PageAgent extends EventTarget {
|
||||
while (true) {
|
||||
await onBeforeStep.call(this, step)
|
||||
|
||||
console.group(`step: ${step + 1}`)
|
||||
console.group(`step: ${step}`)
|
||||
|
||||
// abort
|
||||
if (this.#abortController.signal.aborted) throw new Error('AbortError')
|
||||
@@ -193,7 +184,7 @@ export class PageAgent extends EventTarget {
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: this.#assembleUserPrompt(),
|
||||
content: await this.#assembleUserPrompt(),
|
||||
},
|
||||
],
|
||||
{ AgentOutput: this.#packMacroTool() },
|
||||
@@ -388,7 +379,7 @@ export class PageAgent extends EventTarget {
|
||||
return systemPrompt
|
||||
}
|
||||
|
||||
#assembleUserPrompt(): string {
|
||||
async #assembleUserPrompt(): Promise<string> {
|
||||
let prompt = ''
|
||||
|
||||
// <agent_history>
|
||||
@@ -426,13 +417,13 @@ export class PageAgent extends EventTarget {
|
||||
|
||||
// <browser_state>
|
||||
|
||||
prompt += this.#getBrowserState()
|
||||
prompt += await this.#getBrowserState()
|
||||
|
||||
return trimLines(prompt)
|
||||
}
|
||||
|
||||
#onDone(text: string, success = true) {
|
||||
dom.cleanUpHighlights()
|
||||
this.pageController.cleanUpHighlights()
|
||||
|
||||
// Update panel status
|
||||
this.bus.emit('panel:update', {
|
||||
@@ -451,37 +442,42 @@ export class PageAgent extends EventTarget {
|
||||
this.#abortController.abort()
|
||||
}
|
||||
|
||||
#getBrowserState(): string {
|
||||
const pageUrl = window.location.href
|
||||
const pageTitle = document.title
|
||||
const pi = getPageInfo()
|
||||
async #getBrowserState(): Promise<string> {
|
||||
const pageUrl = await this.pageController.getCurrentUrl()
|
||||
const pageTitle = await this.pageController.getPageTitle()
|
||||
const pi = await this.pageController.getPageInfo()
|
||||
const viewportExpansion = await this.pageController.getViewportExpansion()
|
||||
|
||||
this.#updateTree()
|
||||
this.mask.wrapper.style.pointerEvents = 'none'
|
||||
await this.pageController.updateTree()
|
||||
this.mask.wrapper.style.pointerEvents = 'auto'
|
||||
|
||||
const simplifiedHTML = await this.pageController.getSimplifiedHTML()
|
||||
|
||||
let prompt = trimLines(`<browser_state>
|
||||
Current Page: [${pageTitle}](${pageUrl})
|
||||
|
||||
Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page
|
||||
|
||||
${VIEWPORT_EXPANSION === -1 ? 'Interactive elements from top layer of the current page (full page):' : 'Interactive elements from top layer of the current page inside the viewport:'}
|
||||
${viewportExpansion === -1 ? 'Interactive elements from top layer of the current page (full page):' : 'Interactive elements from top layer of the current page inside the viewport:'}
|
||||
|
||||
`)
|
||||
|
||||
// Page header info
|
||||
const has_content_above = pi.pixels_above > 4
|
||||
if (has_content_above && VIEWPORT_EXPANSION !== -1) {
|
||||
if (has_content_above && viewportExpansion !== -1) {
|
||||
prompt += `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...\n`
|
||||
} else {
|
||||
prompt += `[Start of page]\n`
|
||||
}
|
||||
|
||||
// Current viewport info
|
||||
prompt += this.simplifiedHTML
|
||||
prompt += simplifiedHTML
|
||||
prompt += `\n`
|
||||
|
||||
// Page footer info
|
||||
const has_content_below = pi.pixels_below > 4
|
||||
if (has_content_below && VIEWPORT_EXPANSION !== -1) {
|
||||
if (has_content_below && viewportExpansion !== -1) {
|
||||
prompt += `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...\n`
|
||||
} else {
|
||||
prompt += `[End of page]\n`
|
||||
@@ -492,37 +488,10 @@ export class PageAgent extends EventTarget {
|
||||
return prompt
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document tree
|
||||
*/
|
||||
#updateTree() {
|
||||
this.dispatchEvent(new Event('beforeUpdate'))
|
||||
this.lastTimeUpdate = Date.now()
|
||||
dom.cleanUpHighlights()
|
||||
this.mask.wrapper.style.pointerEvents = 'none'
|
||||
this.flatTree = dom.getFlatTree({
|
||||
...this.config,
|
||||
interactiveBlacklist: [
|
||||
...(this.config.interactiveBlacklist || []),
|
||||
...document.querySelectorAll('[data-page-agent-not-interactive]').values(),
|
||||
],
|
||||
})
|
||||
this.mask.wrapper.style.pointerEvents = 'auto'
|
||||
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.include_attributes)
|
||||
this.selectorMap.clear()
|
||||
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
||||
this.elementTextMap.clear()
|
||||
this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)
|
||||
this.dispatchEvent(new Event('afterUpdate'))
|
||||
}
|
||||
|
||||
dispose(reason?: string) {
|
||||
console.log('Disposing PageAgent...')
|
||||
this.disposed = true
|
||||
dom.cleanUpHighlights()
|
||||
this.flatTree = null
|
||||
this.selectorMap.clear()
|
||||
this.elementTextMap.clear()
|
||||
this.pageController.dispose()
|
||||
this.panel.dispose()
|
||||
this.mask.dispose()
|
||||
this.history = []
|
||||
@@ -1,10 +1,3 @@
|
||||
/**
|
||||
* @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
|
||||
|
||||
// Dev environment: use .env config if available, otherwise fallback to testing api
|
||||
export const DEFAULT_MODEL_NAME: string =
|
||||
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentHistory, ExecutionResult, PageAgent } from '@/PageAgent'
|
||||
import type { DomConfig } from '@/dom'
|
||||
import type { SupportedLanguage } from '@/i18n'
|
||||
import type { PageAgentTool } from '@/tools'
|
||||
import type { PageControllerConfig } from '@page-agent/page-controller'
|
||||
|
||||
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent'
|
||||
import type { SupportedLanguage } from '../i18n'
|
||||
import type { PageAgentTool } from '../tools'
|
||||
import {
|
||||
DEFAULT_API_KEY,
|
||||
DEFAULT_BASE_URL,
|
||||
@@ -72,6 +72,14 @@ export interface AgentConfig {
|
||||
|
||||
// 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
|
||||
|
||||
/**
|
||||
* TODO: @unimplemented
|
||||
* hook when action causes a new page to be opened
|
||||
@@ -87,7 +95,7 @@ export interface AgentConfig {
|
||||
experimentalPreventNewPage?: boolean
|
||||
}
|
||||
|
||||
export type PageAgentConfig = LLMConfig & AgentConfig & DomConfig
|
||||
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
||||
|
||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||
return {
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
import type { PageAgent } from './src/PageAgent'
|
||||
import type { PageAgent } from './PageAgent'
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>
|
||||
@@ -11,15 +11,10 @@ declare module '*.md?raw' {
|
||||
export default content
|
||||
}
|
||||
|
||||
/**
|
||||
* for local dev and umd demo
|
||||
*/
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
pageAgent?: PageAgent
|
||||
PageAgent: typeof PageAgent
|
||||
|
||||
__PAGE_AGENT_IDS__: string[]
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* OpenAI Client implementation
|
||||
*/
|
||||
import type { MacroToolInput } from '@/PageAgent'
|
||||
|
||||
import type { MacroToolInput } from '../PageAgent'
|
||||
import { InvokeError, InvokeErrorType } from './errors'
|
||||
import type { InvokeResult, LLMClient, Message, OpenAIClientConfig, Tool } from './types'
|
||||
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
|
||||
@@ -31,10 +31,9 @@
|
||||
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
|
||||
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
|
||||
*/
|
||||
import type { LLMConfig } from '@/config'
|
||||
import { parseLLMConfig } from '@/config'
|
||||
import { EventBus, getEventBus } from '@/utils/bus'
|
||||
|
||||
import type { LLMConfig } from '../config'
|
||||
import { parseLLMConfig } from '../config'
|
||||
import { EventBus, getEventBus } from '../utils/bus'
|
||||
import { OpenAIClient } from './OpenAILenientClient'
|
||||
import { InvokeError } from './errors'
|
||||
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
||||
@@ -4,8 +4,7 @@
|
||||
import chalk from 'chalk'
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { MacroToolInput } from '@/PageAgent'
|
||||
|
||||
import type { MacroToolInput } from '../PageAgent'
|
||||
import { InvokeError, InvokeErrorType } from './errors'
|
||||
import type { Tool } from './types'
|
||||
|
||||
@@ -42,6 +41,7 @@ export function lenientParseMacroToolCall(
|
||||
// check
|
||||
switch (choice.finish_reason) {
|
||||
case 'tool_calls':
|
||||
case 'function_call': // gemini
|
||||
case 'stop': // will try a robust parse
|
||||
// ✅ Normal
|
||||
break
|
||||
@@ -4,23 +4,8 @@
|
||||
*/
|
||||
import zod, { type z } from 'zod'
|
||||
|
||||
import type { PageAgent } from '@/PageAgent'
|
||||
|
||||
import {
|
||||
clickElement,
|
||||
getElementByIndex,
|
||||
getSystemInfo,
|
||||
inputTextElement,
|
||||
scrollHorizontally,
|
||||
scrollVertically,
|
||||
selectOptionElement,
|
||||
waitFor,
|
||||
} from './actions'
|
||||
// debug
|
||||
import * as utils from './actions'
|
||||
|
||||
// @ts-expect-error debug only
|
||||
window.utils = utils
|
||||
import type { PageAgent } from '../PageAgent'
|
||||
import { waitFor } from '../utils'
|
||||
|
||||
/**
|
||||
* Internal tool definition that has access to PageAgent `this` context
|
||||
@@ -42,18 +27,6 @@ export function tool<TParams>(options: PageAgentTool<TParams>): PageAgentTool<TP
|
||||
*/
|
||||
export const tools = new Map<string, PageAgentTool>()
|
||||
|
||||
// tools.set(
|
||||
// 'get_current_html',
|
||||
// tool({
|
||||
// description: 'Get the current (updated) simplified HTML of the page',
|
||||
// inputSchema: zod.object({}),
|
||||
// execute: function (this: PageAgent) {
|
||||
// this.updateTree()
|
||||
// return this.simplifiedHTML
|
||||
// },
|
||||
// })
|
||||
// )
|
||||
|
||||
tools.set(
|
||||
'done',
|
||||
tool({
|
||||
@@ -80,11 +53,11 @@ tools.set(
|
||||
seconds: zod.number().min(1).max(10).default(1),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const lastTimeUpdate = this.lastTimeUpdate
|
||||
const lastTimeUpdate = await this.pageController.getLastUpdateTime()
|
||||
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
|
||||
console.log(`actualWaitTime: ${actualWaitTime} seconds`)
|
||||
await waitFor(actualWaitTime)
|
||||
return `✅ Waited for ${input.seconds} seconds.` + (await getSystemInfo())
|
||||
return `✅ Waited for ${input.seconds} seconds.`
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -99,7 +72,7 @@ tools.set(
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const answer = await this.panel.askUser(input.question)
|
||||
return `✅ Received user answer: ${answer}` + (await getSystemInfo())
|
||||
return `✅ Received user answer: ${answer}`
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -112,16 +85,8 @@ tools.set(
|
||||
index: zod.int().min(0),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const element = getElementByIndex(this, input.index)
|
||||
const elemText = this.elementTextMap.get(input.index)
|
||||
await clickElement(element)
|
||||
|
||||
// @workaround: Handle links that open in new tabs
|
||||
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
|
||||
return `⚠️ Clicked link that opens in a new tab (${elemText ? elemText : input.index}). You are not capable of reading new tabs.`
|
||||
}
|
||||
|
||||
return `✅ Clicked element (${elemText ? elemText : input.index}).` + (await getSystemInfo())
|
||||
const result = await this.pageController.clickElement(input.index)
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -135,13 +100,8 @@ tools.set(
|
||||
text: zod.string(),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const element = getElementByIndex(this, input.index)
|
||||
const elemText = this.elementTextMap.get(input.index)
|
||||
await inputTextElement(element, input.text)
|
||||
return (
|
||||
`✅ Input text (${input.text}) into element (${elemText ? elemText : input.index}).` +
|
||||
(await getSystemInfo())
|
||||
)
|
||||
const result = await this.pageController.inputText(input.index, input.text)
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -156,13 +116,8 @@ tools.set(
|
||||
text: zod.string(),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const element = getElementByIndex(this, input.index)
|
||||
const elemText = this.elementTextMap.get(input.index)
|
||||
await selectOptionElement(element as HTMLSelectElement, input.text)
|
||||
return (
|
||||
`✅ Selected option (${input.text}) in element (${elemText ? elemText : input.index}).` +
|
||||
(await getSystemInfo())
|
||||
)
|
||||
const result = await this.pageController.selectOption(input.index, input.text)
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -182,13 +137,11 @@ tools.set(
|
||||
index: zod.number().int().min(0).optional(),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const { down, num_pages, index, pixels } = input
|
||||
|
||||
const scroll_amount = pixels ? pixels : num_pages * (down ? 1 : -1) * window.innerHeight
|
||||
|
||||
const element = index !== undefined ? getElementByIndex(this, index) : null
|
||||
|
||||
return (await scrollVertically(down, scroll_amount, element)) + (await getSystemInfo())
|
||||
const result = await this.pageController.scroll({
|
||||
...input,
|
||||
numPages: input.num_pages,
|
||||
})
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -204,13 +157,23 @@ tools.set(
|
||||
index: zod.number().int().min(0).optional(),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const { right, pixels, index } = input
|
||||
const result = await this.pageController.scrollHorizontally(input)
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const scroll_amount = pixels * (right ? 1 : -1)
|
||||
|
||||
const element = index !== undefined ? getElementByIndex(this, index) : null
|
||||
|
||||
return (await scrollHorizontally(right, scroll_amount, element)) + (await getSystemInfo())
|
||||
tools.set(
|
||||
'execute_javascript',
|
||||
tool({
|
||||
description:
|
||||
'Execute JavaScript code on the current page. Supports async/await syntax. Use with caution!',
|
||||
inputSchema: zod.object({
|
||||
script: zod.string(),
|
||||
}),
|
||||
execute: async function (this: PageAgent, input) {
|
||||
const result = await this.pageController.executeJavascript(input.script)
|
||||
return result.message
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { PageAgent } from '@/PageAgent'
|
||||
import type { I18n } from '@/i18n'
|
||||
import { truncate } from '@/utils'
|
||||
import type { EventBus } from '@/utils/bus'
|
||||
|
||||
import type { PageAgent } from '../PageAgent'
|
||||
import type { I18n } from '../i18n'
|
||||
import { truncate } from '../utils'
|
||||
import type { EventBus } from '../utils/bus'
|
||||
import { type Step, UIState } from './UIState'
|
||||
|
||||
import styles from './Panel.module.css'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Motion } from 'ai-motion'
|
||||
|
||||
import { isPageDark } from '@/utils/checkDarkMode'
|
||||
import { isPageDark } from '../utils/checkDarkMode'
|
||||
|
||||
import styles from './SimulatorMask.module.css'
|
||||
import cursorStyles from './cursor.module.css'
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Type-safe event bus for decoupling PageAgent and Panel
|
||||
*/
|
||||
import type { Step } from '@/ui/UIState'
|
||||
import type { Step } from '../ui/UIState'
|
||||
|
||||
/**
|
||||
* Event mapping definitions
|
||||
@@ -23,8 +23,6 @@ export interface PageAgentEventMap {
|
||||
'panel:collapse': { params: undefined }
|
||||
|
||||
// PageAgent status events
|
||||
// 'agent:beforeUpdate': { params: undefined }
|
||||
// 'agent:afterUpdate': { params: undefined }
|
||||
// 'agent:execute': { params: { task: string } }
|
||||
// 'agent:done': { params: { text: string; success: boolean } }
|
||||
// 'agent:paused': { params: undefined }
|
||||
@@ -69,10 +67,7 @@ class EventBus extends EventTarget {
|
||||
/**
|
||||
* Listen to built-in events
|
||||
*/
|
||||
on<T extends keyof PageAgentEventMap>(
|
||||
event: T,
|
||||
handler: EventHandler<T & keyof PageAgentEventMap>
|
||||
): void {
|
||||
on<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
||||
const wrappedHandler = (e: Event) => {
|
||||
const customEvent = e as CustomEvent
|
||||
const params = customEvent.detail?.[0]
|
||||
@@ -84,10 +79,7 @@ class EventBus extends EventTarget {
|
||||
/**
|
||||
* Listen to built-in events (one-time)
|
||||
*/
|
||||
once<T extends keyof PageAgentEventMap>(
|
||||
event: T,
|
||||
handler: EventHandler<T & keyof PageAgentEventMap>
|
||||
): void {
|
||||
once<T extends keyof PageAgentEventMap>(event: T, handler: EventHandler<T>): void {
|
||||
const wrappedHandler = (e: Event) => {
|
||||
const customEvent = e as CustomEvent
|
||||
const params = customEvent.detail?.[0]
|
||||
@@ -20,6 +20,10 @@ export async function waitUntil(check: () => boolean, timeout = 60 * 60_1000): P
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitFor(seconds: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export function truncate(text: string, maxLength: number): string {
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// @workaround DTS bug
|
||||
// dts do not work with monorepo path mapping
|
||||
// disable path mapping for it
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"noEmit": false,
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"paths": {
|
||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"references": [{ "path": "../page-controller" }]
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
// @ts-check
|
||||
// ============================================================================
|
||||
// Export Configuration Based on MODE Environment Variable
|
||||
// ============================================================================
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import chalk from 'chalk'
|
||||
import 'dotenv/config'
|
||||
import process from 'node:process'
|
||||
@@ -13,44 +8,19 @@ import { fileURLToPath } from 'url'
|
||||
import { defineConfig } from 'vite'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// Website Config (React Documentation Site)
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const websiteConfig = {
|
||||
// https://vite.dev/config/
|
||||
base: './',
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@pages': resolve(__dirname, 'pages'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME),
|
||||
'import.meta.env.LLM_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
|
||||
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
|
||||
},
|
||||
}
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
// ============================================================================
|
||||
// Library Config (ES Module for NPM Package)
|
||||
// ============================================================================
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const libConfig = {
|
||||
// Library build configuration
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
|
||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
||||
// dts({ tsconfigPath: './tsconfig.json', bundleTypes: true, compilerOptions: { paths: {} } }),
|
||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
@@ -64,9 +34,8 @@ const libConfig = {
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||
rollupOptions: {
|
||||
external: ['ai', 'ai-motion', 'chalk', 'zod'],
|
||||
external: ['ai', 'ai-motion', 'chalk', 'zod', '@page-agent/*'],
|
||||
},
|
||||
// minify: 'terser',
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
cssCodeSplit: true,
|
||||
@@ -81,17 +50,16 @@ const libConfig = {
|
||||
// ============================================================================
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const umdConfig = {
|
||||
// Library build configuration
|
||||
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/entry.ts'),
|
||||
@@ -109,19 +77,15 @@ const umdConfig = {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
|
||||
const MODE = process.env.MODE
|
||||
|
||||
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'website')}`))
|
||||
console.log(chalk.cyan(`📦 Build mode: ${chalk.bold(MODE || 'lib')}`))
|
||||
|
||||
let config
|
||||
if (MODE === 'lib') {
|
||||
config = libConfig
|
||||
} else if (MODE === 'umd') {
|
||||
if (MODE === 'umd') {
|
||||
config = umdConfig
|
||||
} else {
|
||||
config = websiteConfig
|
||||
config = libConfig
|
||||
}
|
||||
|
||||
export default defineConfig(config)
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@page-agent/page-controller",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"main": "./dist/lib/page-controller.js",
|
||||
"module": "./dist/lib/page-controller.js",
|
||||
"types": "./dist/lib/PageController.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/lib/PageController.d.ts",
|
||||
"import": "./dist/lib/page-controller.js",
|
||||
"default": "./dist/lib/page-controller.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"description": "Page controller for page-agent - DOM operations and element interactions",
|
||||
"keywords": [
|
||||
"page-agent",
|
||||
"dom",
|
||||
"browser-automation",
|
||||
"web-automation"
|
||||
],
|
||||
"author": "Simon<gaomeng1900>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alibaba/page-agent.git"
|
||||
},
|
||||
"homepage": "https://alibaba.github.io/page-agent/",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||
* All rights reserved.
|
||||
*
|
||||
* PageController - Manages DOM operations and element interactions.
|
||||
* Designed to be independent of LLM and can be tested in unit tests.
|
||||
* All public methods are async for potential remote calling support.
|
||||
*/
|
||||
import {
|
||||
clickElement,
|
||||
getElementByIndex,
|
||||
inputTextElement,
|
||||
scrollHorizontally,
|
||||
scrollVertically,
|
||||
selectOptionElement,
|
||||
} from './actions'
|
||||
import { VIEWPORT_EXPANSION } from './constants'
|
||||
import * as dom from './dom'
|
||||
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
|
||||
import { getPageInfo } from './dom/getPageInfo'
|
||||
import { patchReact } from './patches/react'
|
||||
|
||||
/**
|
||||
* Configuration for PageController
|
||||
*/
|
||||
export interface PageControllerConfig extends dom.DomConfig {
|
||||
viewportExpansion?: number
|
||||
}
|
||||
|
||||
interface ActionResult {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* PageController manages DOM state and element interactions.
|
||||
* It provides async methods for all DOM operations, keeping state isolated.
|
||||
*
|
||||
* @lifecycle
|
||||
* - beforeUpdate: Emitted before the DOM tree is updated.
|
||||
* - afterUpdate: Emitted after the DOM tree is updated.
|
||||
*/
|
||||
export class PageController extends EventTarget {
|
||||
private config: PageControllerConfig
|
||||
|
||||
/** Corresponds to eval_page in browser-use */
|
||||
private flatTree: FlatDomTree | null = null
|
||||
|
||||
/**
|
||||
* All highlighted index-mapped interactive elements
|
||||
* Corresponds to DOMState.selector_map in browser-use
|
||||
*/
|
||||
private selectorMap = new Map<number, InteractiveElementDomNode>()
|
||||
|
||||
/** Index -> element text description mapping */
|
||||
private elementTextMap = new Map<number, string>()
|
||||
|
||||
/**
|
||||
* Simplified HTML for LLM consumption.
|
||||
* Corresponds to clickable_elements_to_string in browser-use
|
||||
*/
|
||||
private simplifiedHTML = '<EMPTY>'
|
||||
|
||||
/** last time the tree was updated */
|
||||
private lastTimeUpdate = 0
|
||||
|
||||
constructor(config: PageControllerConfig = {}) {
|
||||
super()
|
||||
|
||||
this.config = config
|
||||
|
||||
patchReact(this)
|
||||
}
|
||||
|
||||
// ======= State Queries =======
|
||||
|
||||
/**
|
||||
* Get current page URL
|
||||
*/
|
||||
async getCurrentUrl(): Promise<string> {
|
||||
return window.location.href
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current page title
|
||||
*/
|
||||
async getPageTitle(): Promise<string> {
|
||||
return document.title
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page scroll and size info
|
||||
*/
|
||||
async getPageInfo() {
|
||||
return getPageInfo()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the simplified HTML representation of the page.
|
||||
* This is used by LLM to understand the page structure.
|
||||
*/
|
||||
async getSimplifiedHTML(): Promise<string> {
|
||||
return this.simplifiedHTML
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text description for an element by index
|
||||
*/
|
||||
async getElementText(index: number): Promise<string | undefined> {
|
||||
return this.elementTextMap.get(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of indexed interactive elements
|
||||
*/
|
||||
async getElementCount(): Promise<number> {
|
||||
return this.selectorMap.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last tree update timestamp
|
||||
*/
|
||||
async getLastUpdateTime(): Promise<number> {
|
||||
return this.lastTimeUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the viewport expansion setting
|
||||
*/
|
||||
async getViewportExpansion(): Promise<number> {
|
||||
return this.config.viewportExpansion ?? VIEWPORT_EXPANSION
|
||||
}
|
||||
|
||||
// ======= DOM Tree Operations =======
|
||||
|
||||
/**
|
||||
* Update DOM tree, returns simplified HTML for LLM.
|
||||
* This is the main method to refresh the page state.
|
||||
*/
|
||||
async updateTree(): Promise<string> {
|
||||
this.dispatchEvent(new Event('beforeUpdate'))
|
||||
|
||||
this.lastTimeUpdate = Date.now()
|
||||
|
||||
dom.cleanUpHighlights()
|
||||
|
||||
const blacklist = [
|
||||
...(this.config.interactiveBlacklist || []),
|
||||
...document.querySelectorAll('[data-page-agent-not-interactive]').values(),
|
||||
]
|
||||
|
||||
this.flatTree = dom.getFlatTree({
|
||||
...this.config,
|
||||
interactiveBlacklist: blacklist,
|
||||
})
|
||||
|
||||
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.include_attributes)
|
||||
|
||||
this.selectorMap.clear()
|
||||
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
||||
|
||||
this.elementTextMap.clear()
|
||||
this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)
|
||||
|
||||
this.dispatchEvent(new Event('afterUpdate'))
|
||||
|
||||
return this.simplifiedHTML
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all element highlights
|
||||
*/
|
||||
async cleanUpHighlights(): Promise<void> {
|
||||
dom.cleanUpHighlights()
|
||||
}
|
||||
|
||||
// ======= Element Actions =======
|
||||
|
||||
/**
|
||||
* Click element by index
|
||||
*/
|
||||
async clickElement(index: number): Promise<ActionResult> {
|
||||
try {
|
||||
const element = getElementByIndex(this.selectorMap, index)
|
||||
const elemText = this.elementTextMap.get(index)
|
||||
await clickElement(element)
|
||||
|
||||
// Handle links that open in new tabs
|
||||
if (element instanceof HTMLAnchorElement && element.target === '_blank') {
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opens in a new tab. You are not capable of reading new tabs.`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Clicked element (${elemText ?? index}).`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Failed to click element: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input text into element by index
|
||||
*/
|
||||
async inputText(index: number, text: string): Promise<ActionResult> {
|
||||
try {
|
||||
const element = getElementByIndex(this.selectorMap, index)
|
||||
const elemText = this.elementTextMap.get(index)
|
||||
await inputTextElement(element, text)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Input text (${text}) into element (${elemText ?? index}).`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Failed to input text: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select dropdown option by index and option text
|
||||
*/
|
||||
async selectOption(index: number, optionText: string): Promise<ActionResult> {
|
||||
try {
|
||||
const element = getElementByIndex(this.selectorMap, index)
|
||||
const elemText = this.elementTextMap.get(index)
|
||||
await selectOptionElement(element as HTMLSelectElement, optionText)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Selected option (${optionText}) in element (${elemText ?? index}).`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Failed to select option: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll vertically
|
||||
*/
|
||||
async scroll(options: {
|
||||
down: boolean
|
||||
numPages: number
|
||||
pixels?: number
|
||||
index?: number
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const { down, numPages, pixels, index } = options
|
||||
|
||||
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
|
||||
|
||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||
|
||||
const message = await scrollVertically(down, scrollAmount, element)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Failed to scroll: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll horizontally
|
||||
*/
|
||||
async scrollHorizontally(options: {
|
||||
right: boolean
|
||||
pixels: number
|
||||
index?: number
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const { right, pixels, index } = options
|
||||
|
||||
const scrollAmount = pixels * (right ? 1 : -1)
|
||||
|
||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||
|
||||
const message = await scrollHorizontally(right, scrollAmount, element)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Failed to scroll horizontally: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute arbitrary JavaScript on the page
|
||||
*/
|
||||
async executeJavascript(script: string): Promise<ActionResult> {
|
||||
try {
|
||||
// Wrap script in async function to support await
|
||||
const asyncFunction = eval(`(async () => { ${script} })`)
|
||||
const result = await asyncFunction()
|
||||
return {
|
||||
success: true,
|
||||
message: `✅ Executed JavaScript. Result: ${result}`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `❌ Error executing JavaScript: ${error}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose and clean up resources
|
||||
*/
|
||||
dispose(): void {
|
||||
dom.cleanUpHighlights()
|
||||
this.flatTree = null
|
||||
this.selectorMap.clear()
|
||||
this.elementTextMap.clear()
|
||||
this.simplifiedHTML = '<EMPTY>'
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,14 @@
|
||||
* Copyright (C) 2025 Alibaba Group Holding Limited
|
||||
* All rights reserved.
|
||||
*/
|
||||
import type { PageAgent } from '../PageAgent'
|
||||
import type { InteractiveElementDomNode } from './dom/dom_tree/type'
|
||||
|
||||
// ======= general utils =======
|
||||
|
||||
export async function waitFor(seconds: number): Promise<void> {
|
||||
async function waitFor(seconds: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
|
||||
}
|
||||
|
||||
let currentUrl = window.location.href
|
||||
export async function getSystemInfo() {
|
||||
// If current URL is already up to date, no need to add message
|
||||
if (currentUrl === window.location.href) return ''
|
||||
|
||||
await waitFor(0.3) // Wait a bit longer for page to load
|
||||
|
||||
currentUrl = window.location.href
|
||||
|
||||
return `\n<sys> Current URL changed to: ${currentUrl} </sys>`
|
||||
}
|
||||
|
||||
// ======= dom utils =======
|
||||
|
||||
export async function movePointerToElement(element: HTMLElement) {
|
||||
@@ -35,10 +23,13 @@ export async function movePointerToElement(element: HTMLElement) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTMLElement by index from the selectorMap in PageAgent.
|
||||
* Get the HTMLElement by index from a selectorMap.
|
||||
*/
|
||||
export function getElementByIndex(pageAgent: PageAgent, index: number): HTMLElement {
|
||||
const interactiveNode = pageAgent.selectorMap.get(index)
|
||||
export function getElementByIndex(
|
||||
selectorMap: Map<number, InteractiveElementDomNode>,
|
||||
index: number
|
||||
): HTMLElement {
|
||||
const interactiveNode = selectorMap.get(index)
|
||||
if (!interactiveNode) {
|
||||
throw new Error(`No interactive element found at index ${index}`)
|
||||
}
|
||||
@@ -170,7 +161,6 @@ export async function selectOptionElement(selectElement: HTMLSelectElement, opti
|
||||
await waitFor(0.1) // Wait to ensure change event processing completes
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export async function scrollIntoViewIfNeeded(element: HTMLElement) {
|
||||
const el = element as any
|
||||
if (el.scrollIntoViewIfNeeded) {
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -1,11 +1,11 @@
|
||||
import { VIEWPORT_EXPANSION } from '@/config/constants'
|
||||
import domTree from '@/dom/dom_tree/index'
|
||||
import { VIEWPORT_EXPANSION } from '../constants'
|
||||
import domTree from './dom_tree/index.js'
|
||||
import {
|
||||
ElementDomNode,
|
||||
FlatDomTree,
|
||||
InteractiveElementDomNode,
|
||||
TextDomNode,
|
||||
} from '@/dom/dom_tree/type'
|
||||
} from './dom_tree/type'
|
||||
|
||||
export interface DomConfig {
|
||||
interactiveBlacklist?: (Element | (() => Element))[]
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageAgent } from '@/PageAgent'
|
||||
import type { PageController } from '../PageController'
|
||||
|
||||
const clearFunctions = [] as (() => void)[]
|
||||
|
||||
@@ -11,9 +11,9 @@ function fixAntdSelect() {
|
||||
// for (const select of selects) {}
|
||||
}
|
||||
|
||||
export function patchAntd(pageAgent: PageAgent) {
|
||||
pageAgent.addEventListener('beforeUpdate', fixAntdSelect)
|
||||
pageAgent.addEventListener('afterUpdate', () => {
|
||||
export function patchAntd(pageController: PageController) {
|
||||
pageController.addEventListener('beforeUpdate', fixAntdSelect)
|
||||
pageController.addEventListener('afterUpdate', () => {
|
||||
for (const fn of clearFunctions) fn()
|
||||
clearFunctions.length = 0
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageAgent } from '@/PageAgent'
|
||||
import type { PageController } from '../PageController'
|
||||
|
||||
// Find common React root elements and add data-page-agent-not-interactive attribute
|
||||
export function patchReact(pageAgent: PageAgent) {
|
||||
export function patchReact(pageController: PageController) {
|
||||
const reactRootElements = document.querySelectorAll(
|
||||
'[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root'
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"noEmit": false,
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-check
|
||||
import chalk from 'chalk'
|
||||
import { dirname, resolve } from 'path'
|
||||
import dts from 'unplugin-dts/vite'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { defineConfig } from 'vite'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
console.log(chalk.cyan(`📦 Building @page-agent/page-controller`))
|
||||
|
||||
export default defineConfig({
|
||||
clearScreen: false,
|
||||
plugins: [
|
||||
dts({ tsconfigPath: './tsconfig.json', bundleTypes: true }),
|
||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||
],
|
||||
publicDir: false,
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/PageController.ts'),
|
||||
name: 'PageController',
|
||||
fileName: 'page-controller',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'dist', 'lib'),
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
},
|
||||
minify: false,
|
||||
sourcemap: true,
|
||||
cssCodeSplit: true,
|
||||
},
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
# Landing Page & Docs
|
||||
@@ -0,0 +1,60 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="https://img.alicdn.com/imgextra/i2/O1CN012eGDRI1X6nnMt9clU_!!6000000002875-49-tps-64-64.webp"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PageAgent - The GUI Agent Living in Your Webpage</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="PageAgent.js: Intelligent GUI Agent for any website. Modern web AI automation with minimal integration."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="PageAgent, AI Agent, GUI Agent, Web Automation, GUI Automation, Frontend, CDN, JavaScript, React, Vite, LLM"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
|
||||
/>
|
||||
<meta property="og:url" content="https://alibaba.github.io/page-agent" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="theme-color" content="#58c0fc" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="author" content="PageAgent.js Team" />
|
||||
<meta property="og:title" content="PageAgent.js - AI-powered GUI Agent" />
|
||||
<meta property="og:description" content="The GUI Agent living in your website." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:locale:alternate" content="zh_CN" />
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HCGRJTN3HM"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag() {
|
||||
dataLayer.push(arguments)
|
||||
}
|
||||
gtag('js', new Date())
|
||||
|
||||
gtag('config', 'G-HCGRJTN3HM')
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
<script>
|
||||
// Dynamically update html lang attribute based on i18n detection
|
||||
const updateHtmlLang = () => {
|
||||
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
|
||||
document.documentElement.lang = lang
|
||||
}
|
||||
updateHtmlLang()
|
||||
window.addEventListener('storage', updateHtmlLang)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@page-agent/website",
|
||||
"private": true,
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"page-agent": "*",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"i18next": "^25.7.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.1.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"wouter": "^3.7.1"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export default function BetaNotice() {
|
||||
return (
|
||||
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4 mb-8">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="shrink-0">
|
||||
<span className="text-xl">🚧</span>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
@@ -28,7 +28,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
|
||||
// 使用 Tailwind 的 dark: 前缀实现自动主题切换
|
||||
const containerClasses =
|
||||
'bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
|
||||
'bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-gray-300 dark:border-gray-700'
|
||||
const headerClasses = 'bg-gray-100 dark:bg-gray-800 border-gray-300 dark:border-gray-700'
|
||||
const headerTextClasses = 'text-gray-700 dark:text-gray-300'
|
||||
const languageTextClasses = 'text-gray-600 dark:text-gray-400'
|
||||
@@ -72,7 +72,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
<div className="flex">
|
||||
{/* 行号 */}
|
||||
{showLineNumbers && (
|
||||
<div className={`flex-shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
|
||||
<div className={`shrink-0 px-4 py-4 ${lineNumbersClasses} border-r select-none`}>
|
||||
<div className="text-xs font-mono leading-6">
|
||||
{lines.map((line, lineIdx) => {
|
||||
const lineNum = lineIdx + 1
|
||||
@@ -54,7 +54,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
|
||||
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
|
||||
<div className="flex gap-8 min-w-[900px]">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 flex-shrink-0" role="complementary" aria-label="文档导航">
|
||||
<aside className="w-64 shrink-0" role="complementary" aria-label="文档导航">
|
||||
<div className="sticky top-8">
|
||||
<nav className="space-y-8" role="navigation" aria-label="文档章节">
|
||||
{navigationSections.map((section) => (
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'wouter'
|
||||
|
||||
import LanguageSwitcher from './LanguageSwitcher'
|
||||
import ThemeSwitcher from './ThemeSwitcher'
|
||||
import { BookIcon, CloseIcon, GithubIcon, MenuIcon } from './icons'
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useTranslation('common')
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
|
||||
role="banner"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 sm:gap-3 group shrink-0"
|
||||
aria-label={t('header.logo_alt')}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<img
|
||||
src="https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp"
|
||||
alt="PageAgent Logo"
|
||||
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight">
|
||||
page-agent
|
||||
</span>
|
||||
<p className="hidden sm:block text-xs text-gray-600 dark:text-gray-300">
|
||||
{t('header.slogan')}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Mobile Icon Navigation (横向滚动) */}
|
||||
<nav
|
||||
className="md:hidden flex items-center gap-1 overflow-x-auto scrollbar-hide flex-1"
|
||||
role="navigation"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<Link
|
||||
href="/docs/introduction/overview"
|
||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||
aria-label={t('header.nav_docs')}
|
||||
>
|
||||
<BookIcon className="w-5 h-5" />
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon className="w-5 h-5" />
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav
|
||||
className="hidden md:flex items-center space-x-6"
|
||||
role="navigation"
|
||||
aria-label={t('header.nav_docs')}
|
||||
>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
<BookIcon />
|
||||
{t('header.nav_docs')}
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon />
|
||||
{t('header.nav_source')}
|
||||
</a>
|
||||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
type="button"
|
||||
className="md:hidden p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 shrink-0"
|
||||
aria-label={t('header.mobile_menu')}
|
||||
aria-expanded={mobileMenuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<nav
|
||||
id="mobile-menu"
|
||||
className="md:hidden pt-4 pb-2 space-y-3 border-t border-gray-200 dark:border-gray-700 mt-4"
|
||||
role="navigation"
|
||||
>
|
||||
<Link
|
||||
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"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<BookIcon className="w-5 h-5" />
|
||||
{t('header.nav_docs')}
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/alibaba/page-agent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
|
||||
aria-label={t('header.nav_source')}
|
||||
>
|
||||
<GithubIcon className="w-5 h-5" />
|
||||
{t('header.nav_source')}
|
||||
</a>
|
||||
<div className="flex items-center gap-3 px-3 py-2">
|
||||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
.syntax {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
font-family: monospace;
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
:global(.dark) .syntax {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* JavaScript/TypeScript 关键字 */
|
||||
.keyword {
|
||||
color: #d73a49;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark) .keyword {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* TypeScript 特定关键字 (interface, type, enum, etc.) */
|
||||
.tsKeyword {
|
||||
color: #af00db;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark) .tsKeyword {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
/* TypeScript 内置类型 */
|
||||
.type {
|
||||
color: #267f99;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.dark) .type {
|
||||
color: #4ec9b0;
|
||||
}
|
||||
|
||||
/* 字符串 */
|
||||
.string {
|
||||
color: #1d6eca;
|
||||
}
|
||||
|
||||
:global(.dark) .string {
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
/* 数字 */
|
||||
.number {
|
||||
color: #00c583;
|
||||
}
|
||||
|
||||
:global(.dark) .number {
|
||||
color: #66bb6a;
|
||||
}
|
||||
|
||||
/* 布尔值和字面量 (true, false, null, undefined) */
|
||||
.literal {
|
||||
color: #0000ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.dark) .literal {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
/* 注释 */
|
||||
.comment {
|
||||
color: #6a737d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.dark) .comment {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
/* 装饰器 (@decorator) */
|
||||
.decorator {
|
||||
color: #e0aa00;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.dark) .decorator {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
/* 箭头函数 (=>) */
|
||||
.arrow {
|
||||
color: #d73a49;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:global(.dark) .arrow {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 标识符(变量名、函数名等) */
|
||||
.identifier {
|
||||
color: #171717;
|
||||
}
|
||||
|
||||
:global(.dark) .identifier {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 属性访问 (.property) */
|
||||
.property {
|
||||
color: #0550ae;
|
||||
}
|
||||
|
||||
:global(.dark) .property {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
/* 运算符 */
|
||||
.operator {
|
||||
color: #5a5a5a;
|
||||
}
|
||||
|
||||
:global(.dark) .operator {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* js 语法高亮组件,适合在文章中演示代码片段
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
import styles from './HighlightSyntax.module.css'
|
||||
|
||||
interface HighlightSyntaxProps {
|
||||
code: string
|
||||
}
|
||||
|
||||
// JavaScript/TypeScript 关键字
|
||||
const keywords =
|
||||
'async|await|function|const|let|var|if|else|for|while|return|try|catch|finally|class|extends|from|import|export|default|undefined|throw|break|continue|switch|case|do|with|yield|delete|typeof|void|static|get|set|super|debugger'
|
||||
|
||||
// TypeScript 特定关键字
|
||||
const tsKeywords =
|
||||
'interface|type|enum|namespace|module|declare|abstract|implements|public|private|protected|readonly|as|satisfies|infer|keyof|is'
|
||||
|
||||
// 布尔值和空值
|
||||
const literals = 'true|false|null|undefined|NaN|Infinity'
|
||||
|
||||
// TypeScript 内置类型
|
||||
const tsTypes =
|
||||
'string|number|boolean|any|unknown|never|void|object|symbol|bigint|Array|Promise|Record|Partial|Required|Readonly|Pick|Omit|Exclude|Extract|NonNullable|ReturnType|Parameters|ConstructorParameters|InstanceType|ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize'
|
||||
|
||||
// 辅助函数:转义 HTML 特殊字符
|
||||
function escapeHtml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
// 语法高亮函数,先提取 token 再转义和高亮
|
||||
function highlightSyntax(code: string): string {
|
||||
// 构建正则模式,包含更多 token 类型(在原始文本上匹配)
|
||||
const pattern = new RegExp(
|
||||
'(' +
|
||||
// 1. 字符串(双引号、单引号、模板字符串)
|
||||
'"([^"\\\\]|\\\\.)*"|' +
|
||||
"'([^'\\\\]|\\\\.)*'|" +
|
||||
'`([^`\\\\]|\\\\.)*`|' +
|
||||
// 2. 注释(单行和多行)
|
||||
'//[^\\n]*|' +
|
||||
'/\\*[\\s\\S]*?\\*/|' +
|
||||
// 3. 装饰器
|
||||
'@[a-zA-Z_$][\\w$]*|' +
|
||||
// 4. 数字(包括小数、十六进制、科学计数法)
|
||||
'\\b0[xX][0-9a-fA-F]+\\b|' +
|
||||
'\\b\\d+\\.?\\d*(?:[eE][+-]?\\d+)?\\b|' +
|
||||
// 5. TypeScript/JavaScript 关键字
|
||||
'\\b(?:' +
|
||||
keywords +
|
||||
'|' +
|
||||
tsKeywords +
|
||||
'|' +
|
||||
literals +
|
||||
')\\b|' +
|
||||
// 6. TypeScript 内置类型
|
||||
'\\b(?:' +
|
||||
tsTypes +
|
||||
')\\b|' +
|
||||
// 7. 箭头函数
|
||||
'=>|' +
|
||||
// 8. 函数调用(函数名后跟括号)
|
||||
'\\b[a-zA-Z_$][\\w$]*(?=\\()|' +
|
||||
// 9. 属性访问
|
||||
'\\.[a-zA-Z_$][\\w$]*|' +
|
||||
// 10. 运算符和特殊符号
|
||||
'[+\\-*/%&|^!~<>=?:]+|' +
|
||||
'[{}\\[\\]();,.]' +
|
||||
')',
|
||||
'g'
|
||||
)
|
||||
|
||||
const tokens: string[] = []
|
||||
let lastIndex = 0
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(code)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
const gap = code.slice(lastIndex, match.index)
|
||||
// 将间隙按空白符分割,保留空白符
|
||||
tokens.push(...gap.split(/(\s+)/))
|
||||
}
|
||||
tokens.push(match[0])
|
||||
lastIndex = pattern.lastIndex
|
||||
}
|
||||
if (lastIndex < code.length) {
|
||||
tokens.push(...code.slice(lastIndex).split(/(\s+)/))
|
||||
}
|
||||
|
||||
const highlighted = tokens
|
||||
.map((token) => {
|
||||
// 空白符直接返回
|
||||
if (/^\s+$/.test(token)) {
|
||||
return token
|
||||
}
|
||||
|
||||
// 1. 注释(单行和多行)
|
||||
if (/^\/\/.*$/.test(token) || /^\/\*[\s\S]*?\*\/$/.test(token)) {
|
||||
return `<span class="${styles.comment}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 2. 字符串
|
||||
if (
|
||||
/^"([^"\\]|\\.)*"$/.test(token) ||
|
||||
/^'([^'\\]|\\.)*'$/.test(token) ||
|
||||
/^`([^`\\]|\\.)*`$/.test(token)
|
||||
) {
|
||||
return `<span class="${styles.string}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 3. 数字
|
||||
if (/^(0[xX][0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?)$/.test(token)) {
|
||||
return `<span class="${styles.number}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 4. 布尔值和特殊字面量
|
||||
if (new RegExp(`^(?:${literals})$`).test(token)) {
|
||||
return `<span class="${styles.literal}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 5. JavaScript/TypeScript 关键字
|
||||
if (new RegExp(`^(?:${keywords})$`).test(token)) {
|
||||
return `<span class="${styles.keyword}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 6. TypeScript 特定关键字
|
||||
if (new RegExp(`^(?:${tsKeywords})$`).test(token)) {
|
||||
return `<span class="${styles.tsKeyword}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 7. TypeScript 内置类型
|
||||
if (new RegExp(`^(?:${tsTypes})$`).test(token)) {
|
||||
return `<span class="${styles.type}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 8. 装饰器
|
||||
if (/^@[a-zA-Z_$][\w$]*$/.test(token)) {
|
||||
return `<span class="${styles.decorator}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 9. 箭头函数
|
||||
if (token === '=>') {
|
||||
return `<span class="${styles.arrow}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 10. 函数调用和标识符
|
||||
if (/^[a-zA-Z_$][\w$]*$/.test(token)) {
|
||||
return `<span class="${styles.identifier}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 11. 属性访问
|
||||
if (/^\.[a-zA-Z_$][\w$]*$/.test(token)) {
|
||||
return `<span class="${styles.property}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 12. 运算符
|
||||
if (/^[+\-*/%&|^!~<>=?:]+$/.test(token)) {
|
||||
return `<span class="${styles.operator}">${escapeHtml(token)}</span>`
|
||||
}
|
||||
|
||||
// 13. 其他符号,需要转义
|
||||
return escapeHtml(token)
|
||||
})
|
||||
.join('')
|
||||
|
||||
return highlighted
|
||||
}
|
||||
|
||||
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
|
||||
const htmlContent = highlightSyntax(code)
|
||||
|
||||
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
|
||||
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
}
|
||||
|
||||
export default HighlightSyntaxClient
|
||||
+1
-1
@@ -56,7 +56,7 @@ export default function ThemeSwitcher() {
|
||||
style={{
|
||||
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
|
||||
}}
|
||||
aria-label={theme === 'light' ? '切换到深色模式' : '切换到浅色模式'}
|
||||
aria-label={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
||||
role="switch"
|
||||
aria-checked={theme === 'dark'}
|
||||
>
|
||||
@@ -0,0 +1,75 @@
|
||||
// SVG图标组件集合,用于Header等地方复用
|
||||
|
||||
interface IconProps {
|
||||
className?: string
|
||||
'aria-hidden'?: boolean
|
||||
}
|
||||
|
||||
export function BookIcon({ className = 'w-4 h-4', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GithubIcon({ className = 'w-4 h-4', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MenuIcon({ className = 'w-6 h-6', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloseIcon({ className = 'w-6 h-6', ...props }: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function CustomTools() {
|
||||
const { t } = useTranslation('docs')
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function DataMasking() {
|
||||
return (
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function KnowledgeInjection() {
|
||||
return (
|
||||
+29
-2
@@ -1,7 +1,8 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function ModelIntegration() {
|
||||
const { t } = useTranslation('docs')
|
||||
|
||||
@@ -106,6 +107,32 @@ export default function ModelIntegration() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.security')}</h2>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-l-4 border-yellow-500 mb-4">
|
||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||
{t('model_integration.security_warning')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{t('model_integration.security_desc')}
|
||||
</p>
|
||||
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg mb-6">
|
||||
<h3 className="text-lg font-semibold mb-3 text-blue-900 dark:text-blue-300">
|
||||
{t('model_integration.security_backend_proxy')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||
{t('model_integration.security_backend_desc')}
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1 list-none pl-0">
|
||||
<li>{t('model_integration.security_method_1')}</li>
|
||||
<li>{t('model_integration.security_method_2')}</li>
|
||||
<li>{t('model_integration.security_method_3')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-3">{t('model_integration.configuration')}</h2>
|
||||
|
||||
<CodeEditor
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
|
||||
export default function SecurityPermissions() {
|
||||
return (
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function BestPractices() {
|
||||
return (
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function CdnSetup() {
|
||||
return (
|
||||
@@ -16,7 +16,7 @@ export default function CdnSetup() {
|
||||
className="mb-8"
|
||||
code={`
|
||||
// 仅供测试使用,稳定 CDN 待定
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" crossorigin="true" type="text/javascript"></script>
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
window.pageAgent.panel.show()
|
||||
+12
-3
@@ -1,4 +1,4 @@
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function Configuration() {
|
||||
return (
|
||||
@@ -9,7 +9,7 @@ export default function Configuration() {
|
||||
className="mb-8"
|
||||
language="typescript"
|
||||
code={`// config
|
||||
type PageAgentConfig = LLMConfig & AgentConfig & DomConfig
|
||||
type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
|
||||
|
||||
interface LLMConfig {
|
||||
baseURL?: string
|
||||
@@ -70,6 +70,14 @@ interface AgentConfig {
|
||||
|
||||
// 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
|
||||
|
||||
/**
|
||||
* TODO: @unimplemented
|
||||
* hook when action causes a new page to be opened
|
||||
@@ -85,12 +93,13 @@ interface AgentConfig {
|
||||
experimentalPreventNewPage?: boolean
|
||||
}
|
||||
|
||||
interface DomConfig {
|
||||
interface PageControllerConfig {
|
||||
interactiveBlacklist?: (Element | (() => Element))[]
|
||||
interactiveWhitelist?: (Element | (() => Element))[]
|
||||
include_attributes?: string[]
|
||||
highlightOpacity?: number
|
||||
highlightLabelOpacity?: number
|
||||
viewportExpansion?: number
|
||||
}
|
||||
|
||||
`}
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function ThirdPartyAgentPage() {
|
||||
return (
|
||||
@@ -48,25 +48,25 @@ const pageAgentTool = {
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">应用场景</h2>
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<div className="bg-linear-to-br from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🤖 智能客服系统</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
客服机器人帮用户直接操作系统,如"帮我提交工单"
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<div className="bg-linear-to-br from-green-50 to-blue-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">📋 业务流程助手</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
引导新员工完成复杂流程,如"完成客户入职"
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<div className="bg-linear-to-br from-purple-50 to-pink-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🎯 个人效率助手</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
跨网站帮你完成任务,如"预订会议室"
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<div className="bg-linear-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-700 p-4 rounded-lg">
|
||||
<h4 className="font-semibold mb-2 text-gray-900 dark:text-white">🔧 运维自动化</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
通过自然语言操作管理后台,如"重启服务器"
|
||||
@@ -98,7 +98,7 @@ const pageAgentTool = {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
|
||||
<div className="bg-linear-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 p-4 rounded-lg">
|
||||
<h3 className="text-lg font-semibold mb-2 text-gray-900 dark:text-white">🎉 开始集成</h3>
|
||||
<p className="mb-3 text-gray-700 dark:text-gray-300">
|
||||
通过这种方式,你的 Agent 系统就能真正成为用户的智能助手。
|
||||
+4
-4
@@ -174,7 +174,7 @@ export default function Overview() {
|
||||
|
||||
<ul className="space-y-4 mb-8">
|
||||
<li className="flex items-start space-x-3">
|
||||
<span className="w-6 h-6 min-w-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 flex-shrink-0">
|
||||
<span className="w-6 h-6 min-w-6 bg-blue-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
|
||||
1
|
||||
</span>
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
@@ -182,7 +182,7 @@ export default function Overview() {
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<span className="w-6 h-6 min-w-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 flex-shrink-0">
|
||||
<span className="w-6 h-6 min-w-6 bg-green-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
|
||||
2
|
||||
</span>
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
@@ -190,7 +190,7 @@ export default function Overview() {
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<span className="w-6 h-6 min-w-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 flex-shrink-0">
|
||||
<span className="w-6 h-6 min-w-6 bg-purple-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
|
||||
3
|
||||
</span>
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
@@ -198,7 +198,7 @@ export default function Overview() {
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<span className="w-6 h-6 min-w-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 flex-shrink-0">
|
||||
<span className="w-6 h-6 min-w-6 bg-orange-500 text-white rounded-full flex items-center justify-center font-bold text-sm mt-0.5 shrink-0">
|
||||
4
|
||||
</span>
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
import BetaNotice from '@pages/components/BetaNotice'
|
||||
import CodeEditor from '@pages/components/CodeEditor'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import BetaNotice from '@/components/BetaNotice'
|
||||
import CodeEditor from '@/components/CodeEditor'
|
||||
|
||||
export default function QuickStart() {
|
||||
const { t } = useTranslation('docs')
|
||||
|
||||
@@ -23,7 +24,7 @@ export default function QuickStart() {
|
||||
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p>
|
||||
<CodeEditor
|
||||
code={`// 仅供测试使用
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.2/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
|
||||
<script src="https://hwcxiuzfylggtcktqgij.supabase.co/storage/v1/object/public/demo-public/v0.0.4/page-agent.js" crossorigin="true" type="text/javascript"></script>`}
|
||||
language="html"
|
||||
/>
|
||||
</div>
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: Record<string, string>
|
||||
export default classes
|
||||
}
|
||||
@@ -2,14 +2,14 @@ export default {
|
||||
overview: {
|
||||
title: 'Overview',
|
||||
subtitle:
|
||||
'page-agent is a pure web-based GUI Agent. Simple integration gives your website an AI operator.',
|
||||
'page-agent is a purely web-based GUI Agent. Gives your website an AI operator in simple steps.',
|
||||
what_is: 'What is page-agent?',
|
||||
what_is_desc:
|
||||
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or generic agent builders. Integrate it into your site to let users interact with pages through natural language.',
|
||||
'page-agent is an embedded GUI Agent. Unlike traditional browser automation tools, page-agent targets web developers, not scrapers or agent clients builders. Integrate it into your site to let users interact with pages through natural language.',
|
||||
features_title: 'Core Features',
|
||||
feature_dom: {
|
||||
title: '🧠 Smart DOM Analysis',
|
||||
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed—pure text for precise operations.',
|
||||
desc: 'DOM-based analysis with high-intensity dehydration. No visual recognition needed. Pure text for fast and precise operations.',
|
||||
},
|
||||
feature_secure: {
|
||||
title: '🔒 Secure & Controllable',
|
||||
@@ -141,6 +141,16 @@ export default {
|
||||
tip_2:
|
||||
"Non-OpenAI models don't guarantee JSON schema compliance—tool call may error but usually recovers. Higher temperature recommended",
|
||||
tip_3: 'Small/nano models perform poorly',
|
||||
security: '🔐 Production Authentication',
|
||||
security_warning: '⚠️ Never commit real LLM API Keys to your frontend codebase',
|
||||
security_desc:
|
||||
'In production environments, to hide the real LLM API Keys, we recommend the following architecture:',
|
||||
security_backend_proxy: 'Backend Proxy Pattern',
|
||||
security_backend_desc:
|
||||
'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:',
|
||||
security_method_1: '• Session/Cookie-based authentication',
|
||||
security_method_2: '• OIDC (OpenID Connect) single sign-on',
|
||||
security_method_3: '• Temporary Access Key or JWT Token',
|
||||
configuration: 'Configuration',
|
||||
},
|
||||
custom_tools: {
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
hero: {
|
||||
badge: 'GUI Agent in your webpage',
|
||||
title_line1: 'An AI Operator',
|
||||
title_line2: 'Living in Your Web Apps',
|
||||
title_line1: 'The AI Operator',
|
||||
title_line2: 'Living in Your Web App',
|
||||
subtitle_emoji: '🪄 One line of CDN',
|
||||
subtitle_main: ' adds intelligent GUI Agents to your website.',
|
||||
subtitle_detail: 'Users give natural language commands, AI handles the rest.',
|
||||
@@ -10,7 +10,8 @@ export default {
|
||||
tab_other: '🌐 Try on Other Sites',
|
||||
input_placeholder: 'Describe what you want AI to do...',
|
||||
execute_button: 'Run',
|
||||
default_task: 'Navigate to docs, find data masking section, and summarize in markdown',
|
||||
default_task:
|
||||
'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown',
|
||||
},
|
||||
try_other: {
|
||||
step1_title: 'Step 1:',
|
||||
@@ -31,16 +32,16 @@ export default {
|
||||
},
|
||||
},
|
||||
benefits: {
|
||||
no_backend: 'Zero Backend',
|
||||
no_backend: 'Pure Front-end Solution',
|
||||
private_model: 'Your Own Models',
|
||||
data_masking: 'Built-in Privacy',
|
||||
open_source: 'MIT Open Source',
|
||||
},
|
||||
features: {
|
||||
section_title: 'Why PageAgent',
|
||||
dom_understanding: {
|
||||
title: 'Smart DOM Analysis',
|
||||
desc: 'Analyzes page structure without screenshots or vision models. Pure text understanding for fast, accurate automation.',
|
||||
in_page: {
|
||||
title: 'In-page Solution',
|
||||
desc: 'Runs entirely within your page. No browser extensions, no headless browsers, and no backend required.',
|
||||
},
|
||||
secure_integration: {
|
||||
title: 'Secure by Design',
|
||||
@@ -48,7 +49,7 @@ export default {
|
||||
},
|
||||
zero_backend: {
|
||||
title: 'Zero Backend Setup',
|
||||
desc: 'Just drop in a script tag. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
|
||||
desc: 'Just drop in a script. Works with any LLM provider—OpenAI, Anthropic, or your own models.',
|
||||
},
|
||||
accessible: {
|
||||
title: 'Natural Language UI',
|
||||
+1
-1
@@ -4,7 +4,7 @@ export default {
|
||||
slogan: 'GUI Agent in your webpage',
|
||||
nav_docs: '文档',
|
||||
nav_source: 'GitHub',
|
||||
mobile_menu: '打开导航',
|
||||
mobile_menu: '打开导航栏',
|
||||
},
|
||||
footer: {
|
||||
copyright: '© 2025 page-agent. All rights reserved.',
|
||||
+20
-10
@@ -1,10 +1,11 @@
|
||||
export default {
|
||||
overview: {
|
||||
title: 'Overview',
|
||||
subtitle: 'page-agent 是一个完全基于Web技术的 GUI Agent,简单引入,让你的网站拥有 AI 操作员。',
|
||||
subtitle:
|
||||
'page-agent 是一个完全基于 Web 技术的 GUI Agent,简单几步,让你的网站拥有 AI 操作员。',
|
||||
what_is: '什么是 page-agent?',
|
||||
what_is_desc:
|
||||
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或通用Agent开发者,将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
|
||||
'page-agent 是一个页面内嵌式 GUI Agent。与传统的浏览器自动化工具不同,page-agent 面向网站开发者,而非爬虫或Agent客户端开发者;将 Agent 集成到你的网站中,让用户可以通过自然语言与页面进行交互。',
|
||||
features_title: '核心特性',
|
||||
feature_dom: {
|
||||
title: '🧠 智能 DOM 理解',
|
||||
@@ -112,7 +113,7 @@ export default {
|
||||
},
|
||||
model_integration: {
|
||||
title: '模型接入',
|
||||
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
|
||||
subtitle: '当前支持符合 OpenAI 接口规范且支持 tool call 的模型,包括公有云服务和私有部署方案。',
|
||||
recommended: '推荐模型',
|
||||
model_gpt4_title: '⚡ gpt-4.1-mini',
|
||||
model_gpt4_badge: '评估基准 ✅',
|
||||
@@ -122,22 +123,31 @@ export default {
|
||||
model_deepseek_title: '💰 DeepSeek-3.2',
|
||||
model_deepseek_badge: '经济实惠',
|
||||
model_deepseek_1: '• 价格远低于同等级其他模型',
|
||||
model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_deepseek_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_deepseek_3: '• 本网站提供的免费试用为 DeepSeek',
|
||||
model_qwen_title: '🛡️ qwen3',
|
||||
model_qwen_badge: '安全合规',
|
||||
model_qwen_1: '• 可控、效果尚可,价格合理',
|
||||
model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_qwen_1: '• 可控、效果尚可,价格合理',
|
||||
model_qwen_2: '• ToolCall 有出错率,通常能够自动修复',
|
||||
model_qwen_3: '• 适合能给出详细步骤的场景',
|
||||
model_gemini_title: '⚡ gemini-2.5-flash',
|
||||
model_gemini_badge: '极其高效,成功率高,价格合理',
|
||||
model_gemini_badge: '极其高效,成功率高,价格合理',
|
||||
available: '可用模型',
|
||||
available_verified: '✅ 已验证可用',
|
||||
tips: '提示',
|
||||
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
|
||||
tip_1: 'reasoning 模型(如 GPT-5),速度偏慢,没有必要',
|
||||
tip_2:
|
||||
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
|
||||
tip_3: '小模型、nano 模型,效果不佳',
|
||||
'不保证 json schema 的模型(openAI 以外的几乎所有模型),tool call 有概率出错,通常能自动修复,建议 temperature 设置高一些',
|
||||
tip_3: '小模型、nano 模型,效果不佳',
|
||||
security: '🔐 生产环境鉴权建议',
|
||||
security_warning: '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码库',
|
||||
security_desc: '在实际应用中,为了隐藏真实的 LLM API Key,建议采用以下架构:',
|
||||
security_backend_proxy: '后端代理转发',
|
||||
security_backend_desc:
|
||||
'在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:',
|
||||
security_method_1: '• Session/Cookie 会话认证',
|
||||
security_method_2: '• OIDC (OpenID Connect) 单点登录',
|
||||
security_method_3: '• 临时 Access Key 或 JWT Token',
|
||||
configuration: '配置方式',
|
||||
},
|
||||
custom_tools: {
|
||||
@@ -4,13 +4,13 @@ export default {
|
||||
title_line1: '让你的 Web 应用',
|
||||
title_line2: '拥有 AI 操作员',
|
||||
subtitle_emoji: '🪄 一行 CDN 引入',
|
||||
subtitle_main: ',为你的网站添加智能 UI Agent。',
|
||||
subtitle_main: ',为你的网站添加 GUI Agent。',
|
||||
subtitle_detail: '用户/答疑机器人给出文字指示,AI 帮你操作页面。',
|
||||
tab_try: '🚀 立即尝试',
|
||||
tab_other: '🌐 其他网页尝试',
|
||||
input_placeholder: '输入您想要 AI 执行的任务...',
|
||||
execute_button: '执行',
|
||||
default_task: '导航栏中进入文档页,打开数据脱敏相关的文档,帮我总结成 markdown',
|
||||
default_task: '从导航栏中进入文档页,打开“快速开始”相关的文档,帮我总结成 markdown',
|
||||
},
|
||||
try_other: {
|
||||
step1_title: '步骤 1:',
|
||||
@@ -31,16 +31,16 @@ export default {
|
||||
},
|
||||
},
|
||||
benefits: {
|
||||
no_backend: '无需后端',
|
||||
no_backend: '纯前端方案',
|
||||
private_model: '支持私有模型',
|
||||
data_masking: '无痛脱敏',
|
||||
open_source: 'MIT 开源',
|
||||
},
|
||||
features: {
|
||||
section_title: '核心特性',
|
||||
dom_understanding: {
|
||||
title: '智能 DOM 理解',
|
||||
desc: '基于 DOM 分析,高强度脱水。无需视觉识别,纯文本实现精准操作。',
|
||||
in_page: {
|
||||
title: '纯页面内方案',
|
||||
desc: '完全运行在你的页面内。不需要浏览器插件、不需要无头浏览器,不需要后端。',
|
||||
},
|
||||
secure_integration: {
|
||||
title: '安全可控集成',
|
||||
@@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
zero_backend: {
|
||||
title: '零后端部署',
|
||||
desc: 'CDN 直接引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。',
|
||||
desc: '前端脚本引入,自定义 LLM 接入点。从 OpenAI 到 qwen3,完全由你掌控。',
|
||||
},
|
||||
accessible: {
|
||||
title: '普惠智能交互',
|
||||
@@ -192,3 +192,13 @@ td {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
} */
|
||||
|
||||
/* 隐藏滚动条,但保持滚动功能 */
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user