Compare commits

...

41 Commits

Author SHA1 Message Date
Simon c0ce6e7d14 chore(version): bump version to 0.2.0 2026-01-15 20:47:39 +08:00
Simon 8f5f98f889 feat(agent)!: rename brain to reflection; fix undefined lines in panel 2026-01-15 20:45:40 +08:00
Simon ee4719cdcf feat: observation 2026-01-15 20:28:39 +08:00
Simon b4c82b7833 refactor: mv getBrowserState to Controller; simplify Agent 2026-01-15 20:17:50 +08:00
Simon aefc3cfb89 Merge pull request #111 from alibaba/feat/extend-agent-history-for-observation
feat: extend history type
2026-01-15 19:57:41 +08:00
Simon 7f2b8c2766 feat: extend history type 2026-01-15 19:18:25 +08:00
Simon 62e24a3050 chore(version): bump version to 0.1.0 2026-01-14 18:50:06 +08:00
Simon 4f6249a252 feat!: require user to provide LLM api. do not fallback to demo LLM
BREAKING CHANGE: LLM API will be required for agent constructor
2026-01-14 18:49:23 +08:00
Simon 62519416bb chore(version): bump version to 0.0.24 2026-01-14 18:34:55 +08:00
Simon a1e70e663d feat(PageController): exclude aria-hidden elements 2026-01-14 18:04:31 +08:00
Simon 338e317be1 chore(website): fix homepage overflow 2026-01-14 17:45:49 +08:00
Simon 9a66d5205f feat(website): display version in header and load from page-agent package 2026-01-14 17:42:18 +08:00
Simon 7d45bf6598 feat(website): update CDN links and usage 2026-01-14 17:27:35 +08:00
Simon 78ee72eea4 chore(version): bump version to 0.0.23 2026-01-14 15:46:10 +08:00
Simon 3b71c535b1 fix: clean up debug code 2026-01-14 15:23:02 +08:00
Simon 6615f7b300 Merge pull request #110 from alibaba/fix/panel-text-uncompleted
feat: add HTML escaping to Panel
2026-01-14 15:17:43 +08:00
Simon 38caae5e23 feat: add HTML escaping to Panel 2026-01-14 15:15:51 +08:00
Simon 1751ba325d chore(version): bump version to 0.0.22 2026-01-13 21:01:16 +08:00
Simon 066b045822 Merge pull request #108 from alibaba/feat/dual-umd
feat: add `cdn` packages for iife builds
2026-01-13 20:58:37 +08:00
Simon 3b21c42cdb feat: add cdn packages for iife builds 2026-01-13 20:52:41 +08:00
Simon 8b1e91d665 Merge pull request #107 from alibaba/refactor/mv-simulator-mask-to-page-controller
Refactor: move simulator mask to page controller
2026-01-13 17:38:37 +08:00
Simon aa5909264a chore: mv mask code to folder 2026-01-13 17:35:24 +08:00
Simon b6232d4e21 refactor: mv SimulatorMask from ui to page-controller 2026-01-13 17:33:38 +08:00
Simon 0d3cef5578 chore(version): bump version to 0.0.21 2026-01-13 15:18:50 +08:00
Simon 1e737f3a71 docs: models recommendation 2026-01-13 15:17:43 +08:00
Simon 2f2bdb4697 fix: update Qwen patch condition for model name 2026-01-13 15:07:01 +08:00
Simon 3fa7c8b85a feat: improve qwen3 compatibility (#106)
* feat: improve qwen3 compatibility

* fix: qwen

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 15:04:03 +08:00
dependabot[bot] 7efce91a91 chore(deps-dev): bump the development-dependencies group with 11 updates (#101)
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.3.0` | `20.3.1` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.3.0` | `20.3.1` |
| [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) | `6.0.1` | `6.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.6` |
| [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-dom) | `2.5.1` | `2.5.5` |
| [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/HEAD/packages/plugins/eslint-plugin-react-x) | `2.5.1` | `2.5.5` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.52.0` | `8.53.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` |
| [i18next](https://github.com/i18next/i18next) | `25.7.3` | `25.7.4` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.1` | `16.5.2` |


Updates `@commitlint/cli` from 20.3.0 to 20.3.1
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.3.0 to 20.3.1
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/config-conventional)

Updates `@trivago/prettier-plugin-sort-imports` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/main/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/compare/v6.0.1...v6.0.2)

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

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

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

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

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

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

Updates `i18next` from 25.7.3 to 25.7.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.7.3...v25.7.4)

Updates `react-i18next` from 16.5.1 to 16.5.2
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v16.5.1...v16.5.2)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-dom
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint-plugin-react-x
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: i18next
  dependency-version: 25.7.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:30:02 +08:00
dependabot[bot] a11d370f2b chore(deps): bump the production-dependencies group with 3 updates (#100)
Bumps the production-dependencies group with 3 updates: [ai-motion](https://github.com/gaomeng1900/ai-motion), [motion](https://github.com/motiondivision/motion) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `ai-motion` from 0.4.7 to 0.4.8
- [Release notes](https://github.com/gaomeng1900/ai-motion/releases)
- [Commits](https://github.com/gaomeng1900/ai-motion/compare/v0.4.7...v0.4.8)

Updates `motion` from 12.24.0 to 12.26.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.24.0...v12.26.1)

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

---
updated-dependencies:
- dependency-name: ai-motion
  dependency-version: 0.4.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
- dependency-name: motion
  dependency-version: 12.26.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
- dependency-name: simple-icons
  dependency-version: 16.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:27:23 +08:00
dependabot[bot] d7bfa42f85 chore(deps-dev): bump globals from 16.5.0 to 17.0.0 (#102)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 14:26:37 +08:00
Simon 551ba49280 Merge pull request #104 from alibaba/refactor/mv-brain-outof-llms
refactor: move agent logic out of `llms`
2026-01-13 14:24:29 +08:00
Simon adab24bb22 Merge branch 'refactor/mv-brain-outof-llms' of https://github.com/alibaba/page-agent into refactor/mv-brain-outof-llms 2026-01-13 14:22:48 +08:00
Simon 64aea7e84c chore: typing 2026-01-13 14:21:57 +08:00
Simon 67f6bd619e chore: rename autoFixer for better understanding 2026-01-13 14:20:52 +08:00
Simon 35d1fd1166 chore: better erroring
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:16:51 +08:00
Simon d5b8019fb1 chore: better docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-13 14:14:56 +08:00
Simon 526cb4dbb3 chore: rename OpenAIClient; clean up old notes 2026-01-13 13:58:21 +08:00
Simon 6dc56c57c6 chore: rm legacy llm client 2026-01-13 13:56:30 +08:00
Simon a3263fdd3a chore: rename OpenAICompatibleClient 2026-01-13 13:51:09 +08:00
Simon 14974c0257 feat!: mv brain from llms to agent; redo toolCall auto fixer 2026-01-13 13:49:19 +08:00
Simon e70ae40096 chore: guardrail 2026-01-12 13:55:39 +08:00
49 changed files with 1395 additions and 1122 deletions
+30 -25
View File
@@ -9,9 +9,10 @@ This is a **monorepo** with npm workspaces:
Internal packages: Internal packages:
- **CDN** (`packages/cdn/`) - IIFE builds for script tag usage (npm: `@page-agent/cdn`)
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model - **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
- **Page Controller** (`packages/page-controller/`) - DOM operations, independent of LLM - **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback (SimulatorMask), independent of LLM
- **UI** (`packages/ui/`) - Panel, SimulatorMask, i18n. Decoupled from PageAgent - **UI** (`packages/ui/`) - Panel and i18n. Decoupled from PageAgent
## Development Commands ## Development Commands
@@ -31,6 +32,7 @@ Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig
``` ```
packages/ packages/
├── page-agent/ # npm: "page-agent" ⭐ MAIN ├── page-agent/ # npm: "page-agent" ⭐ MAIN
├── cdn/ # npm: "@page-agent/cdn" (IIFE builds)
├── website/ # @page-agent/website (private) ├── website/ # @page-agent/website (private)
├── llms/ # @page-agent/llms ├── llms/ # @page-agent/llms
├── page-controller/ # @page-agent/page-controller ├── page-controller/ # @page-agent/page-controller
@@ -43,8 +45,8 @@ packages/
- **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui` - **Page Agent**: Core lib. Imports from `@page-agent/llms`, `@page-agent/page-controller`, `@page-agent/ui`
- **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent - **LLMs**: LLM client with MacroToolInput contract. No dependency on page-agent
- **UI**: Panel, Mask, i18n. No dependency on page-agent - **UI**: Panel and i18n. No dependency on page-agent
- **Page Controller**: Pure DOM operations. No LLM or UI dependency - **Page Controller**: DOM operations with optional visual feedback (SimulatorMask). No LLM dependency. Enable mask via `enableMask: true` config
### PageController ↔ PageAgent Communication ### PageController ↔ PageAgent Communication
@@ -69,41 +71,43 @@ const pageInfo = await this.pageController.getPageInfo()
3. **LLM Processing**: AI returns action plans (page-agent) 3. **LLM Processing**: AI returns action plans (page-agent)
4. **Indexed Operations**: PageAgent calls PageController by element index 4. **Indexed Operations**: PageAgent calls PageController by element index
### CDN Auto-Injection ### CDN Builds (`packages/cdn/`)
Library auto-initializes via script tag: Two IIFE builds for script tag usage:
```html | Build | File | Description |
<script src="page-agent.js?model=gpt-4"></script> | ----- | -------------------- | -------------------------------- |
``` | Demo | `page-agent.demo.js` | Auto-init with built-in test API |
| Full | `page-agent.js` | Exposes `PageAgent` class only |
Query params configure `PageAgentConfig` in `src/umd.ts`. Demo build supports query params (e.g., `?model=gpt-4&lang=en-US`).
## Key Files Reference ## Key Files Reference
### Page Agent (`packages/page-agent/`) ### Page Agent (`packages/page-agent/`)
| File | Description | | File | Description |
|------|-------------| | ------------------ | --------------------------------------- |
| `src/PageAgent.ts` | ⭐ Main AI agent class | | `src/PageAgent.ts` | ⭐ Main AI agent class |
| `src/umd.ts` | CDN/UMD entry with auto-init | | `src/umd.ts` | CDN/UMD entry with auto-init |
| `src/tools/` | Tool definitions calling PageController | | `src/tools/` | Tool definitions calling PageController |
### LLMs (`packages/llms/`) ### LLMs (`packages/llms/`)
| File | Description | | File | Description |
|------|-------------| | ---------------------------- | ------------------------------------- |
| `src/index.ts` | ⭐ LLM class with retry logic | | `src/index.ts` | ⭐ LLM class with retry logic |
| `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig | | `src/types.ts` | MacroToolInput, AgentBrain, LLMConfig |
| `src/OpenAILenientClient.ts` | OpenAI-compatible client | | `src/OpenAILenientClient.ts` | OpenAI-compatible client |
### Page Controller (`packages/page-controller/`) ### Page Controller (`packages/page-controller/`)
| File | Description | | File | Description |
|------|-------------| | --------------------------- | ---------------------------------------------------------- |
| `src/PageController.ts` | ⭐ Main controller class | | `src/PageController.ts` | ⭐ Main controller class with optional mask support |
| `src/actions.ts` | Element interactions (click, input, scroll) | | `src/SimulatorMask.ts` | Visual overlay blocking user interaction during automation |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine | | `src/actions.ts` | Element interactions (click, input, scroll) |
| `src/dom/dom_tree/index.js` | Core DOM extraction engine |
## Adding New Features ## Adding New Features
@@ -123,4 +127,5 @@ Query params configure `PageAgentConfig` in `src/umd.ts`.
- Explicit typing for exported/public APIs - Explicit typing for exported/public APIs
- ESLint relaxes some unsafe rules for rapid iteration - ESLint relaxes some unsafe rules for rapid iteration
- Every change you make should not only implement the desired functionality but also improve the quality of the codebase
- All code and comments must be in English. - All code and comments must be in English.
+38 -25
View File
@@ -28,7 +28,22 @@
## 🚀 快速开始 ## 🚀 快速开始
### NPM 安装 ### 快速体验 (Demo CDN)
最快的体验方式:
```html
<script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js" crossorigin="true"></script>
```
> ⚠️ **仅用于技术评估。** Demo 模型有速率限制和使用限制,生产环境请使用 NPM 方式。
| 位置 | URL |
| ------ | --------------------------------------------------------------------------------------- |
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
### NPM 安装(推荐)
```bash ```bash
npm install page-agent npm install page-agent
@@ -37,40 +52,37 @@ npm install page-agent
```javascript ```javascript
import { PageAgent } from 'page-agent' import { PageAgent } from 'page-agent'
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({ const agent = new PageAgent({
model: DEMO_MODEL, model: 'deepseek-chat',
baseURL: DEMO_BASE_URL, baseURL: 'https://api.deepseek.com',
apiKey: DEMO_API_KEY, apiKey: 'YOUR_API_KEY',
language: 'zh-CN', language: 'zh-CN',
}) })
await agent.execute('点击登录按钮') await agent.execute('点击登录按钮')
``` ```
### CDN 集成 ### CDN 引入
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default. 适用于无法使用 NPM 的环境,用法与 NPM 一致:
| Location | URL |
| -------- | ----------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
```html ```html
<script <script src="https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js" crossorigin="true"></script>
src="https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js" <script>
crossorigin="true" const agent = new PageAgent({
type="text/javascript" model: 'deepseek-chat',
></script> baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
})
await agent.execute('点击登录按钮')
</script>
``` ```
| 位置 | URL |
| ------ | ----------------------------------------------------------------------------------- |
| 全球 | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
| 中国 | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
## 🏗️ 架构设计 ## 🏗️ 架构设计
PageAgent adopts a simplified monorepo structure: PageAgent adopts a simplified monorepo structure:
@@ -79,8 +91,9 @@ PageAgent adopts a simplified monorepo structure:
packages/ packages/
├── page-agent/ # AI agent (npm: page-agent) ├── page-agent/ # AI agent (npm: page-agent)
├── llms/ # LLM 客户端 (npm: @page-agent/llms) ├── llms/ # LLM 客户端 (npm: @page-agent/llms)
├── page-controller/ # DOM 操作 (npm: @page-agent/page-controller) ├── page-controller/ # DOM 操作 & 蒙层 & 模拟鼠标 (npm: @page-agent/page-controller)
├── ui/ # 面板 & 蒙层 & 模拟鼠标 (npm: @page-agent/ui) ├── ui/ # 面板 & i18n (npm: @page-agent/ui)
├── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── website/ # 文档站点 └── website/ # 文档站点
``` ```
+38 -25
View File
@@ -28,7 +28,22 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
## 🚀 Quick Start ## 🚀 Quick Start
### NPM Installation ### Quick Try (Demo CDN)
Fastest way to try PageAgent:
```html
<script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js" crossorigin="true"></script>
```
> ⚠️ **For technical evaluation only.** Demo model has rate limits and usage restrictions. Use NPM for production.
| Location | URL |
| -------- | --------------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js |
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js |
### NPM Installation (Recommended)
```bash ```bash
npm install page-agent npm install page-agent
@@ -37,40 +52,37 @@ npm install page-agent
```javascript ```javascript
import { PageAgent } from 'page-agent' import { PageAgent } from 'page-agent'
// test server
// @note: rate limit. prompt limit. Origin limit. May change anytime. Use your own llm!
// @note Using official DeepSeek-chat(3.2). Go to DeepSeek website for privacy policy.
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({ const agent = new PageAgent({
model: DEMO_MODEL, model: 'deepseek-chat',
baseURL: DEMO_BASE_URL, baseURL: 'https://api.deepseek.com',
apiKey: DEMO_API_KEY, apiKey: 'YOUR_API_KEY',
language: 'en-US', language: 'en-US',
}) })
await agent.execute('Click the login button') await agent.execute('Click the login button')
``` ```
### CDN Integration ### CDN Build
Fastest way to try PageAgent is to include it via CDN. Demo model will be used by default. For environments where NPM is not available. Usage is identical to NPM:
| Location | URL |
| -------- | ----------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js |
| China | https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js |
```html ```html
<script <script src="https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js" crossorigin="true"></script>
src="https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js" <script>
crossorigin="true" const agent = new PageAgent({
type="text/javascript" model: 'deepseek-chat',
></script> baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
})
await agent.execute('Click the login button')
</script>
``` ```
| Location | URL |
| -------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js |
| China | https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js |
## 🏗️ Structure ## 🏗️ Structure
PageAgent adopts a simplified monorepo structure: PageAgent adopts a simplified monorepo structure:
@@ -79,8 +91,9 @@ PageAgent adopts a simplified monorepo structure:
packages/ packages/
├── page-agent/ # AI agent (npm: page-agent) ├── page-agent/ # AI agent (npm: page-agent)
├── llms/ # LLM client (npm: @page-agent/llms) ├── llms/ # LLM client (npm: @page-agent/llms)
├── page-controller/ # DOM operations (npm: @page-agent/page-controller) ├── page-controller/ # DOM operations & Visual Mask (npm: @page-agent/page-controller)
├── ui/ # Panel & Mask & Mouse Animation (npm: @page-agent/ui) ├── ui/ # Panel & i18n (npm: @page-agent/ui)
├── cdn/ # CDN IIFE builds (npm: @page-agent/cdn)
└── website/ # Demo & Documentation site └── website/ # Demo & Documentation site
``` ```
+264 -251
View File
@@ -1,43 +1,44 @@
{ {
"name": "root", "name": "root",
"version": "0.0.20", "version": "0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "root", "name": "root",
"version": "0.0.20", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
"packages/ui", "packages/ui",
"packages/llms", "packages/llms",
"packages/page-agent", "packages/page-agent",
"packages/cdn",
"packages/website" "packages/website"
], ],
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.3.0", "@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.0", "@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2", "@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.3", "@types/node": "^25.0.7",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.5.1", "eslint-plugin-react-dom": "^2.5.7",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.5.1", "eslint-plugin-react-x": "^2.5.7",
"globals": "^16.4.0", "globals": "^17.0.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.4", "lint-staged": "^16.2.4",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.52.0", "typescript-eslint": "^8.53.0",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.0", "vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2" "vite-plugin-css-injected-by-js": "^3.5.2"
}, },
"engines": { "engines": {
@@ -316,17 +317,17 @@
} }
}, },
"node_modules/@commitlint/cli": { "node_modules/@commitlint/cli": {
"version": "20.3.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.3.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.3.1.tgz",
"integrity": "sha512-HXO8YVfqdBK+MnlX2zqNrv6waGYPs6Ysjm5W2Y0GMagWXwiIKx7C8dcIX9ca+QdHq4WA0lcMnZLQ0pzQh1piZg==", "integrity": "sha512-NtInjSlyev/+SLPvx/ulz8hRE25Wf5S9dLNDcIwazq0JyB4/w1ROF/5nV0ObPTX8YpRaKYeKtXDYWqumBNHWsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/format": "^20.2.0", "@commitlint/format": "^20.3.1",
"@commitlint/lint": "^20.3.0", "@commitlint/lint": "^20.3.1",
"@commitlint/load": "^20.3.0", "@commitlint/load": "^20.3.1",
"@commitlint/read": "^20.2.0", "@commitlint/read": "^20.3.1",
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"tinyexec": "^1.0.0", "tinyexec": "^1.0.0",
"yargs": "^17.0.0" "yargs": "^17.0.0"
}, },
@@ -338,13 +339,13 @@
} }
}, },
"node_modules/@commitlint/config-conventional": { "node_modules/@commitlint/config-conventional": {
"version": "20.3.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.3.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.3.1.tgz",
"integrity": "sha512-g1OXVl6E2v0xF1Ru2RpxQ+Vfy7XUcUsCmLKzGUrhFLS4hSNykje0QSy6djBtzOiOBQCepBrmIlqx/gRlzrSh5A==", "integrity": "sha512-NCzwvxepstBZbmVXsvg49s+shCxlJDJPWxXqONVcAtJH9wWrOlkMQw/zyl+dJmt8lyVopt5mwQ3mR5M2N2rUWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"conventional-changelog-conventionalcommits": "^7.0.2" "conventional-changelog-conventionalcommits": "^7.0.2"
}, },
"engines": { "engines": {
@@ -352,13 +353,13 @@
} }
}, },
"node_modules/@commitlint/config-validator": { "node_modules/@commitlint/config-validator": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.3.1.tgz",
"integrity": "sha512-SQCBGsL9MFk8utWNSthdxd9iOD1pIVZSHxGBwYIGfd67RTjxqzFOSAYeQVXOu3IxRC3YrTOH37ThnTLjUlyF2w==", "integrity": "sha512-ErVLC/IsHhcvxCyh+FXo7jy12/nkQySjWXYgCoQbZLkFp4hysov8KS6CdxBB0cWjbZWjvNOKBMNoUVqkmGmahw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"ajv": "^8.11.0" "ajv": "^8.11.0"
}, },
"engines": { "engines": {
@@ -366,13 +367,13 @@
} }
}, },
"node_modules/@commitlint/ensure": { "node_modules/@commitlint/ensure": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.3.1.tgz",
"integrity": "sha512-+8TgIGv89rOWyt3eC6lcR1H7hqChAKkpawytlq9P1i/HYugFRVqgoKJ8dhd89fMnlrQTLjA5E97/4sF09QwdoA==", "integrity": "sha512-h664FngOEd7bHAm0j8MEKq+qm2mH+V+hwJiIE2bWcw3pzJMlO0TPKtk0ATyRAtV6jQw+xviRYiIjjSjfajiB5w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1", "lodash.kebabcase": "^4.1.1",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
@@ -394,13 +395,13 @@
} }
}, },
"node_modules/@commitlint/format": { "node_modules/@commitlint/format": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.3.1.tgz",
"integrity": "sha512-PhNoLNhxpfIBlW/i90uZ3yG3hwSSYx7n4d9Yc+2FAorAHS0D9btYRK4ZZXX+Gm3W5tDtu911ow/eWRfcRVgNWg==", "integrity": "sha512-jfsjGPFTd2Yti2YHwUH4SPRPbWKAJAwrfa3eNa9bXEdrXBb9mCwbIrgYX38LdEJK9zLJ3AsLBP4/FLEtxyu2AA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"chalk": "^5.3.0" "chalk": "^5.3.0"
}, },
"engines": { "engines": {
@@ -408,13 +409,13 @@
} }
}, },
"node_modules/@commitlint/is-ignored": { "node_modules/@commitlint/is-ignored": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.3.1.tgz",
"integrity": "sha512-Lz0OGeZCo/QHUDLx5LmZc0EocwanneYJUM8z0bfWexArk62HKMLfLIodwXuKTO5y0s6ddXaTexrYHs7v96EOmw==", "integrity": "sha512-tWwAoh93QvAhxgp99CzCuHD86MgxE4NBtloKX+XxQxhfhSwHo7eloiar/yzx53YW9eqSLP95zgW2KDDk4/WX+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"semver": "^7.6.0" "semver": "^7.6.0"
}, },
"engines": { "engines": {
@@ -422,32 +423,32 @@
} }
}, },
"node_modules/@commitlint/lint": { "node_modules/@commitlint/lint": {
"version": "20.3.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.3.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.3.1.tgz",
"integrity": "sha512-X19HOGU5nRo6i9DIY0kG0mhgtvpn1UGO1D6aLX1ILLyeqSM5yJyMcrRqNj8SLgeSeUDODhLY9QYsBIG0LdNHkA==", "integrity": "sha512-LaOtrQ24+6SfUaWg8A+a+Wc77bvLbO5RIr6iy9F7CI3/0iq1uPEWgGRCwqWTuLGHkZDAcwaq0gZ01zpwZ1jCGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/is-ignored": "^20.2.0", "@commitlint/is-ignored": "^20.3.1",
"@commitlint/parse": "^20.2.0", "@commitlint/parse": "^20.3.1",
"@commitlint/rules": "^20.3.0", "@commitlint/rules": "^20.3.1",
"@commitlint/types": "^20.2.0" "@commitlint/types": "^20.3.1"
}, },
"engines": { "engines": {
"node": ">=v18" "node": ">=v18"
} }
}, },
"node_modules/@commitlint/load": { "node_modules/@commitlint/load": {
"version": "20.3.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.3.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.3.1.tgz",
"integrity": "sha512-amkdVZTXp5R65bsRXRSCwoNXbJHR2aAIY/RGFkoyd63t8UEwqEgT3f0MgeLqYw4hwXyq+TYXKdaW133E29pnGQ==", "integrity": "sha512-YDD9XA2XhgYgbjju8itZ/weIvOOobApDqwlPYCX5NLO/cPtw2UMO5Cmn44Ks8RQULUVI5fUT6roKvyxcoLbNmw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/config-validator": "^20.2.0", "@commitlint/config-validator": "^20.3.1",
"@commitlint/execute-rule": "^20.0.0", "@commitlint/execute-rule": "^20.0.0",
"@commitlint/resolve-extends": "^20.2.0", "@commitlint/resolve-extends": "^20.3.1",
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"cosmiconfig-typescript-loader": "^6.1.0", "cosmiconfig-typescript-loader": "^6.1.0",
@@ -470,13 +471,13 @@
} }
}, },
"node_modules/@commitlint/parse": { "node_modules/@commitlint/parse": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.3.1.tgz",
"integrity": "sha512-LXStagGU1ivh07X7sM+hnEr4BvzFYn1iBJ6DRg2QsIN8lBfSzyvkUcVCDwok9Ia4PWiEgei5HQjju6xfJ1YaSQ==", "integrity": "sha512-TuUTdbLpyUNLgDzLDYlI2BeTE6V/COZbf3f8WwsV0K6eq/2nSpNTMw7wHtXb+YxeY9wwxBp/Ldad4P+YIxHJoA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"conventional-changelog-angular": "^7.0.0", "conventional-changelog-angular": "^7.0.0",
"conventional-commits-parser": "^5.0.0" "conventional-commits-parser": "^5.0.0"
}, },
@@ -485,14 +486,14 @@
} }
}, },
"node_modules/@commitlint/read": { "node_modules/@commitlint/read": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.3.1.tgz",
"integrity": "sha512-+SjF9mxm5JCbe+8grOpXCXMMRzAnE0WWijhhtasdrpJoAFJYd5UgRTj/oCq5W3HJTwbvTOsijEJ0SUGImECD7Q==", "integrity": "sha512-nCmJAdIg3OdNVUpQW0Idk/eF/vfOo2W2xzmvRmNeptLrzFK7qhwwl/kIwy1Q1LZrKHUFNj7PGNpIT5INbgZWzA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/top-level": "^20.0.0", "@commitlint/top-level": "^20.0.0",
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"git-raw-commits": "^4.0.0", "git-raw-commits": "^4.0.0",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"tinyexec": "^1.0.0" "tinyexec": "^1.0.0"
@@ -502,14 +503,14 @@
} }
}, },
"node_modules/@commitlint/resolve-extends": { "node_modules/@commitlint/resolve-extends": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.3.1.tgz",
"integrity": "sha512-KVoLDi9BEuqeq+G0wRABn4azLRiCC22/YHR2aCquwx6bzCHAIN8hMt3Nuf1VFxq/c8ai6s8qBxE8+ZD4HeFTlQ==", "integrity": "sha512-iGTGeyaoDyHDEZNjD8rKeosjSNs8zYanmuowY4ful7kFI0dnY4b5QilVYaFQJ6IM27S57LAeH5sKSsOHy4bw5w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/config-validator": "^20.2.0", "@commitlint/config-validator": "^20.3.1",
"@commitlint/types": "^20.2.0", "@commitlint/types": "^20.3.1",
"global-directory": "^4.0.1", "global-directory": "^4.0.1",
"import-meta-resolve": "^4.0.0", "import-meta-resolve": "^4.0.0",
"lodash.mergewith": "^4.6.2", "lodash.mergewith": "^4.6.2",
@@ -520,16 +521,16 @@
} }
}, },
"node_modules/@commitlint/rules": { "node_modules/@commitlint/rules": {
"version": "20.3.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.3.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.3.1.tgz",
"integrity": "sha512-TGgXN/qBEhbzVD13crE1l7YSMJRrbPbUL0OBZALbUM5ER36RZmiZRu2ud2W/AA7HO9YLBRbyx6YVi2t/2Be0yQ==", "integrity": "sha512-/uic4P+4jVNpqQxz02+Y6vvIC0A2J899DBztA1j6q3f3MOKwydlNrojSh0dQmGDxxT1bXByiRtDhgFnOFnM6Pg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@commitlint/ensure": "^20.2.0", "@commitlint/ensure": "^20.3.1",
"@commitlint/message": "^20.0.0", "@commitlint/message": "^20.0.0",
"@commitlint/to-lines": "^20.0.0", "@commitlint/to-lines": "^20.0.0",
"@commitlint/types": "^20.2.0" "@commitlint/types": "^20.3.1"
}, },
"engines": { "engines": {
"node": ">=v18" "node": ">=v18"
@@ -559,9 +560,9 @@
} }
}, },
"node_modules/@commitlint/types": { "node_modules/@commitlint/types": {
"version": "20.2.0", "version": "20.3.1",
"resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.2.0.tgz", "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.3.1.tgz",
"integrity": "sha512-KTy0OqRDLR5y/zZMnizyx09z/rPlPC/zKhYgH8o/q6PuAjoQAKlRfY4zzv0M64yybQ//6//4H1n14pxaLZfUnA==", "integrity": "sha512-VmIFV/JkBRhDRRv7N5B7zEUkNZIx9Mp+8Pe65erz0rKycXLsi8Epcw0XJ+btSeRXgTzE7DyOyA9bkJ9mn/yqVQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1057,16 +1058,16 @@
} }
}, },
"node_modules/@eslint-react/ast": { "node_modules/@eslint-react/ast": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-react/ast/-/ast-2.5.7.tgz",
"integrity": "sha512-l+pEb3kS/RhFN+8ZOhL5+P5+Yj7iw+ta7+abUBNH+5PMyiixlnAJlJ7Wy+rsTGxjUJtzEff3CzSr06V7qwJuwQ==", "integrity": "sha512-yQvKvyIzo9DPKFUOtFSEfpX0He763G1jFf/LQcIY9xoSOWP9pQ2X8ADNE6ZnmIc0L5zZLQ9nF/H0+MGh2NF3fA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@typescript-eslint/types": "^8.51.0", "@typescript-eslint/types": "^8.53.0",
"@typescript-eslint/typescript-estree": "^8.51.0", "@typescript-eslint/typescript-estree": "^8.53.0",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"string-ts": "^2.3.1" "string-ts": "^2.3.1"
}, },
"engines": { "engines": {
@@ -1078,19 +1079,19 @@
} }
}, },
"node_modules/@eslint-react/core": { "node_modules/@eslint-react/core": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-react/core/-/core-2.5.7.tgz",
"integrity": "sha512-48YQYhjHga0TAv7FI0uGGT+X3SITec89fJLKSTqIEa7jTLpXTV56B+B/V/IcfZ0s8I0lNVEmtHFTXaibAxmfyw==", "integrity": "sha512-LqY4YV4Z/uiymdy2OLXkjsEVnoDjoFsxJZI4GWsZ9aBgGWYAsuoKKUG0bdMLp1U9jwMaY0OY9bYgBB5R1ANkoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/ast": "2.5.1", "@eslint-react/ast": "2.5.7",
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@eslint-react/shared": "2.5.1", "@eslint-react/shared": "2.5.7",
"@eslint-react/var": "2.5.1", "@eslint-react/var": "2.5.7",
"@typescript-eslint/scope-manager": "^8.51.0", "@typescript-eslint/scope-manager": "^8.53.0",
"@typescript-eslint/types": "^8.51.0", "@typescript-eslint/types": "^8.53.0",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"birecord": "^0.1.1", "birecord": "^0.1.1",
"ts-pattern": "^5.9.0" "ts-pattern": "^5.9.0"
}, },
@@ -1103,9 +1104,9 @@
} }
}, },
"node_modules/@eslint-react/eff": { "node_modules/@eslint-react/eff": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-react/eff/-/eff-2.5.7.tgz",
"integrity": "sha512-henDz7sTB67Q+YPb+Kiy0UpD7eHLvDtB2nOrRtHLTatS/LFr7wVr4J2muQPkPzY9KFF8pv811nrwW58FuDphPg==", "integrity": "sha512-oxd8eP+dykyOUmQUEItnOZhitlKyo9yFq8Ne0k9+CJeV0Pk1db1+lNcDWswWYoeb1IFEloqHo6y63fVxQXvJew==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1113,14 +1114,14 @@
} }
}, },
"node_modules/@eslint-react/shared": { "node_modules/@eslint-react/shared": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-react/shared/-/shared-2.5.7.tgz",
"integrity": "sha512-JSTnpZdBu57ZvrJkBwUZXgkBW8iPiQfSiQMYHEjVFR2NHWFgcOd9qYJLX7mJLrdcw/j+GgRQeOAGok0a0xcxGA==", "integrity": "sha512-ZjKO3rNDoLN2ZTn3PxmMcgoASPNiL3umK1BHQvYwCXqqnXlLEouu7sxK6htOvDu1NC+Dbwsqpu4JGUY7t0XQTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"ts-pattern": "^5.9.0", "ts-pattern": "^5.9.0",
"zod": "^4.3.5" "zod": "^4.3.5"
}, },
@@ -1133,17 +1134,17 @@
} }
}, },
"node_modules/@eslint-react/var": { "node_modules/@eslint-react/var": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.5.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-react/var/-/var-2.5.7.tgz",
"integrity": "sha512-O3rbdK8pHKZ4NYPD39Zq7zhWu76dvhyeroqG8nQzf667F/FdHGpGhsCPJOeIU5L/ooYO/3/Qz2vJD1pB5JeROQ==", "integrity": "sha512-VWoyZENbK+OV70s/sE9E6T4HD0WO6lsg2CnYjjpKBKBDOBW67SRdIlM7dUPKzqQKsj6p4yNh59eQ5iKaH82LLg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/ast": "2.5.1", "@eslint-react/ast": "2.5.7",
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@typescript-eslint/scope-manager": "^8.51.0", "@typescript-eslint/scope-manager": "^8.53.0",
"@typescript-eslint/types": "^8.51.0", "@typescript-eslint/types": "^8.53.0",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"ts-pattern": "^5.9.0" "ts-pattern": "^5.9.0"
}, },
"engines": { "engines": {
@@ -1597,6 +1598,10 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/@page-agent/cdn": {
"resolved": "packages/cdn",
"link": true
},
"node_modules/@page-agent/llms": { "node_modules/@page-agent/llms": {
"resolved": "packages/llms", "resolved": "packages/llms",
"link": true "link": true
@@ -3458,9 +3463,9 @@
} }
}, },
"node_modules/@trivago/prettier-plugin-sort-imports": { "node_modules/@trivago/prettier-plugin-sort-imports": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz",
"integrity": "sha512-6B13DCWDfAfh4AEJ43gRgeCSAQmlKG5LHqHzHc0lbUwgBy0rX7o41US+46Fd4XiXBx+JDGEz3NBadCbUls0dUQ==", "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -3556,9 +3561,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.0.3", "version": "25.0.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.7.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3566,9 +3571,9 @@
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.7", "version": "19.2.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3586,17 +3591,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
"integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/type-utils": "8.53.0",
"@typescript-eslint/utils": "8.52.0", "@typescript-eslint/utils": "8.53.0",
"@typescript-eslint/visitor-keys": "8.52.0", "@typescript-eslint/visitor-keys": "8.53.0",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0" "ts-api-utils": "^2.4.0"
@@ -3609,7 +3614,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.52.0", "@typescript-eslint/parser": "^8.53.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
@@ -3625,16 +3630,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
"integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/typescript-estree": "8.53.0",
"@typescript-eslint/visitor-keys": "8.52.0", "@typescript-eslint/visitor-keys": "8.53.0",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@@ -3650,14 +3655,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
"integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/tsconfig-utils": "^8.53.0",
"@typescript-eslint/types": "^8.52.0", "@typescript-eslint/types": "^8.53.0",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@@ -3672,14 +3677,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
"integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"@typescript-eslint/visitor-keys": "8.52.0" "@typescript-eslint/visitor-keys": "8.53.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3690,9 +3695,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
"integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3707,15 +3712,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
"integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/typescript-estree": "8.53.0",
"@typescript-eslint/utils": "8.52.0", "@typescript-eslint/utils": "8.53.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"ts-api-utils": "^2.4.0" "ts-api-utils": "^2.4.0"
}, },
@@ -3732,9 +3737,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
"integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3746,16 +3751,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
"integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/project-service": "8.53.0",
"@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.53.0",
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"@typescript-eslint/visitor-keys": "8.52.0", "@typescript-eslint/visitor-keys": "8.53.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"minimatch": "^9.0.5", "minimatch": "^9.0.5",
"semver": "^7.7.3", "semver": "^7.7.3",
@@ -3800,16 +3805,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
"integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/scope-manager": "8.53.0",
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"@typescript-eslint/typescript-estree": "8.52.0" "@typescript-eslint/typescript-estree": "8.53.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3824,13 +3829,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
"integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.52.0", "@typescript-eslint/types": "8.53.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -3911,9 +3916,9 @@
} }
}, },
"node_modules/ai-motion": { "node_modules/ai-motion": {
"version": "0.4.7", "version": "0.4.8",
"resolved": "https://registry.npmjs.org/ai-motion/-/ai-motion-0.4.7.tgz", "resolved": "https://registry.npmjs.org/ai-motion/-/ai-motion-0.4.8.tgz",
"integrity": "sha512-yuFtVgWeaxKyvfSFocA01Mr4UbRvwwy7HWZ6Mq9NM/yccAkqp3UCGdWX6AflNB8cU27PgwTBiMRfx6C9di7xkQ==", "integrity": "sha512-9nQVjf8lrYQWSW0sZdb9bES4x8Ei1w9C0S62po2KvRbMjslmjm0WMeszyW771SEauDnu4YGWqurLZ/gCkCTosQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ajv": { "node_modules/ajv": {
@@ -4860,20 +4865,20 @@
} }
}, },
"node_modules/eslint-plugin-react-dom": { "node_modules/eslint-plugin-react-dom": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-2.5.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-dom/-/eslint-plugin-react-dom-2.5.7.tgz",
"integrity": "sha512-7x3k0bQwtTOSxjReH11buYL6aSMopBzSaWr2bU/LPOuy1dDxvqE6HC2Y6QPEyZjLJ1ppUSKwRmcX85uiF0jHAA==", "integrity": "sha512-KHCU7KRXeLYlrrXKtInpCqHzt+nYhIGpYIuMM4Q0FyGBOcswRS2Gv9PhN+yQqDI9PRBhnjCuRf+q2Y3ZrYWjsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/ast": "2.5.1", "@eslint-react/ast": "2.5.7",
"@eslint-react/core": "2.5.1", "@eslint-react/core": "2.5.7",
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@eslint-react/shared": "2.5.1", "@eslint-react/shared": "2.5.7",
"@eslint-react/var": "2.5.1", "@eslint-react/var": "2.5.7",
"@typescript-eslint/scope-manager": "^8.51.0", "@typescript-eslint/scope-manager": "^8.53.0",
"@typescript-eslint/types": "^8.51.0", "@typescript-eslint/types": "^8.53.0",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
"string-ts": "^2.3.1", "string-ts": "^2.3.1",
"ts-pattern": "^5.9.0" "ts-pattern": "^5.9.0"
@@ -4917,21 +4922,21 @@
} }
}, },
"node_modules/eslint-plugin-react-x": { "node_modules/eslint-plugin-react-x": {
"version": "2.5.1", "version": "2.5.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.5.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-x/-/eslint-plugin-react-x-2.5.7.tgz",
"integrity": "sha512-w3KtEG8M7sFRh/psUmep3H+i2O/5Nn9wI8GXjlqzwp8zLb4ax66Ue+1oecLTzY/0j/M1xRfB3hzk5oio54RwLA==", "integrity": "sha512-EU+h7eUHNg9ikLaGOa0Tjsc/o4FO2KdyT3M97TkQmaKc9tzpcy8nlPQp/czBMbUuxAgbj2495wzOvCZ0fEK4mw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-react/ast": "2.5.1", "@eslint-react/ast": "2.5.7",
"@eslint-react/core": "2.5.1", "@eslint-react/core": "2.5.7",
"@eslint-react/eff": "2.5.1", "@eslint-react/eff": "2.5.7",
"@eslint-react/shared": "2.5.1", "@eslint-react/shared": "2.5.7",
"@eslint-react/var": "2.5.1", "@eslint-react/var": "2.5.7",
"@typescript-eslint/scope-manager": "^8.51.0", "@typescript-eslint/scope-manager": "^8.53.0",
"@typescript-eslint/type-utils": "^8.51.0", "@typescript-eslint/type-utils": "^8.53.0",
"@typescript-eslint/types": "^8.51.0", "@typescript-eslint/types": "^8.53.0",
"@typescript-eslint/utils": "^8.51.0", "@typescript-eslint/utils": "^8.53.0",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
"is-immutable-type": "^5.0.1", "is-immutable-type": "^5.0.1",
"string-ts": "^2.3.1", "string-ts": "^2.3.1",
@@ -5310,13 +5315,13 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "12.24.0", "version": "12.26.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.24.0.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.26.1.tgz",
"integrity": "sha512-ggTMRkIDPc76lHmM+dRT1MmVfFV6t/y+jkWjWuzR7FG5xRvnAAl/5wFPjzSkLE8Nu5E5uIQRCNxmIXtWJVo6XQ==", "integrity": "sha512-Uzc8wGldU4FpmGotthjjcj0SZhigcODjqvKT7lzVZHsmYkzQMFfMIv0vHQoXCeoe/Ahxqp4by4A6QbzFA/lblw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-dom": "^12.24.0", "motion-dom": "^12.24.11",
"motion-utils": "^12.23.28", "motion-utils": "^12.24.10",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -5457,9 +5462,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "16.5.0", "version": "17.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -5543,9 +5548,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "25.7.3", "version": "25.7.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz",
"integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==", "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -6523,12 +6528,12 @@
} }
}, },
"node_modules/motion": { "node_modules/motion": {
"version": "12.24.0", "version": "12.26.1",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.24.0.tgz", "resolved": "https://registry.npmjs.org/motion/-/motion-12.26.1.tgz",
"integrity": "sha512-FAnpl/DhCFct3p+T2hCjAY95w+3EggUk3b8Ql4jQ6mmdRbEaGD1000goqWeEYVoD7mqx9H0As9ORmRADD4LQAw==", "integrity": "sha512-IVhzx9HOQTiJ9ykthMOlZPnLwrkXziN5Q/yebsqBYlFJb2rHP8yhmKc8O/YUT9byPJlxOeqkzfNYCrVKZx8vqg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"framer-motion": "^12.24.0", "framer-motion": "^12.26.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -6549,18 +6554,18 @@
} }
}, },
"node_modules/motion-dom": { "node_modules/motion-dom": {
"version": "12.24.0", "version": "12.24.11",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.0.tgz", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.24.11.tgz",
"integrity": "sha512-RD2kZkFd/GH4fITI8IJvypGgn0vIu5vkrJaXIAkYqORGs5P0CKDHKNvswmoY1H+tbUAOPSh6VtUqoAmc/3Gvig==", "integrity": "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-utils": "^12.23.28" "motion-utils": "^12.24.10"
} }
}, },
"node_modules/motion-utils": { "node_modules/motion-utils": {
"version": "12.23.28", "version": "12.24.10",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.28.tgz", "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.24.10.tgz",
"integrity": "sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ==", "integrity": "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ms": { "node_modules/ms": {
@@ -6935,9 +6940,9 @@
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "16.5.1", "version": "16.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.1.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz",
"integrity": "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw==", "integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7174,9 +7179,9 @@
} }
}, },
"node_modules/simple-icons": { "node_modules/simple-icons": {
"version": "16.4.0", "version": "16.5.0",
"resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.4.0.tgz", "resolved": "https://registry.npmjs.org/simple-icons/-/simple-icons-16.5.0.tgz",
"integrity": "sha512-8CKtCvx1Zq3L0CBsR4RR1MjGCXkXbzdspwl2yCxs8oWkstbzj2+DatRKDee/tuj3Ffd/2CDzwEky9RgG2yggew==", "integrity": "sha512-72nn0oHADKx6Hknu7q6M0vfL8LiCUMKABOHane2+4xdqaFBSHfNNBjuZioihiqVQMz7IvVle4NKAM0IlXvl/9A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -7577,16 +7582,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.52.0", "version": "8.53.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.52.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz",
"integrity": "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==", "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.52.0", "@typescript-eslint/parser": "8.53.0",
"@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/typescript-estree": "8.53.0",
"@typescript-eslint/utils": "8.52.0" "@typescript-eslint/utils": "8.53.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7772,9 +7777,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.0", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -8140,9 +8145,17 @@
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
} }
}, },
"packages/cdn": {
"name": "@page-agent/cdn",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"page-agent": "0.2.0"
}
},
"packages/llms": { "packages/llms": {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "0.0.20", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
@@ -8150,32 +8163,32 @@
} }
}, },
"packages/page-agent": { "packages/page-agent": {
"version": "0.0.20", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@page-agent/llms": "0.0.20", "@page-agent/llms": "0.2.0",
"@page-agent/page-controller": "0.0.20", "@page-agent/page-controller": "0.2.0",
"@page-agent/ui": "0.0.20", "@page-agent/ui": "0.2.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"zod": "^4.3.5" "zod": "^4.3.5"
} }
}, },
"packages/page-controller": { "packages/page-controller": {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "0.0.20", "version": "0.2.0",
"license": "MIT" "license": "MIT",
"dependencies": {
"ai-motion": "^0.4.8"
}
}, },
"packages/ui": { "packages/ui": {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "0.0.20", "version": "0.2.0",
"license": "MIT", "license": "MIT"
"dependencies": {
"ai-motion": "^0.4.7"
}
}, },
"packages/website": { "packages/website": {
"name": "@page-agent/website", "name": "@page-agent/website",
"version": "0.0.20", "version": "0.2.0",
"dependencies": { "dependencies": {
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
@@ -8185,23 +8198,23 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.24.0", "motion": "^12.26.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.4.0", "simple-icons": "^16.5.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0" "tailwind-merge": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.2", "@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.3", "i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-i18next": "^16.5.1", "react-i18next": "^16.5.2",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"wouter": "^3.9.0" "wouter": "^3.9.0"
+11 -10
View File
@@ -1,13 +1,14 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
"packages/ui", "packages/ui",
"packages/llms", "packages/llms",
"packages/page-agent", "packages/page-agent",
"packages/cdn",
"packages/website" "packages/website"
], ],
"description": "AI-powered UI agent for web applications", "description": "AI-powered UI agent for web applications",
@@ -34,28 +35,28 @@
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.3.0", "@commitlint/cli": "^20.3.1",
"@commitlint/config-conventional": "^20.3.0", "@commitlint/config-conventional": "^20.3.1",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.55.2", "@microsoft/api-extractor": "^7.55.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.1", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.0.3", "@types/node": "^25.0.7",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.5.1", "eslint-plugin-react-dom": "^2.5.7",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26", "eslint-plugin-react-refresh": "^0.4.26",
"eslint-plugin-react-x": "^2.5.1", "eslint-plugin-react-x": "^2.5.7",
"globals": "^16.4.0", "globals": "^17.0.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.4", "lint-staged": "^16.2.4",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.52.0", "typescript-eslint": "^8.53.0",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.0", "vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2" "vite-plugin-css-injected-by-js": "^3.5.2"
}, },
"lint-staged": { "lint-staged": {
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@page-agent/cdn",
"private": false,
"version": "0.2.0",
"type": "module",
"files": [
"dist/"
],
"description": "CDN builds for page-agent - IIFE bundles for script tag usage",
"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 && vite build --mode demo"
},
"dependencies": {
"page-agent": "0.2.0"
}
}
+64
View File
@@ -0,0 +1,64 @@
// @ts-nocheck
/**
* Demo CDN build for page-agent
* Auto-initializes with built-in demo API for testing
*
* Usage:
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.demo.js"></script>
*/
import { PageAgent } from 'page-agent'
// Clean up existing instances to prevent multiple injections from bookmarklet
if (window.pageAgent) {
window.pageAgent.dispose()
}
// Mount to global window object
window.PageAgent = PageAgent
// Export for IIFE
export { PageAgent }
console.log('🚀 page-agent.js loaded!')
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
// Demo warning
console.log(
'%c⚠️ DEMO MODE %c\n' +
'This build uses a shared testing LLM with rate limits.\n' +
'For production, use page-agent.js with your own API key:\n' +
'https://alibaba.github.io/page-agent/#/docs/features/models',
'background: #f59e0b; color: #000; font-size: 14px; font-weight: bold; padding: 4px 8px; border-radius: 4px;',
'color: #f59e0b; font-size: 12px;'
)
// in case document.x is not ready yet
setTimeout(() => {
const currentScript = document.currentScript
if (currentScript) {
console.log('🚀 page-agent.js detected current script:', currentScript.src)
const url = new URL(currentScript.src)
const model = url.searchParams.get('model') || DEMO_MODEL
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
const language = url.searchParams.get('lang') || 'zh-CN'
const config = { model, baseURL, apiKey, language }
window.pageAgent = new PageAgent(config)
} else {
console.log('🚀 page-agent.js no current script detected, using default demo config')
const config = {
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
}
window.pageAgent = new PageAgent(config)
}
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
window.pageAgent.panel.show()
})
+15
View File
@@ -0,0 +1,15 @@
// @ts-nocheck
/**
* Full CDN build for page-agent
* Exposes PageAgent class for manual instantiation and configuration
*
* Usage:
* <script src="https://unpkg.com/@page-agent/cdn/dist/page-agent.js"></script>
* <script>
* const agent = new PageAgent({ model: 'gpt-4o', apiKey: 'your-key' })
* agent.panel.show()
* </script>
*/
import { PageAgent } from 'page-agent'
export { PageAgent }
+42
View File
@@ -0,0 +1,42 @@
// @ts-nocheck
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url))
/**
* CDN IIFE builds for page-agent
*
* Build targets (via --mode):
* - demo: page-agent.demo.js with auto-init and built-in API
* - full (default): page-agent.js exposes PageAgent class only
*/
export default defineConfig(({ mode }) => {
const isDemo = mode === 'demo'
const entry = isDemo ? resolve(__dirname, 'src/demo.js') : resolve(__dirname, 'src/full.js')
const fileName = isDemo ? 'page-agent.demo' : 'page-agent'
return {
plugins: [cssInjectedByJsPlugin({ relativeCSSInjection: true })],
publicDir: false,
esbuild: {
keepNames: true,
},
build: {
lib: {
entry,
name: 'PageAgent',
fileName: () => `${fileName}.js`,
formats: ['iife'],
},
outDir: resolve(__dirname, 'dist'),
emptyOutDir: !isDemo, // only empty on first build (full)
minify: false,
},
define: {
'process.env.NODE_ENV': '"production"',
},
}
})
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-llms.js", "main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js", "module": "./dist/lib/page-agent-llms.js",
+88 -74
View File
@@ -1,60 +1,58 @@
/** /**
* OpenAI Client implementation * OpenAI Client implementation
* @note This client is only for demonstrating how to implement a LLM client.
* @note Use OpenAILenientClient instead.
*/ */
import { InvokeError, InvokeErrorType } from './errors' import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types' import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import { modelPatch, zodToOpenAITool } from './utils' import { modelPatch, zodToOpenAITool } from './utils'
/** /**
* @deprecated Use OpenAILenientClient instead. * Client for OpenAI compatible APIs
*/ */
export class OpenAIClient implements LLMClient { export class OpenAIClient implements LLMClient {
config: LLMConfig config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: LLMConfig) { constructor(config: Required<LLMConfig>) {
this.config = config this.config = config
this.fetch = config.customFetch
} }
async invoke( async invoke(
messages: Message[], messages: Message[],
tools: Record<string, Tool>, tools: Record<string, Tool>,
abortSignal?: AbortSignal abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> { ): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format // 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool)) const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))
// Build request body
const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
}
// 2. Call API // 2. Call API
let response: Response let response: Response
try { try {
response = await fetch(`${this.config.baseURL}/chat/completions`, { response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`, Authorization: `Bearer ${this.config.apiKey}`,
}, },
body: JSON.stringify( body: JSON.stringify(modelPatch(requestBody)),
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
// model specific params
// reasoning_effort: 'minimal',
// verbosity: 'low',
parallel_tool_calls: false,
})
),
signal: abortSignal, signal: abortSignal,
}) })
} catch (error: unknown) { } catch (error: unknown) {
// Network error console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error) throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
} }
@@ -92,77 +90,94 @@ export class OpenAIClient implements LLMClient {
) )
} }
// 4. Parse and validate response
const data = await response.json() const data = await response.json()
// 4. Check finish_reason
const choice = data.choices?.[0] const choice = data.choices?.[0]
if (!choice) { if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data) throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
} }
// Check finish_reason
switch (choice.finish_reason) { switch (choice.finish_reason) {
case 'tool_calls': case 'tool_calls':
// ✅ Normal case 'function_call': // gemini
case 'stop': // some models use this even with tool calls
break break
case 'length': case 'length':
// ⚠️ Token limit reached
throw new InvokeError( throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH, InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached', 'Response truncated: max tokens reached'
data
) )
case 'content_filter': case 'content_filter':
// ❌ Content filtered throw new InvokeError(InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system')
throw new InvokeError(
InvokeErrorType.CONTENT_FILTER,
'Content filtered by safety system',
data
)
case 'stop':
// ❌ Did not call tool (we require tool call)
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'Model did not call any tool', data)
default: default:
throw new InvokeError( throw new InvokeError(
InvokeErrorType.UNKNOWN, InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`, `Unexpected finish_reason: ${choice.finish_reason}`
data
) )
} }
// 5. Parse tool call // Apply normalizeResponse if provided (for fixing format issues automatically)
const toolCall = choice.message?.tool_calls?.[0] const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data
if (!toolCall) { const normalizedChoice = (normalizedData as any).choices?.[0]
throw new InvokeError(InvokeErrorType.NO_TOOL_CALL, 'No tool call found in response', data)
}
const toolName = toolCall.function.name // Get tool name from response
const tool = tools[toolName] const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
if (!tool) { if (!toolCallName) {
throw new InvokeError(InvokeErrorType.UNKNOWN, `Tool ${toolName} not found`, data)
}
// 6. Parse and validate arguments
let toolArgs: unknown
try {
toolArgs = JSON.parse(toolCall.function.arguments)
} catch (e) {
throw new InvokeError(InvokeErrorType.INVALID_TOOL_ARGS, 'Invalid JSON in tool arguments', e)
}
// Validate against zod schema
const validation = tool.inputSchema.safeParse(toolArgs)
if (!validation.success) {
throw new InvokeError( throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS, InvokeErrorType.NO_TOOL_CALL,
`Tool arguments validation failed: ${validation.error.message}`, 'No tool call found in response',
validation.error normalizedData
) )
} }
// 7. Execute tool const tool = tools[toolCallName]
if (!tool) {
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Tool "${toolCallName}" not found in tools`,
normalizedData
)
}
// Extract and parse tool arguments
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
if (!argString) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'No tool call arguments found',
normalizedData
)
}
let parsedArgs: unknown
try {
parsedArgs = JSON.parse(argString)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
// Validate with schema
const validation = tool.inputSchema.safeParse(parsedArgs)
if (!validation.success) {
console.error(validation.error)
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Tool arguments validation failed',
validation.error
)
}
const toolInput = validation.data
// 5. Execute tool
let toolResult: unknown let toolResult: unknown
try { try {
toolResult = await tool.execute(validation.data) toolResult = await tool.execute(toolInput)
} catch (e) { } catch (e) {
throw new InvokeError( throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR, InvokeErrorType.TOOL_EXECUTION_ERROR,
@@ -171,12 +186,11 @@ export class OpenAIClient implements LLMClient {
) )
} }
// 8. Return result (including cache tokens) // Return result
return { return {
toolCall: { toolCall: {
// id: toolCall.id, name: toolCallName,
name: toolName, args: toolInput,
args: validation.data as Record<string, unknown>,
}, },
toolResult, toolResult,
usage: { usage: {
-124
View File
@@ -1,124 +0,0 @@
/**
* OpenAI Client implementation
*/
import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeResult, LLMClient, LLMConfig, MacroToolInput, Message, Tool } from './types'
import { lenientParseMacroToolCall, modelPatch, zodToOpenAITool } from './utils'
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
this.config = config
this.fetch = config.customFetch
}
async invoke(
messages: Message[],
tools: { AgentOutput: Tool<MacroToolInput> },
abortSignal?: AbortSignal
): Promise<InvokeResult> {
// 1. Convert tools to OpenAI format
const openaiTools = Object.entries(tools).map(([name, tool]) => zodToOpenAITool(name, tool))
// 2. Call API
let response: Response
try {
response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(
modelPatch({
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
// tool_choice: 'required',
tool_choice: { type: 'function', function: { name: 'AgentOutput' } },
parallel_tool_calls: false,
})
),
signal: abortSignal,
})
} catch (error: unknown) {
// Network error
console.error(error)
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, 'Network request failed', error)
}
// 3. Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch()
const errorMessage =
(errorData as { error?: { message?: string } }).error?.message || response.statusText
if (response.status === 401 || response.status === 403) {
throw new InvokeError(
InvokeErrorType.AUTH_ERROR,
`Authentication failed: ${errorMessage}`,
errorData
)
}
if (response.status === 429) {
throw new InvokeError(
InvokeErrorType.RATE_LIMIT,
`Rate limit exceeded: ${errorMessage}`,
errorData
)
}
if (response.status >= 500) {
throw new InvokeError(
InvokeErrorType.SERVER_ERROR,
`Server error: ${errorMessage}`,
errorData
)
}
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`HTTP ${response.status}: ${errorMessage}`,
errorData
)
}
// parse response
const data = await response.json()
const tool = tools.AgentOutput
const macroToolInput = lenientParseMacroToolCall(data, tool.inputSchema as any)
// Execute tool
let toolResult: unknown
try {
toolResult = await tool.execute(macroToolInput)
} catch (e) {
throw new InvokeError(
InvokeErrorType.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${(e as Error).message}`,
e
)
}
// Return result (including cache tokens)
return {
toolCall: {
// id: toolCall.id,
name: 'AgentOutput',
args: macroToolInput,
},
toolResult,
usage: {
promptTokens: data.usage?.prompt_tokens ?? 0,
completionTokens: data.usage?.completion_tokens ?? 0,
totalTokens: data.usage?.total_tokens ?? 0,
cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
},
rawResponse: data,
}
}
}
+1 -18
View File
@@ -1,20 +1,3 @@
// Dev environment: use .env config if available, otherwise fallback to testing api // Internal constants
export const DEFAULT_MODEL_NAME: string =
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
? import.meta.env.LLM_MODEL_NAME
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_API_KEY: string =
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEFAULT_BASE_URL: string =
import.meta.env.DEV && import.meta.env.LLM_BASE_URL
? import.meta.env.LLM_BASE_URL
: 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
// internal
export const LLM_MAX_RETRIES = 2 export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
+18 -66
View File
@@ -1,72 +1,23 @@
/** import { OpenAIClient } from './OpenAIClient'
* @topic LLM 与主流程的隔离 import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
* @reasoning
* 将 llm 的调用和主流程分开是复杂的,
* 因为 agent 的 tool call 通常集成在 llm 模块中,而而先得到 llm 返回,然后处理工具调用
* tools 和 llm 调用的逻辑不可避免地耦合在一起,tool 的执行又和主流程耦合在一起
* 而 history 的维护和更新逻辑,又必须嵌入多轮 tool call 中
* @reasoning
* - 放弃框架提供的自动的多轮调用,每轮调用都由主流程发起
* - 理想情况下,llm 调用应该获得 structured output,然后由额外的模块触发 tool call,目前模型和框架都无法实现
* - 当前只能将 llm api 和 本地 tool call 耦合在一起,不关心其中的衔接方式
* @conclusion
* - @llm responsibility boundary:
* - call llm api with given messages and tools
* - invoke tool call and get the result of the tool
* - return the result to main loop
* - @main_loop responsibility boundary:
* - maintain all behaviors of an **agent**
* @conclusion
* - 这里的 llm 模块不是 agent,只负责一轮 llm 调用和工具调用,无状态
*/
/**
* @topic 结构化输出
* @facts
* - 几乎所有模型都支持 tool call schema
* - 几乎所有模型都支持返回 json
* - 只有 openAI/grok/gemini 支持 schema 并保证格式
* - 主流模型都支持 tool_choice: required
* - 除了 qwen 必须指定一个函数名 (9月上新后支持)
* @conclusion
* - 永远使用 tool call 来返回结构化数据,禁止模型直接返回(视为出错)
* - 不能假设 tool 参数合法,必须有修复机制,而且修复也应该使用 tool call 返回
*/
import { OpenAIClient } from './OpenAILenientClient'
import {
DEFAULT_API_KEY,
DEFAULT_BASE_URL,
DEFAULT_MODEL_NAME,
DEFAULT_TEMPERATURE,
LLM_MAX_RETRIES,
} from './constants'
import { InvokeError } from './errors' import { InvokeError } from './errors'
import type { import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
} from './types'
export type { export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
AgentBrain,
InvokeResult,
LLMClient,
LLMConfig,
MacroToolInput,
MacroToolResult,
Message,
Tool,
}
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> { export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.apiKey || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' +
'See: https://alibaba.github.io/page-agent/#/docs/features/models'
)
}
return { return {
baseURL: config.baseURL ?? DEFAULT_BASE_URL, baseURL: config.baseURL,
apiKey: config.apiKey ?? DEFAULT_API_KEY, apiKey: config.apiKey,
model: config.model ?? DEFAULT_MODEL_NAME, model: config.model,
temperature: config.temperature ?? DEFAULT_TEMPERATURE, temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES, maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
@@ -93,11 +44,12 @@ export class LLM extends EventTarget {
async invoke( async invoke(
messages: Message[], messages: Message[],
tools: Record<string, Tool>, tools: Record<string, Tool>,
abortSignal: AbortSignal abortSignal: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> { ): Promise<InvokeResult> {
return await withRetry( return await withRetry(
async () => { async () => {
const result = await this.client.invoke(messages, tools, abortSignal) const result = await this.client.invoke(messages, tools, abortSignal, options)
return result return result
}, },
+23 -37
View File
@@ -32,6 +32,24 @@ export interface Tool<TParams = any, TResult = any> {
execute: (args: TParams) => Promise<TResult> execute: (args: TParams) => Promise<TResult>
} }
/**
* Invoke options for LLM call
*/
export interface InvokeOptions {
/**
* Force LLM to call a specific tool by name.
* If provided: tool_choice = { type: 'function', function: { name: toolChoiceName } }
* If not provided: tool_choice = 'required' (must call some tool, but model chooses which)
*/
toolChoiceName?: string
/**
* Response normalization function.
* Called before parsing the response.
* Used to fix various response format errors from the model.
*/
normalizeResponse?: (response: any) => any
}
/** /**
* LLM Client interface * LLM Client interface
* Note: Does not use generics because each tool in the tools array has different types * Note: Does not use generics because each tool in the tools array has different types
@@ -40,7 +58,8 @@ export interface LLMClient {
invoke( invoke(
messages: Message[], messages: Message[],
tools: Record<string, Tool>, tools: Record<string, Tool>,
abortSignal?: AbortSignal abortSignal?: AbortSignal,
options?: InvokeOptions
): Promise<InvokeResult> ): Promise<InvokeResult>
} }
@@ -68,9 +87,9 @@ export interface InvokeResult<TResult = unknown> {
* LLM configuration * LLM configuration
*/ */
export interface LLMConfig { export interface LLMConfig {
baseURL?: string baseURL: string
apiKey?: string apiKey: string
model?: string model: string
temperature?: number temperature?: number
maxRetries?: number maxRetries?: number
@@ -82,36 +101,3 @@ export interface LLMConfig {
*/ */
customFetch?: typeof globalThis.fetch customFetch?: typeof globalThis.fetch
} }
/**
* Agent brain state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentBrain {
// thinking?: string
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends AgentBrain {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
+18 -175
View File
@@ -4,8 +4,7 @@
import chalk from 'chalk' import chalk from 'chalk'
import { z } from 'zod' import { z } from 'zod'
import { InvokeError, InvokeErrorType } from './errors' import type { Tool } from './types'
import type { MacroToolInput, Tool } from './types'
function debug(message: string) { function debug(message: string) {
console.debug(chalk.gray('[LLM]'), message) console.debug(chalk.gray('[LLM]'), message)
@@ -26,176 +25,6 @@ export function zodToOpenAITool(name: string, tool: Tool) {
} }
} }
/**
* Although some models cannot guarantee correct response. Common issues are fixable:
* - Instead of returning a proper tool call. Return the tool call parameters in the message content.
* - Returned tool calls or messages don't follow the nested MacroToolInput format.
*/
export function lenientParseMacroToolCall(
responseData: any,
inputSchema: z.ZodObject<MacroToolInput & Record<string, any>>
): MacroToolInput {
// check
const choice = responseData.choices?.[0]
if (!choice) {
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', responseData)
}
// check
switch (choice.finish_reason) {
case 'tool_calls':
case 'function_call': // gemini
case 'stop': // will try a robust parse
// ✅ Normal
break
case 'length':
// ⚠️ Token limit reached
throw new InvokeError(
InvokeErrorType.CONTEXT_LENGTH,
'Response truncated: max tokens reached'
)
case 'content_filter':
// ❌ Content filtered
throw new InvokeError(InvokeErrorType.CONTENT_FILTER, 'Content filtered by safety system')
default:
throw new InvokeError(
InvokeErrorType.UNKNOWN,
`Unexpected finish_reason: ${choice.finish_reason}`
)
}
// Extract action schema from MacroToolInput schema
const actionSchema = inputSchema.shape.action
if (!actionSchema) {
throw new Error('inputSchema must have an "action" field')
}
// patch stopReason mis-format
let arg: string | null = null
// try to use tool call
const toolCall = choice.message?.tool_calls?.[0]?.function
arg = toolCall?.arguments ?? null
if (arg && toolCall.name !== 'AgentOutput') {
// TODO: check if toolCall.name is a valid action name
// case: instead of AgentOutput, the model returned a action name as tool call
console.log(chalk.yellow('lenientParseMacroToolCall: #1 fixing incorrect tool call'))
let tmpArg
try {
tmpArg = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
arg = JSON.stringify({ action: { [toolCall.name]: tmpArg } })
}
if (!arg) {
// try to use message content as JSON
arg = choice.message?.content.trim() || null
}
if (!arg) {
throw new InvokeError(
InvokeErrorType.NO_TOOL_CALL,
'No tool call or content found in response',
responseData
)
}
// make sure is valid JSON
let parsedArgs: any
try {
parsedArgs = JSON.parse(arg)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse tool arguments as JSON',
error
)
}
// patch incomplete formats
if (parsedArgs.action || parsedArgs.evaluation_previous_goal || parsedArgs.next_goal) {
// case: nested MacroToolInput format (correct format)
// some models may give a empty action (they may think reasoning and action should be separate)
if (!parsedArgs.action) {
console.log(chalk.yellow('lenientParseMacroToolCall: #2 fixing incorrect tool call'))
parsedArgs.action = {
wait: { seconds: 1 },
}
}
} else if (parsedArgs.type && parsedArgs.function) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.function.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.function.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #3 fixing incorrect tool call'))
parsedArgs = parsedArgs.function.arguments
} else if (parsedArgs.name && parsedArgs.arguments) {
// case: upper level function call format provided. only keep its arguments
// TODO: check if function name is a valid action name
if (parsedArgs.name !== 'AgentOutput')
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Expected function name "AgentOutput", got "${parsedArgs.name}"`,
null
)
console.log(chalk.yellow('lenientParseMacroToolCall: #4 fixing incorrect tool call'))
parsedArgs = parsedArgs.arguments
} else {
// case: only action parameters provided, wrap into MacroToolInput
// TODO: check if action name is valid
console.log(chalk.yellow('lenientParseMacroToolCall: #5 fixing incorrect tool call'))
parsedArgs = { action: parsedArgs } as MacroToolInput
}
// make sure it's not wrapped as string
if (typeof parsedArgs === 'string') {
console.log(chalk.yellow('lenientParseMacroToolCall: #6 fixing incorrect tool call'))
try {
parsedArgs = JSON.parse(parsedArgs)
} catch (error) {
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
'Failed to parse nested tool arguments as JSON',
error
)
}
}
const validation = inputSchema.safeParse(parsedArgs)
if (validation.success) {
return validation.data as unknown as MacroToolInput
} else {
const action = parsedArgs.action ?? {}
const actionName = Object.keys(action)[0] || 'unknown'
const actionArgs = JSON.stringify(action[actionName] || 'unknown')
// TODO: check if action name is valid. give a readable error message
throw new InvokeError(
InvokeErrorType.INVALID_TOOL_ARGS,
`Tool arguments validation failed: action "${actionName}" with args ${actionArgs}`,
validation.error
)
}
}
/** /**
* Patch model specific parameters * Patch model specific parameters
*/ */
@@ -205,11 +34,25 @@ export function modelPatch(body: Record<string, any>) {
const modelName = normalizeModelName(model) const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
}
if (modelName.startsWith('claude')) { if (modelName.startsWith('claude')) {
debug('Applying Claude patch: change tool_choice and disable thinking') debug('Applying Claude patch: disable thinking')
body.tool_choice = { type: 'tool', name: 'AgentOutput' }
body.thinking = { type: 'disabled' } body.thinking = { type: 'disabled' }
// body.reasoning = { enabled: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
} }
if (modelName.startsWith('grok')) { if (modelName.startsWith('grok')) {
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "page-agent", "name": "page-agent",
"private": false, "private": false,
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/esm/page-agent.js", "main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js", "module": "./dist/esm/page-agent.js",
@@ -46,8 +46,8 @@
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
"zod": "^4.3.5", "zod": "^4.3.5",
"@page-agent/llms": "0.0.20", "@page-agent/llms": "0.2.0",
"@page-agent/page-controller": "0.0.20", "@page-agent/page-controller": "0.2.0",
"@page-agent/ui": "0.0.20" "@page-agent/ui": "0.2.0"
} }
} }
+186 -99
View File
@@ -2,15 +2,9 @@
* Copyright (C) 2025 Alibaba Group Holding Limited * Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved. * All rights reserved.
*/ */
import { import { LLM, type Tool } from '@page-agent/llms'
type AgentBrain,
LLM,
type MacroToolInput,
type MacroToolResult,
type Tool,
} from '@page-agent/llms'
import { PageController } from '@page-agent/page-controller' import { PageController } from '@page-agent/page-controller'
import { Panel, SimulatorMask } from '@page-agent/ui' import { Panel } from '@page-agent/ui'
import chalk from 'chalk' import chalk from 'chalk'
import zod from 'zod' import zod from 'zod'
@@ -18,15 +12,50 @@ import type { PageAgentConfig } from './config'
import { MAX_STEPS } from './config/constants' import { MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw' import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools' import { tools } from './tools'
import { trimLines, uid, waitUntil } from './utils' import { normalizeResponse, trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert' import { assert } from './utils/assert'
/**
* Agent reflection state - the reflection-before-action model
*
* Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal?
* - memory: Key information to remember for future steps
* - next_goal: What should be accomplished in the next action?
*/
export interface AgentReflection {
evaluation_previous_goal: string
memory: string
next_goal: string
}
/**
* MacroTool input structure
*
* This is the core abstraction that enforces the "reflection-before-action" mental model.
* Before executing any action, the LLM must output its reasoning state.
*/
export interface MacroToolInput extends Partial<AgentReflection> {
action: Record<string, any>
}
/**
* MacroTool output structure
*/
export interface MacroToolResult {
input: MacroToolInput
output: string
}
export type { PageAgentConfig } export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools' export { tool, type PageAgentTool } from './tools'
export type { AgentBrain, MacroToolInput, MacroToolResult }
export interface AgentHistory { /**
brain: AgentBrain * A single agent step with reflection and action
*/
export interface AgentStep {
type: 'step'
reflection: Partial<AgentReflection>
action: { action: {
name: string name: string
input: any input: any
@@ -41,10 +70,33 @@ export interface AgentHistory {
} }
} }
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Union type for all history events
*/
export type HistoryEvent = AgentStep | ObservationEvent | UserTakeoverEvent
/** @deprecated Use AgentStep instead */
export type AgentHistory = AgentStep
export interface ExecutionResult { export interface ExecutionResult {
success: boolean success: boolean
data: string data: string
history: AgentHistory[] history: HistoryEvent[]
} }
export class PageAgent extends EventTarget { export class PageAgent extends EventTarget {
@@ -58,7 +110,6 @@ export class PageAgent extends EventTarget {
taskId = '' taskId = ''
#llm: LLM #llm: LLM
#totalWaitTime = 0
#abortController = new AbortController() #abortController = new AbortController()
#llmRetryListener: ((e: Event) => void) | null = null #llmRetryListener: ((e: Event) => void) | null = null
#llmErrorListener: ((e: Event) => void) | null = null #llmErrorListener: ((e: Event) => void) | null = null
@@ -67,12 +118,18 @@ export class PageAgent extends EventTarget {
/** PageController for DOM operations */ /** PageController for DOM operations */
pageController: PageController pageController: PageController
/** Fullscreen mask */ /** Runtime states for tracking across steps */
mask = new SimulatorMask() states = {
/** History records */ /** Accumulated wait time in seconds, used by wait tool */
history: AgentHistory[] = [] totalWaitTime: 0,
/** Last known URL for detecting navigation */
lastURL: '',
}
constructor(config: PageAgentConfig = {}) { /** History event stream */
history: HistoryEvent[] = []
constructor(config: PageAgentConfig) {
super() super()
this.config = config this.config = config
@@ -89,8 +146,11 @@ export class PageAgent extends EventTarget {
}) })
this.tools = new Map(tools) this.tools = new Map(tools)
// Initialize PageController with config // Initialize PageController with config (mask enabled by default)
this.pageController = new PageController(this.config) this.pageController = new PageController({
...this.config,
enableMask: this.config.enableMask ?? true,
})
// Listen to LLM events // Listen to LLM events
this.#llmRetryListener = (e) => { this.#llmRetryListener = (e) => {
@@ -125,8 +185,13 @@ export class PageAgent extends EventTarget {
} }
/** /**
* @todo maybe return something? * Push a persistent observation to the history event stream.
* This will be visible in <agent_history> and remain in memory across steps.
*/ */
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
}
async execute(task: string): Promise<ExecutionResult> { async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required') if (!task) throw new Error('Task is required')
this.task = task this.task = task
@@ -140,7 +205,7 @@ export class PageAgent extends EventTarget {
await onBeforeTask.call(this) await onBeforeTask.call(this)
// Show mask and panel // Show mask and panel
this.mask.show() this.pageController.showMask()
this.panel.show() this.panel.show()
this.panel.reset() this.panel.reset()
@@ -154,10 +219,18 @@ export class PageAgent extends EventTarget {
this.history = [] this.history = []
// Reset states
this.states = {
totalWaitTime: 0,
lastURL: '',
}
try { try {
let step = 0 let step = 0
while (true) { while (true) {
await this.#generateObservations(step)
await onBeforeStep.call(this, step) await onBeforeStep.call(this, step)
console.group(`step: ${step}`) console.group(`step: ${step}`)
@@ -183,29 +256,34 @@ export class PageAgent extends EventTarget {
}, },
], ],
{ AgentOutput: this.#packMacroTool() }, { AgentOutput: this.#packMacroTool() },
this.#abortController.signal this.#abortController.signal,
{
toolChoiceName: 'AgentOutput',
normalizeResponse,
}
) )
const macroResult = result.toolResult as MacroToolResult const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input const input = macroResult.input
const output = macroResult.output const output = macroResult.output
const brain = { const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal || '', evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory || '', memory: input.memory,
next_goal: input.next_goal || '', next_goal: input.next_goal,
} }
const actionName = Object.keys(input.action)[0] const actionName = Object.keys(input.action)[0]
const action = { const action: AgentStep['action'] = {
name: actionName, name: actionName,
input: input.action[actionName], input: input.action[actionName],
output: output, output: output,
} }
this.history.push({ this.history.push({
brain, type: 'step',
reflection,
action, action,
usage: result.usage, usage: result.usage,
}) } as AgentStep)
console.log(chalk.green('Step finished:'), actionName) console.log(chalk.green('Step finished:'), actionName)
console.groupEnd() console.groupEnd()
@@ -279,6 +357,7 @@ export class PageAgent extends EventTarget {
}) })
return { return {
description: 'You MUST call this tool every step. Outputs your reflections and next action.',
inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>, inputSchema: macroToolSchema as zod.ZodType<MacroToolInput>,
execute: async (input: MacroToolInput): Promise<MacroToolResult> => { execute: async (input: MacroToolInput): Promise<MacroToolResult> => {
// abort // abort
@@ -291,13 +370,20 @@ export class PageAgent extends EventTarget {
const toolName = Object.keys(action)[0] const toolName = Object.keys(action)[0]
const toolInput = action[toolName] const toolInput = action[toolName]
const brain = trimLines(`✅: ${input.evaluation_previous_goal}
💾: ${input.memory}
🎯: ${input.next_goal}
`)
console.log(brain) // Build reflection text, only include non-empty fields
this.panel.update({ type: 'thinking', text: brain }) const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
this.panel.update({ type: 'thinking', text: reflectionText })
}
// Find the corresponding tool // Find the corresponding tool
const tool = tools.get(toolName) const tool = tools.get(toolName)
@@ -309,20 +395,14 @@ export class PageAgent extends EventTarget {
const startTime = Date.now() const startTime = Date.now()
// Execute tool, bind `this` to PageAgent // Execute tool, bind `this` to PageAgent
let result = await tool.execute.bind(this)(toolInput) const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result) console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
if (toolName === 'wait') { // Reset wait time for non-wait tools
this.#totalWaitTime += Math.round(toolInput.seconds + duration / 1000) if (toolName !== 'wait') {
result += `\n<sys> You have waited ${this.#totalWaitTime} seconds accumulatively.` this.states.totalWaitTime = 0
if (this.#totalWaitTime >= 3)
result += '\nDo NOT wait any longer unless you have a good reason.\n'
result += '</sys>'
} else {
// For other tools, reset wait time
this.#totalWaitTime = 0
} }
// Briefly display execution result // Briefly display execution result
@@ -399,6 +479,34 @@ export class PageAgent extends EventTarget {
return result return result
} }
/**
* Generate observations before each step
* - URL change detection
* - Too many steps warning
* @todo loop detection
* @todo console error
*/
async #generateObservations(stepCount: number): Promise<void> {
// Detect URL change
const currentURL = await this.pageController.getCurrentUrl()
if (currentURL !== this.states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.states.lastURL = currentURL
}
// Warn about remaining steps
const remaining = MAX_STEPS - stepCount
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
}
async #assembleUserPrompt(): Promise<string> { async #assembleUserPrompt(): Promise<string> {
let prompt = '' let prompt = ''
@@ -410,31 +518,42 @@ export class PageAgent extends EventTarget {
// - <step_info> // - <step_info>
// <agent_state> // <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += `<agent_state> prompt += `<agent_state>
<user_request> <user_request>
${this.task} ${this.task}
</user_request> </user_request>
<step_info> <step_info>
Step ${this.history.length + 1} of ${MAX_STEPS} max possible steps Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
Current date and time: ${new Date().toISOString()} Current date and time: ${new Date().toISOString()}
</step_info> </step_info>
</agent_state> </agent_state>
` `
// <agent_history> // <agent_history>
// - <step_> // - <step_N> for steps
// - <sys> for observations and system messages
prompt += '\n<agent_history>\n' prompt += '\n<agent_history>\n'
this.history.forEach((history, index) => { let stepIndex = 0
prompt += `<step_${index + 1}> for (const event of this.history) {
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal} if (event.type === 'step') {
Memory: ${history.brain.memory} stepIndex++
Next Goal: ${history.brain.next_goal} prompt += `<step_${stepIndex}>
Action Results: ${history.action.output} Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
</step_${index + 1}> Memory: ${event.reflection.memory}
Next Goal: ${event.reflection.next_goal}
Action Results: ${event.action.output}
</step_${stepIndex}>
` `
}) } else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
}
}
prompt += '</agent_history>\n\n' prompt += '</agent_history>\n\n'
@@ -458,59 +577,28 @@ export class PageAgent extends EventTarget {
// Task completed // Task completed
this.panel.update({ type: 'completed' }) this.panel.update({ type: 'completed' })
this.mask.hide() this.pageController.hideMask()
this.#abortController.abort() this.#abortController.abort()
} }
async #getBrowserState(): Promise<string> { async #getBrowserState(): Promise<string> {
const pageUrl = await this.pageController.getCurrentUrl() const state = await this.pageController.getBrowserState()
const pageTitle = await this.pageController.getPageTitle()
const pi = await this.pageController.getPageInfo()
const viewportExpansion = await this.pageController.getViewportExpansion()
this.mask.wrapper.style.pointerEvents = 'none'
await this.pageController.updateTree()
this.mask.wrapper.style.pointerEvents = 'auto'
let simplifiedHTML = await this.pageController.getSimplifiedHTML()
let content = state.content
if (this.config.transformPageContent) { if (this.config.transformPageContent) {
simplifiedHTML = await this.config.transformPageContent(simplifiedHTML) content = await this.config.transformPageContent(content)
} }
let prompt = trimLines(`<browser_state> return trimLines(`<browser_state>
Current Page: [${pageTitle}](${pageUrl}) Current Page: [${state.title}](${state.url})
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 ${state.header}
${content}
${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:'} ${state.footer}
</browser_state>
`) `)
// Page header info
const has_content_above = pi.pixels_above > 4
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 += simplifiedHTML
prompt += `\n`
// Page footer info
const has_content_below = pi.pixels_below > 4
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`
}
prompt += `</browser_state>\n`
return prompt
} }
dispose(reason?: string) { dispose(reason?: string) {
@@ -518,7 +606,6 @@ export class PageAgent extends EventTarget {
this.disposed = true this.disposed = true
this.pageController.dispose() this.pageController.dispose()
this.panel.dispose() this.panel.dispose()
this.mask.dispose()
this.history = [] this.history = []
this.#abortController.abort(reason ?? 'PageAgent disposed') this.#abortController.abort(reason ?? 'PageAgent disposed')
+2 -2
View File
@@ -2,7 +2,7 @@ import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller' import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@page-agent/ui' import type { SupportedLanguage } from '@page-agent/ui'
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent' import type { ExecutionResult, HistoryEvent, PageAgent } from '../PageAgent'
import type { PageAgentTool } from '../tools' import type { PageAgentTool } from '../tools'
export type { LLMConfig } export type { LLMConfig }
@@ -65,7 +65,7 @@ export interface AgentConfig {
// @todo: remove `this` binding, pass agent as explicit parameter instead // @todo: remove `this` binding, pass agent as explicit parameter instead
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void onAfterStep?: (this: PageAgent, stepCnt: number, history: HistoryEvent[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
+9
View File
@@ -57,6 +57,15 @@ tools.set(
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000) const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`) console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime) await waitFor(actualWaitTime)
this.states.totalWaitTime += input.seconds
if (this.states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
)
}
return `✅ Waited for ${input.seconds} seconds.` return `✅ Waited for ${input.seconds} seconds.`
}, },
}) })
+6 -1
View File
@@ -35,7 +35,12 @@ setTimeout(() => {
window.pageAgent = new PageAgent(config) window.pageAgent = new PageAgent(config)
} else { } else {
console.log('🚀 page-agent.js no current script detected, using default demo config') console.log('🚀 page-agent.js no current script detected, using default demo config')
window.pageAgent = new PageAgent() const config: PageAgentConfig = {
model: DEMO_MODEL,
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
}
window.pageAgent = new PageAgent(config)
} }
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config) console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
+157
View File
@@ -0,0 +1,157 @@
import chalk from 'chalk'
/**
* Normalize LLM response and fix common format issues.
*
* Handles:
* - No tool_calls but JSON in message.content (fallback)
* - Model returns action name as tool call instead of AgentOutput
* - Arguments wrapped as double JSON string
* - Nested function call format
* - Missing action field (fallback to wait)
* - etc.
*/
export function normalizeResponse(response: any): any {
let resolvedArguments = null as any
const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response')
const message = choice.message
if (!message) throw new Error('No message in choice')
const toolCall = message.tool_calls?.[0]
// fix level and location of arguments
if (toolCall?.function?.arguments) {
resolvedArguments = safeJsonParse(toolCall.function.arguments)
// case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #1: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
// case: sometimes the model returns json in content instead of tool_calls
if (message.content) {
const content = message.content.trim()
const jsonInContent = retrieveJsonFromString(content)
if (jsonInContent) {
resolvedArguments = safeJsonParse(jsonInContent)
// case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #2: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.arguments)
}
// case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') {
console.log(chalk.yellow(`[normalizeResponse] #3: fixing tool_call`))
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
}
// case: and sometimes action level only
// todo: needs better detection logic
if (
!resolvedArguments?.action &&
!resolvedArguments?.evaluation_previous_goal &&
!resolvedArguments?.memory &&
!resolvedArguments?.next_goal &&
!resolvedArguments?.thinking
) {
console.log(chalk.yellow(`[normalizeResponse] #4: fixing tool_call`))
resolvedArguments = { action: safeJsonParse(resolvedArguments) }
}
} else {
throw new Error('No tool_call and the message content does not contain valid JSON')
}
} else {
throw new Error('No tool_call nor message content is present')
}
}
// fix double stringified arguments
resolvedArguments = safeJsonParse(resolvedArguments)
if (resolvedArguments.action) {
resolvedArguments.action = safeJsonParse(resolvedArguments.action)
}
// fix incomplete formats
if (!resolvedArguments.action) {
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`))
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
}
// pack back to standard format
return {
...response,
choices: [
{
...choice,
message: {
...message,
tool_calls: [
{
...(toolCall || {}),
function: {
...(toolCall?.function || {}),
name: 'AgentOutput',
arguments: JSON.stringify(resolvedArguments),
},
},
],
},
},
],
}
}
/**
* Safely parse JSON, return original input if not json.
*/
function safeJsonParse(input: any): any {
if (typeof input === 'string') {
try {
return JSON.parse(input.trim())
} catch {
return input
}
}
return input
}
/**
* Extract and parse JSON from a string.
* - Treat content between the first `{` and the last `}` as JSON.
* - Try to parse that content as JSON and return the parsed value (object/array/primitive) if successful, otherwise return null.
*/
function retrieveJsonFromString(str: string): any {
try {
const json = /({[\s\S]*})/.exec(str) ?? []
if (json.length === 0) {
return null
}
return JSON.parse(json[0]!)
} catch {
return null
}
}
interface Choice {
message?: {
role?: 'assistant'
content?: string
tool_calls?: {
id?: string
type?: 'function'
function?: {
name?: string
arguments?: string
}
}[]
}
index?: 0
finish_reason?: 'tool_calls'
}
+2
View File
@@ -1,3 +1,5 @@
export { normalizeResponse } from './autoFixer'
/** /**
* Wait until condition becomes true * Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise * @returns Returns when condition becomes true, throws otherwise
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-controller.js", "main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js", "module": "./dist/lib/page-controller.js",
@@ -34,5 +34,8 @@
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "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{}})\"" "postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.8"
} }
} }
+93 -39
View File
@@ -18,13 +18,32 @@ import { VIEWPORT_EXPANSION } from './constants'
import * as dom from './dom' import * as dom from './dom'
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type' import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
import { getPageInfo } from './dom/getPageInfo' import { getPageInfo } from './dom/getPageInfo'
import { SimulatorMask } from './mask/SimulatorMask'
import { patchReact } from './patches/react' import { patchReact } from './patches/react'
export { SimulatorMask }
/** /**
* Configuration for PageController * Configuration for PageController
*/ */
export interface PageControllerConfig extends dom.DomConfig { export interface PageControllerConfig extends dom.DomConfig {
viewportExpansion?: number viewportExpansion?: number
/** Enable visual mask overlay during operations (default: false) */
enableMask?: boolean
}
/**
* Structured browser state for LLM consumption
*/
export interface BrowserState {
url: string
title: string
/** Page info + scroll position hint (e.g. "Page info: 1920x1080px...\n[Start of page]") */
header: string
/** Simplified HTML of interactive elements */
content: string
/** Page footer hint (e.g. "... 300 pixels below ..." or "[End of page]") */
footer: string
} }
interface ActionResult { interface ActionResult {
@@ -64,12 +83,19 @@ export class PageController extends EventTarget {
/** last time the tree was updated */ /** last time the tree was updated */
private lastTimeUpdate = 0 private lastTimeUpdate = 0
/** Visual mask overlay for blocking user interaction during automation */
private mask: SimulatorMask | null = null
constructor(config: PageControllerConfig = {}) { constructor(config: PageControllerConfig = {}) {
super() super()
this.config = config this.config = config
patchReact(this) patchReact(this)
if (config.enableMask) {
this.mask = new SimulatorMask()
}
} }
// ======= State Queries ======= // ======= State Queries =======
@@ -81,42 +107,6 @@ export class PageController extends EventTarget {
return window.location.href 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 * Get last tree update timestamp
*/ */
@@ -125,10 +115,43 @@ export class PageController extends EventTarget {
} }
/** /**
* Get the viewport expansion setting * Get structured browser state for LLM consumption.
* Automatically calls updateTree() to refresh the DOM state.
*/ */
async getViewportExpansion(): Promise<number> { async getBrowserState(): Promise<BrowserState> {
return this.config.viewportExpansion ?? VIEWPORT_EXPANSION const url = window.location.href
const title = document.title
const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
await this.updateTree()
const content = this.simplifiedHTML
// Build header: page info + scroll position hint
const pageInfoLine = `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`
const elementsLabel =
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:'
const hasContentAbove = pi.pixels_above > 4
const scrollHintAbove =
hasContentAbove && viewportExpansion !== -1
? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...`
: '[Start of page]'
const header = `${pageInfoLine}\n\n${elementsLabel}\n\n${scrollHintAbove}`
// Build footer: scroll position hint
const hasContentBelow = pi.pixels_below > 4
const footer =
hasContentBelow && viewportExpansion !== -1
? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...`
: '[End of page]'
return { url, title, header, content, footer }
} }
// ======= DOM Tree Operations ======= // ======= DOM Tree Operations =======
@@ -136,12 +159,18 @@ export class PageController extends EventTarget {
/** /**
* Update DOM tree, returns simplified HTML for LLM. * Update DOM tree, returns simplified HTML for LLM.
* This is the main method to refresh the page state. * This is the main method to refresh the page state.
* Automatically bypasses mask during DOM extraction if enabled.
*/ */
async updateTree(): Promise<string> { async updateTree(): Promise<string> {
this.dispatchEvent(new Event('beforeUpdate')) this.dispatchEvent(new Event('beforeUpdate'))
this.lastTimeUpdate = Date.now() this.lastTimeUpdate = Date.now()
// Temporarily bypass mask to allow DOM extraction
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'none'
}
dom.cleanUpHighlights() dom.cleanUpHighlights()
const blacklist = [ const blacklist = [
@@ -162,6 +191,11 @@ export class PageController extends EventTarget {
this.elementTextMap.clear() this.elementTextMap.clear()
this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML) this.elementTextMap = dom.getElementTextMap(this.simplifiedHTML)
// Restore mask blocking
if (this.mask) {
this.mask.wrapper.style.pointerEvents = 'auto'
}
this.dispatchEvent(new Event('afterUpdate')) this.dispatchEvent(new Event('afterUpdate'))
return this.simplifiedHTML return this.simplifiedHTML
@@ -326,6 +360,24 @@ export class PageController extends EventTarget {
} }
} }
// ======= Mask Operations =======
/**
* Show the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
showMask(): void {
this.mask?.show()
}
/**
* Hide the visual mask overlay.
* Only works if enableMask was set to true in config.
*/
hideMask(): void {
this.mask?.hide()
}
/** /**
* Dispose and clean up resources * Dispose and clean up resources
*/ */
@@ -335,5 +387,7 @@ export class PageController extends EventTarget {
this.selectorMap.clear() this.selectorMap.clear()
this.elementTextMap.clear() this.elementTextMap.clear()
this.simplifiedHTML = '<EMPTY>' this.simplifiedHTML = '<EMPTY>'
this.mask?.dispose()
this.mask = null
} }
} }
@@ -16,6 +16,7 @@
* @edit scrollable element detection * @edit scrollable element detection
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area * @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements
*/ */
export default ( export default (
@@ -1446,7 +1447,14 @@ export default (
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
*/ */
if (node.dataset?.browserUseIgnore === 'true') { if (node.dataset?.browserUseIgnore === 'true') {
return true // Skip this node and its children return null // Skip this node and its children
}
/**
* @edit exclude aria-hidden elements
*/
if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {
return null // Skip this node and its children
} }
// Special handling for root node (body) // Special handling for root node (body)
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default defineConfig({
}, },
outDir: resolve(__dirname, 'dist', 'lib'), outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: { rollupOptions: {
external: ['@page-agent/*'], external: ['@page-agent/*', 'ai-motion'],
}, },
minify: false, minify: false,
sourcemap: true, sourcemap: true,
+2 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-ui.js", "main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js", "module": "./dist/lib/page-agent-ui.js",
@@ -15,7 +15,7 @@
"files": [ "files": [
"dist/" "dist/"
], ],
"description": "UI components for page-agent - Panel, SimulatorMask, and i18n", "description": "UI components for page-agent - Panel and i18n",
"keywords": [ "keywords": [
"page-agent", "page-agent",
"ui", "ui",
@@ -34,8 +34,5 @@
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "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{}})\"" "postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
},
"dependencies": {
"ai-motion": "^0.4.7"
} }
} }
+2 -5
View File
@@ -1,6 +1,6 @@
import { type Step, UIState } from './UIState' import { type Step, UIState } from './UIState'
import { I18n, type SupportedLanguage } from './i18n' import { I18n, type SupportedLanguage } from './i18n'
import { truncate } from './utils' import { escapeHtml, truncate } from './utils'
import styles from './Panel.module.css' import styles from './Panel.module.css'
@@ -590,10 +590,7 @@ export class Panel {
#updateHistory(): void { #updateHistory(): void {
const steps = this.#state.getAllSteps() const steps = this.#state.getAllSteps()
this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('') this.#historySection.innerHTML = steps.map((step) => this.#createHistoryItem(step)).join('')
// Scroll to bottom to show latest records
this.#scrollToBottom() this.#scrollToBottom()
} }
@@ -660,7 +657,7 @@ export class Panel {
<div class="${styles.historyItem} ${typeClass}"> <div class="${styles.historyItem} ${typeClass}">
<div class="${styles.historyContent}"> <div class="${styles.historyContent}">
<span class="${styles.statusIcon}">${statusIcon}</span> <span class="${styles.statusIcon}">${statusIcon}</span>
<span>${step.displayText}</span> <span>${escapeHtml(step.displayText)}</span>
</div> </div>
<div class="${styles.historyMeta}"> <div class="${styles.historyMeta}">
${stepLabel} ${stepLabel}
-1
View File
@@ -1,4 +1,3 @@
export { Panel, type PanelConfig, type PanelUpdate } from './Panel' export { Panel, type PanelConfig, type PanelUpdate } from './Panel'
export { SimulatorMask } from './SimulatorMask'
export { UIState, type Step, type AgentStatus } from './UIState' export { UIState, type Step, type AgentStatus } from './UIState'
export { I18n, type SupportedLanguage, type TranslationKey } from './i18n' export { I18n, type SupportedLanguage, type TranslationKey } from './i18n'
+12
View File
@@ -4,3 +4,15 @@ export function truncate(text: string, maxLength: number): string {
} }
return text return text
} }
/**
* Escape HTML special characters to prevent XSS and rendering issues
*/
export function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
+1 -1
View File
@@ -29,7 +29,7 @@ export default defineConfig({
}, },
outDir: resolve(__dirname, 'dist', 'lib'), outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: { rollupOptions: {
external: ['ai-motion'], external: [],
}, },
minify: false, minify: false,
sourcemap: true, sourcemap: true,
+7 -7
View File
@@ -1,24 +1,24 @@
{ {
"name": "@page-agent/website", "name": "@page-agent/website",
"private": true, "private": true,
"version": "0.0.20", "version": "0.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host 0.0.0.0",
"build:website": "vite build", "build:website": "vite build",
"preview": "vite preview", "preview": "vite preview",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.2", "@types/react": "^19.2.8",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"i18next": "^25.7.3", "i18next": "^25.7.4",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-i18next": "^16.5.1", "react-i18next": "^16.5.2",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"wouter": "^3.9.0" "wouter": "^3.9.0"
@@ -32,10 +32,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.24.0", "motion": "^12.26.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.4.0", "simple-icons": "^16.5.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0" "tailwind-merge": "^3.4.0"
} }
@@ -41,6 +41,7 @@ export default function Header() {
className="hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible" className="hidden sm:block text-xs text-gray-600 dark:text-gray-300 py-0 font-normal overflow-visible"
duration={600} duration={600}
animateOnHover={true} animateOnHover={true}
aria-hidden="true"
> >
{t('header.slogan')} {t('header.slogan')}
</HyperText> </HyperText>
@@ -84,6 +85,9 @@ export default function Header() {
role="navigation" role="navigation"
aria-label={t('header.nav_docs')} aria-label={t('header.nav_docs')}
> >
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
</span>
<Link <Link
href="/docs/introduction/overview" href="/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
+15 -3
View File
@@ -1,3 +1,15 @@
export const CDN_URL = 'https://cdn.jsdelivr.net/npm/page-agent@latest/dist/umd/page-agent.js' // Demo build (auto-init with demo LLM, for quick testing)
export const CDN_CN_URL = export const CDN_DEMO_URL = 'https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.demo.js'
'https://registry.npmmirror.com/page-agent/latest/files/dist/umd/page-agent.js' export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.demo.js'
// Full build (exports PageAgent class, usage identical to npm)
export const CDN_FULL_URL = 'https://cdn.jsdelivr.net/npm/@page-agent/cdn/dist/page-agent.js'
export const CDN_FULL_CN_URL =
'https://registry.npmmirror.com/@page-agent/cdn/latest/files/dist/page-agent.js'
// Demo LLM for website testing
export const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
export const DEMO_BASE_URL =
'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
export const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
+4
View File
@@ -1,5 +1,9 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VERSION: string
}
declare module '*.module.css' { declare module '*.module.css' {
const classes: Record<string, string> const classes: Record<string, string>
export default classes export default classes
+24 -10
View File
@@ -12,10 +12,16 @@ import { Highlighter } from '../components/ui/highlighter'
import { NeonGradientCard } from '../components/ui/neon-gradient-card' import { NeonGradientCard } from '../components/ui/neon-gradient-card'
import { Particles } from '../components/ui/particles' import { Particles } from '../components/ui/particles'
import { SparklesText } from '../components/ui/sparkles-text' import { SparklesText } from '../components/ui/sparkles-text'
import { CDN_CN_URL, CDN_URL } from '../constants' import {
CDN_DEMO_CN_URL,
CDN_DEMO_URL,
DEMO_API_KEY,
DEMO_BASE_URL,
DEMO_MODEL,
} from '../constants'
function getInjection(useCN?: boolean) { function getInjection(useCN?: boolean) {
const cdn = useCN ? CDN_CN_URL : CDN_URL const cdn = useCN ? CDN_DEMO_CN_URL : CDN_DEMO_URL
const injection = encodeURI( const injection = encodeURI(
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();` `javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
@@ -73,13 +79,18 @@ export default function HomePage() {
}, },
}, },
// experimentalScriptExecutionTool: true, model:
import.meta.env.DEV && import.meta.env.LLM_MODEL_NAME
// testing server ? import.meta.env.LLM_MODEL_NAME
// @note: rate limit. prompt limit. : DEMO_MODEL,
// model: DEMO_MODEL, baseURL:
// baseURL: DEMO_BASE_URL, import.meta.env.DEV && import.meta.env.LLM_BASE_URL
// apiKey: DEMO_API_KEY, ? import.meta.env.LLM_BASE_URL
: DEMO_BASE_URL,
apiKey:
import.meta.env.DEV && import.meta.env.LLM_API_KEY
? import.meta.env.LLM_API_KEY
: DEMO_API_KEY,
}) })
win.pageAgent = pageAgent win.pageAgent = pageAgent
} }
@@ -95,7 +106,10 @@ export default function HomePage() {
{/* Hero Section */} {/* Hero Section */}
<main id="main-content"> <main id="main-content">
<section className="relative px-6 py-22 lg:py-28" aria-labelledby="hero-heading"> <section
className="relative px-6 py-22 lg:py-28 overflow-hidden"
aria-labelledby="hero-heading"
>
<div className="max-w-7xl mx-auto text-center"> <div className="max-w-7xl mx-auto text-center">
{/* Background Pattern + Particles */} {/* Background Pattern + Particles */}
<div className="absolute inset-0 opacity-30" aria-hidden="true"> <div className="absolute inset-0 opacity-30" aria-hidden="true">
+2 -2
View File
@@ -32,7 +32,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
{ {
title: t('nav.features'), title: t('nav.features'),
items: [ items: [
{ title: t('nav.models'), path: '/docs/features/model-integration' }, { title: t('nav.models'), path: '/docs/features/models' },
{ title: t('nav.custom_tools'), path: '/docs/features/custom-tools' }, { title: t('nav.custom_tools'), path: '/docs/features/custom-tools' },
{ title: t('nav.knowledge_injection'), path: '/docs/features/custom-instructions' }, { title: t('nav.knowledge_injection'), path: '/docs/features/custom-instructions' },
{ title: t('nav.data_masking'), path: '/docs/features/data-masking' }, { title: t('nav.data_masking'), path: '/docs/features/data-masking' },
@@ -43,7 +43,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
items: [ items: [
{ title: t('nav.configuration'), path: '/docs/integration/configuration' }, { title: t('nav.configuration'), path: '/docs/integration/configuration' },
{ title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' }, { title: t('nav.third_party_agent'), path: '/docs/integration/third-party-agent' },
{ title: '🚧 ' + t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' }, { title: t('nav.cdn_setup'), path: '/docs/integration/cdn-setup' },
{ {
title: '🚧 ' + t('nav.security_permissions'), title: '🚧 ' + t('nav.security_permissions'),
path: '/docs/integration/security-permissions', path: '/docs/integration/security-permissions',
@@ -4,13 +4,19 @@ import CodeEditor from '@/components/CodeEditor'
// Recommended models: lightweight with excellent tool call capabilities // Recommended models: lightweight with excellent tool call capabilities
const MODELS = { const MODELS = {
recommended: ['gpt-4.1-mini', 'claude-haiku-4.5', 'gemini-3-flash', 'deepseek-3.2', 'gpt-5.2'], recommended: [
verified: [ 'gpt-4.1-mini',
'claude-haiku-4.5',
'gemini-3-flash',
'deepseek-3.2',
'gpt-5.2',
'qwen-3-max', 'qwen-3-max',
],
verified: [
'qwen-3-plus',
'gpt-4.1', 'gpt-4.1',
'gpt-5', 'gpt-5',
'gpt-5-mini', 'gpt-5-mini',
'gpt-5.1',
'grok-4', 'grok-4',
'grok-code-fast', 'grok-code-fast',
'claude-sonnet-3.5', 'claude-sonnet-3.5',
@@ -21,7 +27,7 @@ const MODELS = {
], ],
} }
export default function ModelIntegration() { export default function Models() {
const { t } = useTranslation('docs') const { t } = useTranslation('docs')
const allModels = [...MODELS.recommended, ...MODELS.verified] const allModels = [...MODELS.recommended, ...MODELS.verified]
@@ -1,81 +1,124 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants' import { CDN_DEMO_CN_URL, CDN_DEMO_URL, CDN_FULL_CN_URL, CDN_FULL_URL } from '@/constants'
export default function CdnSetup() { export default function CdnSetup() {
const { i18n } = useTranslation() const { i18n } = useTranslation()
const isZh = i18n.language === 'zh-CN' const isZh = i18n.language === 'zh-CN'
return ( return (
<div> <div className="space-y-10">
<BetaNotice /> <header>
<h1 className="text-4xl font-bold mb-4">{isZh ? 'CDN 引入' : 'CDN Setup'}</h1>
<p className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed">
{isZh
? 'CDN 提供两种构建版本:Demo 版用于快速体验,标准版用法与 NPM 一致。'
: 'CDN provides two builds: Demo for quick testing, standard build with usage identical to NPM.'}
</p>
</header>
<h1 className="text-4xl font-bold mb-6">{isZh ? 'CDN 引入' : 'CDN Setup'}</h1> {/* Demo Build Section */}
<section>
<h2 className="text-2xl font-bold mb-3">{isZh ? '🚀 快速体验' : '🚀 Quick Try'}</h2>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed"> <p className="text-gray-600 dark:text-gray-300 mb-3">
{isZh {isZh
? '通过 CDN 快速集成 page-agent,无需复杂的构建配置。默认使用演示模型。' ? '自动初始化并使用内置 Demo LLM,无需任何配置:'
: 'Fastest way to integrate page-agent via CDN. No complex build setup needed. Demo model will be used by default.'} : 'Auto-initializes with built-in demo LLM, no configuration needed:'}
</p> </p>
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">{isZh ? 'CDN 地址' : 'CDN URLs'}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 font-semibold">{isZh ? '位置' : 'Location'}</th>
<th className="text-left py-3 px-4 font-semibold">URL</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4">{isZh ? '全球' : 'Global'}</td>
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_URL}</td>
</tr>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4">{isZh ? '中国镜像' : 'China Mirror'}</td>
<td className="py-3 px-4 font-mono text-xs break-all">{CDN_CN_URL}</td>
</tr>
</tbody>
</table>
</div>
</section>
<section className="mb-8">
<h2 className="text-2xl font-bold mb-4">{isZh ? '快速开始' : 'Quick Start'}</h2>
<CodeEditor <CodeEditor
className="mb-6" className="mb-3"
code={`<script code={`<script src="${CDN_DEMO_URL}" crossorigin="true"></script>`}
src="${CDN_URL}"
crossorigin="true"
type="text/javascript"
></script>`}
/> />
<div className="bg-yellow-50 dark:bg-yellow-900/20 px-4 py-3 rounded-lg mb-4 text-sm">
<span className="text-yellow-800 dark:text-yellow-200">
{' '}
{isZh
? '仅用于技术评估,Demo 模型有速率限制。'
: 'For evaluation only. Demo model has rate limits.'}
</span>
</div>
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-semibold w-28">
{isZh ? '位置' : 'Location'}
</th>
<th className="text-left py-2 px-3 font-semibold">URL</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-2 px-3">{isZh ? '全球' : 'Global'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_URL}</td>
</tr>
<tr>
<td className="py-2 px-3">{isZh ? '中国' : 'China'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_DEMO_CN_URL}</td>
</tr>
</tbody>
</table>
</section> </section>
<section className="mb-8"> {/* CDN Build Section */}
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg"> <section>
<h3 className="text-lg font-semibold mb-2 text-yellow-900 dark:text-yellow-300"> <h2 className="text-2xl font-bold mb-3">{isZh ? '📦 标准版' : '📦 Standard Build'}</h2>
{isZh ? '注意事项' : 'Notes'}
</h3> <p className="text-gray-600 dark:text-gray-300 mb-3">
<ul className="text-gray-600 dark:text-gray-300 space-y-1"> {isZh
<li> ? '将 PageAgent 类暴露到 globalThis,用法与 NPM 一致:'
{isZh ? '生产环境建议使用固定版本号' : 'Use fixed version number in production'} : 'Exposes PageAgent class to globalThis, usage identical to NPM:'}
</li> </p>
<li> {isZh ? '确保 HTTPS 环境下使用' : 'Ensure HTTPS environment'}</li>
<li> <CodeEditor
{' '} className="mb-4"
{isZh code={`<script src="${CDN_FULL_URL}" crossorigin="true"></script>
? '配置 CSP 策略允许脚本执行' <script>
: 'Configure CSP policy to allow script execution'} const agent = new PageAgent({
</li> model: 'deepseek-chat',
</ul> baseURL: 'https://api.deepseek.com',
</div> apiKey: 'YOUR_API_KEY',
})
await agent.execute('Click the submit button')
</script>`}
/>
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-semibold w-28">
{isZh ? '位置' : 'Location'}
</th>
<th className="text-left py-2 px-3 font-semibold">URL</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100 dark:border-gray-800">
<td className="py-2 px-3">{isZh ? '全球' : 'Global'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_FULL_URL}</td>
</tr>
<tr>
<td className="py-2 px-3">{isZh ? '中国' : 'China'}</td>
<td className="py-2 px-3 font-mono text-xs break-all">{CDN_FULL_CN_URL}</td>
</tr>
</tbody>
</table>
</section>
{/* Tips */}
<section className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
<h3 className="text-base font-semibold mb-2 text-blue-900 dark:text-blue-300">
💡 {isZh ? '提示' : 'Tips'}
</h3>
<ul className="text-gray-600 dark:text-gray-300 text-sm space-y-1">
<li> {isZh ? '生产环境推荐使用 NPM' : 'NPM is recommended for production'}</li>
<li> {isZh ? '生产环境建议锁定版本号' : 'Lock version number in production'}</li>
<li> {isZh ? '确保 HTTPS 环境' : 'Ensure HTTPS environment'}</li>
</ul>
</section> </section>
</div> </div>
) )
@@ -121,6 +121,13 @@ export default function Configuration() {
/** Viewport expansion in pixels (-1 for full page) */ /** Viewport expansion in pixels (-1 for full page) */
viewportExpansion?: number viewportExpansion?: number
/**
* Enable visual mask overlay during automation.
* Blocks user interaction while agent is running.
* Default: false for PageController, true for PageAgent.
*/
enableMask?: boolean
}`} }`}
/> />
</section> </section>
@@ -2,10 +2,11 @@ import { useTranslation } from 'react-i18next'
import BetaNotice from '@/components/BetaNotice' import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { CDN_CN_URL, CDN_URL } from '@/constants' import { CDN_DEMO_CN_URL, CDN_DEMO_URL } from '@/constants'
export default function QuickStart() { export default function QuickStart() {
const { t } = useTranslation('docs') const { t, i18n } = useTranslation('docs')
const isZh = i18n.language === 'zh-CN'
return ( return (
<div> <div>
@@ -16,64 +17,62 @@ export default function QuickStart() {
<h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2> <h2 className="text-2xl font-bold mb-3">{t('quick_start.installation')}</h2>
<div className="space-y-4 mb-6"> <div className="space-y-4 mb-6">
{/* Demo CDN - One Line */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg"> <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300"> <h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-blue-300">
{t('quick_start.step1_title')} {isZh ? '🚀 快速体验(Demo CDN' : '🚀 Quick Try (Demo CDN)'}
</h3> </h3>
<div className="space-y-3"> <div className="bg-yellow-50 dark:bg-yellow-900/20 p-2 rounded mb-3 text-sm">
<div> <span className="text-yellow-800 dark:text-yellow-200">
<p className="text-sm font-medium mb-2">{t('quick_start.step1_cdn')}</p> {isZh ? '仅用于技术评估' : 'For evaluation only'}
<CodeEditor </span>
code={`// CDN: \t${CDN_URL}
// Mirror: \t${CDN_CN_URL}
<script src="${CDN_URL}" crossorigin="true" type="text/javascript"></script>`}
language="html"
/>
</div>
<div>
<p className="text-sm font-medium mb-2">{t('quick_start.step1_npm')}</p>
<CodeEditor
code={`// npm install page-agent
import PageAgent from 'page-agent'`}
language="bash"
/>
</div>
</div> </div>
<CodeEditor
code={`// Global: ${CDN_DEMO_URL}
// China: ${CDN_DEMO_CN_URL}
<script src="${isZh ? CDN_DEMO_CN_URL : CDN_DEMO_URL}" crossorigin="true"></script>`}
language="html"
/>
</div> </div>
{/* NPM - Recommended */}
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg"> <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300"> <h3 className="text-lg font-semibold mb-2 text-green-900 dark:text-green-300">
{t('quick_start.step2_title')} {isZh ? '📦 NPM 安装(推荐)' : '📦 NPM Install (Recommended)'}
</h3> </h3>
<CodeEditor <CodeEditor
code={`// 仅供测试使用,生产环境需要配置 LLM 接入点,本工具不提供 LLM 服务 code={`npm install page-agent
// 测试接口
// @note: 限流,限制 prompt 内容,限制来源,随时变更,请替换成你自己的
// @note: 使用 DeepSeek-chat(3.2) 官方版本,使用协议和隐私策略见 DeepSeek 网站
const DEMO_MODEL = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const DEMO_BASE_URL = 'https://hwcxiuzfylggtcktqgij.supabase.co/functions/v1/llm-testing-proxy'
const DEMO_API_KEY = 'PAGE-AGENT-FREE-TESTING-RANDOM'
const agent = new PageAgent({ import { PageAgent } from 'page-agent'`}
model: DEMO_MODEL, language="bash"
baseURL: DEMO_BASE_URL,
apiKey: DEMO_API_KEY,
language: 'zh-CN'
})`}
language="javascript"
/> />
</div> </div>
<div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg"> <div className="p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300"> <h3 className="text-lg font-semibold mb-2 text-purple-900 dark:text-purple-300">
{t('quick_start.step2_title')}
</h3>
<CodeEditor
code={`const agent = new PageAgent({
model: 'deepseek-chat',
baseURL: 'https://api.deepseek.com',
apiKey: 'YOUR_API_KEY',
language: '${isZh ? 'zh-CN' : 'en-US'}'
})`}
language="javascript"
/>
</div>
<div className="p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<h3 className="text-lg font-semibold mb-2 text-orange-900 dark:text-orange-300">
{t('quick_start.step3_title')} {t('quick_start.step3_title')}
</h3> </h3>
<CodeEditor <CodeEditor
code={`// 程序化执行自然语言指令 code={`// ${isZh ? '程序化执行自然语言指令' : 'Execute natural language instructions programmatically'}
await pageAgent.execute('点击提交按钮,然后填写用户名为张三'); await agent.execute('${isZh ? '' : 'Click submit button, then fill username as John'}');
// 或者显示对话框让用户输入指令 // ${isZh ? '或者显示对话框让用户输入指令' : 'Or show panel for user to input instructions'}
pageAgent.panel.show() agent.panel.show()
`} `}
language="javascript" language="javascript"
/> />
+3 -3
View File
@@ -7,7 +7,7 @@ import DocsLayout from './pages/docs/Layout'
import Instructions from './pages/docs/features/custom-instructions/page' import Instructions from './pages/docs/features/custom-instructions/page'
import CustomTools from './pages/docs/features/custom-tools/page' import CustomTools from './pages/docs/features/custom-tools/page'
import DataMasking from './pages/docs/features/data-masking/page' import DataMasking from './pages/docs/features/data-masking/page'
import ModelIntegration from './pages/docs/features/model-integration/page' import Models from './pages/docs/features/models/page'
// Integration pages // Integration pages
import BestPractices from './pages/docs/integration/best-practices/page' import BestPractices from './pages/docs/integration/best-practices/page'
import CdnSetup from './pages/docs/integration/cdn-setup/page' import CdnSetup from './pages/docs/integration/cdn-setup/page'
@@ -80,11 +80,11 @@ export default function Router() {
</div> </div>
</Route> </Route>
<Route path="/docs/features/model-integration"> <Route path="/docs/features/models">
<div className="min-h-screen bg-white dark:bg-gray-900"> <div className="min-h-screen bg-white dark:bg-gray-900">
<Header /> <Header />
<DocsLayout> <DocsLayout>
<ModelIntegration /> <Models />
</DocsLayout> </DocsLayout>
</div> </div>
</Route> </Route>
+5
View File
@@ -1,12 +1,16 @@
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import { config as dotenvConfig } from 'dotenv' import { config as dotenvConfig } from 'dotenv'
import { readFileSync } from 'node:fs'
import process from 'node:process' import process from 'node:process'
import { dirname, resolve } from 'path' import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
const pageAgentPkg = JSON.parse(
readFileSync(resolve(__dirname, '../page-agent/package.json'), 'utf-8')
)
// Load .env from repo root // Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') }) dotenvConfig({ path: resolve(__dirname, '../../.env') })
@@ -32,5 +36,6 @@ export default defineConfig({
'import.meta.env.LLM_MODEL_NAME': JSON.stringify(process.env.LLM_MODEL_NAME), '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_API_KEY': JSON.stringify(process.env.LLM_API_KEY),
'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL), 'import.meta.env.LLM_BASE_URL': JSON.stringify(process.env.LLM_BASE_URL),
'import.meta.env.VERSION': JSON.stringify(pageAgentPkg.version),
}, },
}) })