Compare commits

...

173 Commits

Author SHA1 Message Date
Simon b6691b4732 chore(version): bump version to 1.6.0 2026-03-21 02:24:42 +08:00
Simon a557a85d28 Merge pull request #325 from alibaba/fix/action-into-iframes
fix(PageController): same-origin iframe actions
2026-03-21 02:20:13 +08:00
Simon eeb5b6a5af fix(clickElement): reorder iframe scroll and click actions 2026-03-21 02:18:42 +08:00
Simon ad31e2b750 chore: clarify current type handling in isHTMLElement 2026-03-21 02:16:51 +08:00
Simon e98d80b6a0 fix(PageController): same-origin iframe actions 2026-03-21 01:46:09 +08:00
Simon 93b1e52376 Merge pull request #322 from alibaba/feat/optional-toolchoice 2026-03-20 17:43:44 +08:00
Simon 66a85c0dd3 feat: option to disable named tool choice 2026-03-20 17:40:16 +08:00
Simon 50ce56a4f6 Merge pull request #321 from alibaba:chore/ext-rm-keydown
chore(ext): rm keydown event on history
2026-03-20 17:14:38 +08:00
Simon 5aeb40abac chore(ext): rm keydown event on history 2026-03-20 17:13:42 +08:00
Simon 8c06cdc898 Merge pull request #313 from Adonis0123/feature/239-history-export
feat(extension): export history sessions as json
2026-03-20 17:01:59 +08:00
Simon dfde89aa70 Merge branch 'main' into pr/Adonis0123/313 2026-03-20 16:58:13 +08:00
Simon 49b3316a5f Merge pull request #314 from Adonis0123/feat/issue-304-rerun-history-task
feat(ext): rerun tasks from history
2026-03-20 16:39:09 +08:00
Simon c35f367b39 feat(ext): style adjust; rm rerunDisabled 2026-03-20 16:36:23 +08:00
Simon 3459836a14 fix(PageController): lint error 2026-03-20 16:10:43 +08:00
Simon b7b5b6db30 Merge pull request #319 from alibaba/fix/missing-pointerout
fix(PageController): add `mouseleave` event
2026-03-20 16:05:37 +08:00
Simon 05d16313c7 fix(PageController): add mouseleave event 2026-03-20 15:54:36 +08:00
Simon a3a96d85d5 Merge pull request #210 from voidborne-d/fix/contenteditable-fallback
fix: add execCommand fallback for contenteditable input
2026-03-20 15:38:06 +08:00
Simon c89042f142 chore: wording 2026-03-20 15:36:10 +08:00
voidborne-d 2e18bd862d refactor: use const for planASucceeded, clarify LinkedIn comment 2026-03-20 07:08:45 +00:00
adonis 0bc47a997d feat(ext): rerun tasks from history 2026-03-19 23:54:08 +08:00
adonis bcc7dfea2d feat(extension): export history sessions as json 2026-03-19 23:46:58 +08:00
Simon ba242d3a1b Merge pull request #311 from alibaba/feat/llms-ak-always-optional
feat: optional AK
2026-03-19 20:08:15 +08:00
Simon fe82b11285 feat: AK optional 2026-03-19 19:50:05 +08:00
Simon bdf79b7c10 chore(version): bump version to 1.5.11 2026-03-18 20:45:19 +08:00
Simon 24eefdef23 chore(version): bump version to 1.5.10 2026-03-18 20:36:29 +08:00
Simon 252f550c04 style(ext): fix ai-motion overflow; wording; launcher info 2026-03-18 20:25:54 +08:00
Simon 628c249d45 fix(ext): init without initialTab throws error 2026-03-18 19:36:02 +08:00
Simon 61d598142d feat(ext): ask user approval for MCP task 2026-03-18 19:14:44 +08:00
Simon c0510b2235 Merge pull request #294 from octo-patch/feature/upgrade-minimax-m27
feat: upgrade MiniMax default model to M2.7
2026-03-18 15:21:19 +08:00
Octopus ca0005c825 feat: upgrade MiniMax default model to M2.7
- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model in code example
- Keep all previous models as alternatives
2026-03-18 02:12:58 -05:00
Simon 17e92b6200 chore(version): bump version to 1.5.9 2026-03-18 03:45:56 +08:00
Simon 2e0cbef034 Merge pull request #283 from alibaba/feat/mcp
feat: mcp (WIP)
2026-03-18 03:42:44 +08:00
Simon 95b6785f05 feat(mcp): mark MCP package as private 2026-03-18 03:37:02 +08:00
Simon 2917941b13 feat(mcp): add TODO for version checking 2026-03-18 03:31:38 +08:00
Simon 31c496cc65 docs(mcp): add development instructions for mcp 2026-03-18 03:15:02 +08:00
Simon cef39d5090 feat(mcp): MCP connector 2026-03-18 03:12:20 +08:00
Simon 3063b2a06d feat(ext): hub protocol 2026-03-18 02:59:28 +08:00
Simon d66a390516 chore(ext): add pub key to fix extension ID 2026-03-18 01:56:56 +08:00
Simon aae6c93f11 fix(ext): useAgent execute return result 2026-03-18 01:55:09 +08:00
Simon 2f5476b76c refactor(ext): mv ui components for later reuse 2026-03-17 19:11:13 +08:00
Simon 88ea2c6708 Merge pull request #221 from octo-patch/feature/add-minimax-provider
feat: add MiniMax model support with temperature clamping
2026-03-17 14:16:59 +08:00
Simon 37760a97df Merge pull request #274 from alibaba/dependabot/npm_and_yarn/development-dependencies-8ef9c9264a
chore(deps-dev): bump the development-dependencies group with 10 updates
2026-03-17 14:09:18 +08:00
dependabot[bot] de76d97303 chore(deps-dev): bump the development-dependencies group with 10 updates
Bumps the development-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.3` | `20.5.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.3` | `20.5.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.4.0` | `25.5.0` |
| [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react-swc) | `4.2.3` | `4.3.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.3.2` | `16.4.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.0` | `8.57.1` |
| [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) | `1.2.1` | `1.2.2` |
| [motion](https://github.com/motiondivision/motion) | `12.35.2` | `12.37.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.11.0` | `16.12.0` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.18` | `0.20.19` |


Updates `@commitlint/cli` from 20.4.3 to 20.5.0
- [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.5.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.3 to 20.5.0
- [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.5.0/@commitlint/config-conventional)

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

Updates `@vitejs/plugin-react-swc` from 4.2.3 to 4.3.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.0/packages/plugin-react-swc)

Updates `lint-staged` from 16.3.2 to 16.4.0
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.3.2...v16.4.0)

Updates `typescript-eslint` from 8.57.0 to 8.57.1
- [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.57.1/packages/typescript-eslint)

Updates `@wxt-dev/module-react` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/storage-v1.2.2/packages/module-react)

Updates `motion` from 12.35.2 to 12.37.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.35.2...v12.37.0)

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

Updates `wxt` from 0.20.18 to 0.20.19
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.18...wxt-v0.20.19)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@vitejs/plugin-react-swc"
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@wxt-dev/module-react"
  dependency-version: 1.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 19:36:05 +00:00
Simon 92196ac793 chore(website): tailwind important; footer 2026-03-17 00:29:13 +08:00
Simon 80dda37f8c fix: improve button cursor styles in UI components 2026-03-16 23:23:27 +08:00
Simon 5e7fe18463 docs(website): clean up 2026-03-16 23:14:48 +08:00
Simon 3d014f15e7 chore(version): bump version to 1.5.8 2026-03-16 22:52:32 +08:00
Simon ce8f0fdbd8 Merge pull request #273 from alibaba/feat/extension-control-curr-tab-group
feat(ext): initial controlled group
2026-03-16 22:51:51 +08:00
Simon 48fede38fd feat(ext): initialTab should be in controlled group; rm windowId; rm gray color 2026-03-16 20:51:32 +08:00
Simon f303ed5fc5 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-16 19:45:29 +08:00
Simon 6dbc33efd9 docs: limitation; readme; attribution 2026-03-16 19:45:14 +08:00
Simon 53628532df Merge pull request #251 from linked-danis/pr5-scrollintoview 2026-03-16 18:45:19 +08:00
Simon 2aae34d74a Merge pull request #250 from linked-danis/pr2-step-delay 2026-03-16 17:50:50 +08:00
Simon e27d1fb57f fix(style): Tailwind important flag 2026-03-16 17:35:32 +08:00
octo-patch 8ad4d19232 feat: add MiniMax model support with temperature clamping
Add MiniMax (M2.5 / M2.5-highspeed) as a tested model provider:

- Add model-specific patch in utils.ts: clamp temperature to (0, 1]
  since MiniMax API rejects temperature=0, and remove unsupported
  parallel_tool_calls parameter
- Add MiniMax to the tested models list on the Models documentation page
- Add MiniMax configuration example alongside existing providers
2026-03-15 23:42:30 +00:00
Simon 0fa7dc0531 chore(version): bump version to 1.5.7 2026-03-13 22:02:02 +08:00
linked-danis 89546887bd fix: type-safe scrollIntoViewIfNeeded
Add proper interface for WebKit extension method.

Changes:
- Add ScrollableElement interface
- Use typeof check instead of 'as any' cast
2026-03-13 14:22:47 +01:00
linked-danis b0bcba2922 feat: add stepDelay config option
Add configurable delay between agent steps.
Previously hardcoded to 0.4s.

Changes:
- Add stepDelay?: number to AgentConfig
- Use config value with 0.4s default
2026-03-13 14:22:37 +01:00
Simon 618ae8bf14 Merge pull request #236 from Wizard-Guido/fix/some-typos-and-grammar
fix: typos and grammar in system prompts and source code
2026-03-13 21:13:40 +08:00
Simon 5ed5d574fd Merge pull request #248 from alibaba/split/pr-207-simulator-mask-css 2026-03-13 21:07:26 +08:00
Simon bde9d9023a Merge pull request #247 from alibaba/split/pr-207-validate-url
fix: validate URL in fetchLlmsTxt
2026-03-13 21:05:10 +08:00
Simon c8a8858d24 fix(llms): edge cases 2026-03-13 21:03:29 +08:00
linked-danis 6491118cde refactor: SimulatorMask use CSS classes
Replace inline style.display with CSS class toggling.

Changes:
- Add .visible class to CSS module
- Use classList.add/remove instead of style.display

(cherry picked from commit 33465bbf52)
2026-03-13 20:44:34 +08:00
linked-danis b4acd02007 fix: validate URL in fetchLlmsTxt
Prevent crash on invalid URLs.

Changes:
- Wrap URL constructor in try/catch
- Return null for invalid URLs instead of throwing

(cherry picked from commit 8b0acf314d)
2026-03-13 20:44:14 +08:00
Simon 5695b3d6aa chore(website): add social media link; add star 2026-03-13 20:23:13 +08:00
Simon 0a160697d1 docs(terms): add Permitted Use Only 2026-03-13 19:16:20 +08:00
Wen 11d3ca3d6e fix: typos and grammar in system prompts and source code 2026-03-13 01:32:34 +00:00
Simon ee88b23891 Merge pull request #229 from alibaba/feat/seo
feat(website): basic SEO
2026-03-12 20:06:56 +08:00
Simon ca68be7a56 feat(website): basic SEO 2026-03-12 20:06:12 +08:00
Simon 4ad2abe997 docs: no-bots policy 2026-03-12 19:12:59 +08:00
Simon 8f08322721 chore: add security policy document 2026-03-12 17:06:16 +08:00
Simon fb043e5a81 Merge pull request #202 from Lubrsy706/fix/heuristic-elements-include-attributes
fix: extract attributes for heuristically-detected interactive elements
2026-03-12 02:20:58 +08:00
Simon 1628d48c97 chore: align doc styles 2026-03-12 02:20:24 +08:00
d 🔹 2f92a9cb32 fix: add execCommand fallback for contenteditable input (#168)
When typing into contenteditable elements (e.g. LinkedIn post editor),
the synthetic event approach (Plan A) may fail silently — the events
fire but the editor's internal state doesn't update, leaving the
element empty.

This adds an automatic fallback: after Plan A, we verify the text was
actually inserted by checking element.innerText. If it wasn't, we
fall back to execCommand('insertText') which integrates natively with
most rich-text editors including LinkedIn, Quill, and Slate.js.

The fallback uses proper Selection/Range API to select-all before
replacing, and preserves the undo stack since execCommand is handled
by the browser natively.

Fixes #168
2026-03-11 17:07:22 +00:00
Simon 025cb3391e chore: update README 2026-03-12 01:05:24 +08:00
Simon 4aab7f991f chore(version): bump version to 1.5.6 2026-03-12 00:53:10 +08:00
Simon 67846db28f chore(ext): ui layout 2026-03-12 00:52:19 +08:00
Simon cac55fa2bf Merge pull request #209 from alibaba/feat/extension-use-same-version-as-packages
feat: extension use the same version as packages
2026-03-12 00:41:58 +08:00
Simon a74b7542ba feat: extension use the same version as packages 2026-03-12 00:37:43 +08:00
Simon a9f462ed37 Merge pull request #191 from RinZ27/fix/ui-security-xss
fix(ui): escape array content in cards and fix double-escaping
2026-03-11 23:43:02 +08:00
Simon c33b3d8278 fix(ui): remove unnecessary tags 2026-03-11 23:38:57 +08:00
caibing de3a6e4660 fix: extract attributes for heuristically-detected interactive elements
Elements detected as interactive via heuristic methods (cursor:pointer
style, interactive class names, event listeners) had empty attributes
because `isInteractiveCandidate()` was used as the gate for attribute
extraction. This function only recognizes standard HTML tags and ARIA
attributes, missing heuristic detections.

After interactivity is confirmed by `isInteractiveElement()`, backfill
attributes for elements that were missed. This ensures
`includeAttributes` (e.g. `['class']`) works correctly for all
interactive elements, not just semantically standard ones.

Closes #124

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:38:36 +08:00
Simon e2c00b1bfc chore(ext): bump version to 0.1.17 2026-03-10 22:50:02 +08:00
Simon 692ac4acca chore(version): bump version to 1.5.5 2026-03-10 22:49:03 +08:00
RinZ27 c4bdd8fc4b fix(ui): escape array content in cards and fix double-escaping 2026-03-10 21:08:20 +07:00
Simon aa88221845 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-10 22:06:51 +08:00
Simon b425e48afe docs: guideline against AI-generated PRs 2026-03-10 22:06:41 +08:00
Simon ff54b363aa Merge pull request #179 from JasonOA888/fix/contenteditable-input-events
fix(page-controller): improve contenteditable input with proper events
2026-03-10 21:04:05 +08:00
Simon 4d931b4430 chore: comments; spell check 2026-03-10 21:02:48 +08:00
JasonOA888 6054ca1217 docs(page-controller): document contenteditable limitations and execCommand fallback
## Changes

Based on @gaomeng1900's comprehensive testing feedback:

1. **Document known limitations**
   - Slate.js and Draft.js do not work with synthetic events
   - Draft.js: Cannot be supported via DOM manipulation (by design, unmaintained)
   - Monaco/CodeMirror: Require direct JS instance access

2. **Clarify tested editors**
   - Works: Quill, LinkedIn, simple contenteditable editors
   - Does not work: Slate.js, Draft.js

3. **Preserve execCommand as fallback**
   - execCommand works better for LinkedIn, Quill, and Draft.js
   - Kept as commented fallback (deprecated API)
   - Users can uncomment if synthetic events don't work

Co-authored-by: gaomeng1900 <gaomeng1900@users.noreply.github.com>
2026-03-10 20:39:20 +08:00
Simon 7f9f0d1589 feat: simplify ContentEditable handling 2026-03-10 19:40:49 +08:00
Simon ddcfa5b499 Merge branch 'main' into fix/contenteditable-input-events 2026-03-10 17:37:20 +08:00
JasonOA888 441b41c713 fix(page-controller): address all Copilot review feedback
## Changes

1. **Fix early return bypassing cleanup**
   - Remove early return when beforeinput is canceled
   - Use shouldInsert flag to skip mutation but continue cleanup
   - Ensures waitFor and blurLastClickedElement always run

2. **Fix duplicate input events**
   - Contenteditable path now dispatches its own input events with inputType
   - Skip shared input dispatch for contenteditable
   - Prevents double-triggering framework listeners

3. **Fix event order (mutation before events)**
   - Clear content now dispatches beforeinput(inputType:deleteContent) first
   - Then performs the clear mutation
   - Then dispatches input event for the deletion
   - Proper event-mutation ordering

4. **Single-character keyboard events remain**
   - PR description clarified this is intentional
   - Multi-character input uses bulk insertion (not per-character typing)
   - Maintains semantic consistency

5. **Fix duplicate focusout event**
   - blur() already triggers native focusout
   - Removed manual focusout dispatch
   - Prevents double validation handlers

All changes follow Copilot's suggested fixes.
2026-03-10 16:03:25 +08:00
Simon ed71dd08b9 Merge pull request #181 from fancyboi999/fancy/fix-180-viewport-expansion
fix(page-controller): honor viewportExpansion in DOM extraction
2026-03-10 15:50:34 +08:00
fancy 1c354ab5d3 refactor(page-controller): remove viewportExpansion constants module 2026-03-10 15:34:24 +08:00
Simon 151760713a Merge pull request #177 from alibaba/dependabot/npm_and_yarn/development-dependencies-05afa3b718
chore(deps-dev): bump the development-dependencies group with 10 updates
2026-03-10 15:34:14 +08:00
Simon 4450d6302b docs: update guidelines, CLA should be optional 2026-03-10 15:23:19 +08:00
JasonOA888 2d055d3909 style: fix Prettier formatting 2026-03-10 15:10:17 +08:00
fancy 16da7d936d fix(page-controller): honor viewportExpansion in DOM extraction 2026-03-10 12:28:54 +08:00
JasonOA888 efe08f445f fix(page-controller): address Copilot review feedback
## Changes

1. **Check beforeinput cancellation**
   - dispatchEvent returns false if canceled
   - Check defaultPrevented as well
   - Abort mutation if event was canceled by any listener

2. **Fix event order to match real user typing**
   - Before: beforeinput -> mutation -> input -> keydown -> keyup
   - After: keydown -> beforeinput -> mutation -> input -> keyup
   - This matches typical browser event sequence

3. **Fix blur event semantics**
   - blur doesn't bubble; focusout does
   - Call editableElement.blur() to actually change focus
   - Dispatch focusout with bubbles:true for listeners
   - Then refocus

4. **Keep single-character keyboard events**
   - Already fixed in previous commit
   - Maintained here with correct order

All changes follow Copilot's suggested fixes.
2026-03-10 12:02:01 +08:00
JasonOA888 4e7f755ae9 fix(page-controller): address PR review feedback
## Changes

1. **Fix keyboard event semantics** (per review feedback)
   - Only dispatch keydown/keyup for single-character input
   - Avoids inconsistent event payloads for multi-character strings
   - Prevents confusion in editors that correlate key events with text changes

2. **Remove extra blank line**
   - Formatting consistency

Reviewer noted that dispatching key events with only the last character
of multi-character text creates semantic inconsistency with the actual
DOM mutation (which inserts the full string at once).

This fix follows the suggested change from the review.
2026-03-10 12:00:47 +08:00
JasonOA888 28bb2204e7 fix(page-controller): improve contenteditable input with proper events
## Problem
Input into contenteditable elements (like LinkedIn post editor) fails
because simply setting innerText does not trigger framework event listeners.

## Solution
Dispatch a full sequence of events that rich text editors expect:
- beforeinput (for React apps)
- input (standard)
- keydown/keyup (for keyboard listeners)
- change (for validation)
- blur + refocus (to trigger change detection)

## Testing
Tested on:
- LinkedIn post editor
- Draft.js editors
- Contenteditable divs with React listeners

Fixes #168
2026-03-10 10:32:07 +08:00
dependabot[bot] 0c3b4592b2 chore(deps-dev): bump the development-dependencies group with 10 updates
Bumps the development-dependencies group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `20.4.2` | `20.4.3` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `20.4.2` | `20.4.3` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.57.6` | `7.57.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.3` | `25.4.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.3.1` | `16.3.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.56.1` | `8.57.0` |
| [@wxt-dev/module-react](https://github.com/wxt-dev/wxt/tree/HEAD/packages/module-react) | `1.1.5` | `1.2.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.576.0` | `0.577.0` |
| [motion](https://github.com/motiondivision/motion) | `12.34.5` | `12.35.2` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.10.0` | `16.11.0` |


Updates `@commitlint/cli` from 20.4.2 to 20.4.3
- [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.4.3/@commitlint/cli)

Updates `@commitlint/config-conventional` from 20.4.2 to 20.4.3
- [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.4.3/@commitlint/config-conventional)

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

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

Updates `lint-staged` from 16.3.1 to 16.3.2
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.3.1...v16.3.2)

Updates `typescript-eslint` from 8.56.1 to 8.57.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.57.0/packages/typescript-eslint)

Updates `@wxt-dev/module-react` from 1.1.5 to 1.2.1
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/module-react/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/storage-v1.2.1/packages/module-react)

Updates `lucide-react` from 0.576.0 to 0.577.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.577.0/packages/lucide-react)

Updates `motion` from 12.34.5 to 12.35.2
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.34.5...v12.35.2)

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

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.57.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@wxt-dev/module-react"
  dependency-version: 1.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 0.577.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.35.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 20:33:42 +00:00
Simon 222bbef670 chore: update readme 2026-03-09 23:41:12 +08:00
Simon fa8ab451eb chore: update readme 2026-03-09 23:39:41 +08:00
Simon b4ebb2b98f chore(version): bump version to 1.5.4 2026-03-09 23:35:04 +08:00
Simon c757270101 Merge pull request #173 from alibaba/feat/tolerant-html-cleaning
feat: support wildcard in `includeAttributes`
2026-03-09 22:48:34 +08:00
Simon 5873e68d63 Merge pull request #175 from alibaba/copilot/sub-pr-173-again
perf(page-controller): cache compiled regexes in `globToRegex`
2026-03-09 22:34:58 +08:00
Simon 0c124c3b44 Merge pull request #176 from alibaba/copilot/sub-pr-173-another-one
refactor: use Object.keys() instead of for...in in matchAttributes
2026-03-09 22:33:56 +08:00
copilot-swe-agent[bot] d46a57f8ef refactor: use Object.keys() instead of for...in in matchAttributes
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:32:20 +00:00
Simon 153fa23a71 docs: update model list; give example for cookie auth 2026-03-09 22:31:35 +08:00
copilot-swe-agent[bot] 5852054e3a perf: cache compiled regexes in globToRegex to avoid repeated compilation
Co-authored-by: gaomeng1900 <10131203+gaomeng1900@users.noreply.github.com>
2026-03-09 14:29:56 +00:00
copilot-swe-agent[bot] 5988d4ba24 Initial plan 2026-03-09 14:29:10 +00:00
copilot-swe-agent[bot] 0b6a698f6b Initial plan 2026-03-09 14:26:53 +00:00
Simon 01db520881 feat: support wildcard in includeAttributes 2026-03-09 22:02:35 +08:00
Simon 30b9956c4f Merge pull request #172 from alibaba/fix/contenteditable
fix: contenteditable
2026-03-09 19:19:23 +08:00
Simon 4b37a3e538 feat(controller): add contenteditable to DEFAULT_INCLUDE_ATTRIBUTES 2026-03-09 19:15:51 +08:00
Simon 928d8d2fb3 fix: allow ContentEditable in inputTextElement; clean up code 2026-03-09 19:05:32 +08:00
Simon 8ece20f54b chore(ext): build script 2026-03-09 17:29:45 +08:00
Simon 26b4afca6a chore(ext): bump version to 0.1.16 2026-03-09 17:17:04 +08:00
Simon 5f43b76b57 chore: improve scripts for extension 2026-03-09 17:15:16 +08:00
Simon 15d609aebc chore(version): bump version to 1.5.3 2026-03-09 17:02:34 +08:00
Simon d66bf8f3cf docs: improve Contributing.md 2026-03-09 17:00:25 +08:00
Simon d3b42036af Merge pull request #169 from tsubasakong/lucas/opener-b-page-agent-167
fix(llms): avoid reasoning_effort for GPT-5.4 chat tools
2026-03-09 16:33:23 +08:00
Simon e448a43769 docs(terms): wording 2026-03-09 15:53:02 +08:00
Frank 349de7bf48 Merge remote-tracking branch 'upstream/main' into HEAD 2026-03-08 16:51:01 -07:00
tsubasakong eec601e6b2 fix(llms): avoid reasoning_effort for GPT-5.4 chat tools 2026-03-08 16:37:32 -07:00
Simon 5b280a4a67 docs: update README graph 2026-03-09 03:44:36 +08:00
Simon 46deaf373f docs(readme): add HN discussion link and star history 2026-03-08 00:18:48 +08:00
Simon e098d5701f docs(terms): wording 2026-03-07 23:57:09 +08:00
Simon 21d4be0020 docs(terms): wording 2026-03-07 23:51:14 +08:00
Simon 3f6a1856ac chore(ext): bump version; fix zip naming 2026-03-07 23:29:12 +08:00
Simon 816e24a102 Merge pull request #160 from alibaba/fix/extension-error-handling
fix: add button to clear saved configuration from the error boundary
2026-03-07 23:22:41 +08:00
Simon 80e2a93a8c feat: add button to clear saved configuration from the error boundary 2026-03-07 23:16:03 +08:00
Simon 2f8096883f docs(terms): wording for testing API terms 2026-03-07 04:50:34 +08:00
Simon 088f002583 chore(ext): bump version 2026-03-07 03:16:47 +08:00
Simon 27e9e78636 chore(version): bump version to 1.5.2 2026-03-07 03:08:33 +08:00
Simon a0c979602e feat: do not throw for webgl2 fail 2026-03-07 03:07:21 +08:00
Simon 3dc9edb8b5 fix(ext): ts types 2026-03-06 17:39:09 +08:00
Simon 32b68ab7c8 fix(docs): broken demo video 2026-03-06 16:07:22 +08:00
Simon 75a61c05c1 docs(terms): wording
Clarify the note regarding data processing location for the demo.
2026-03-06 04:33:03 +08:00
Simon bdfa98358b fix: requestIdleCallback on safari 2026-03-06 02:35:01 +08:00
Simon dc8b38ccb9 chore(ext): bump extension version to 0.1.11 2026-03-05 20:48:00 +08:00
Simon e1c288aaeb docs(changelog): 1.5.1 2026-03-05 20:44:26 +08:00
Simon 7a97de2a37 feat(ext): expose more config 2026-03-05 20:34:55 +08:00
Simon 19b03b83ea chore(version): bump version to 1.5.1 2026-03-05 19:28:19 +08:00
Simon 598b144d06 feat: change maxSteps to 40 2026-03-05 19:26:56 +08:00
Simon 1cd8a8de60 Merge pull request #158 from alibaba/fix/zod-v3
feat: import zod properly
2026-03-05 19:23:47 +08:00
Simon 9beffca985 docs: update doc for Zod v4 support 2026-03-05 19:15:03 +08:00
Simon 53db2069ce feat: zod support v3/4 2026-03-05 19:13:18 +08:00
Simon 121104e13d feat: truncate llms.txt results 2026-03-05 16:59:08 +08:00
Simon 0dacbda9da chore: improve logging 2026-03-05 16:54:41 +08:00
Simon 8bc27414a4 chore(version): bump version to 1.4.4 2026-03-05 16:25:16 +08:00
Simon 76509d93f0 feat: replace data-browser-use-ignore with data-page-agent-ignore 2026-03-05 16:24:50 +08:00
Simon ff31b0c03e chore: adjust website structure 2026-03-05 16:21:58 +08:00
Simon 6b2ab73d65 docs: model recommendation 2026-03-05 16:10:03 +08:00
Simon 2754023a80 chore(version): bump version to 1.4.3 2026-03-04 21:18:51 +08:00
Simon fa5c65db53 docs: update custom-tool docs 2026-03-04 21:17:32 +08:00
Simon 53c2c19d69 docs: update advanced docs; rm best-practice 2026-03-04 21:13:10 +08:00
Simon e5437b445a fix(core): Core and PageAgent use different config types; improve code structure 2026-03-04 20:05:48 +08:00
Simon 09bdf9ddaf feat(core): experimental support for llms.txt (#157)
* feat(core): experimental support for llms.txt

* docs: experimentalLlmsTxt
2026-03-04 18:53:24 +08:00
Simon b58d2a09ef Merge pull request #152 from alibaba/dependabot/npm_and_yarn/development-dependencies-7e2b43fd30
chore(deps-dev): bump the development-dependencies group with 7 updates
2026-03-03 22:04:35 +08:00
Simon ce442742ba Merge pull request #153 from alibaba/dependabot/npm_and_yarn/vite-plugin-css-injected-by-js-4.0.1
chore(deps-dev): bump vite-plugin-css-injected-by-js from 3.5.2 to 4.0.1
2026-03-03 22:04:17 +08:00
Simon 632f22962e docs: add recommended startup parameters for Ollama 2026-03-03 21:59:02 +08:00
dependabot[bot] 72c3b12fb4 chore(deps-dev): bump the development-dependencies group with 7 updates
Bumps the development-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.57.3` | `7.57.6` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.0` | `25.3.3` |
| [globals](https://github.com/sindresorhus/globals) | `17.3.0` | `17.4.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.2.7` | `16.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.575.0` | `0.576.0` |
| [motion](https://github.com/motiondivision/motion) | `12.34.3` | `12.34.4` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.9.0` | `16.10.0` |


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

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

Updates `globals` from 17.3.0 to 17.4.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.3.0...v17.4.0)

Updates `lint-staged` from 16.2.7 to 16.3.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.7...v16.3.1)

Updates `lucide-react` from 0.575.0 to 0.576.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.576.0/packages/lucide-react)

Updates `motion` from 12.34.3 to 12.34.4
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.34.3...v12.34.4)

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

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.57.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 0.576.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.34.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 13:58:04 +00:00
Simon 4807d550a3 chore(version): bump version to 1.4.2 2026-03-03 21:54:41 +08:00
Simon f9722f0619 Merge branch 'main' of https://github.com/alibaba/page-agent 2026-03-03 21:53:35 +08:00
Simon e1557a5d2e Merge pull request #156 from hobostay/main
fix(actions): remove debug console.log statements
2026-03-03 21:53:05 +08:00
Simon 06280c2ba5 chore(core): add wait time between steps for the page to react 2026-03-03 21:51:10 +08:00
Simon 645a7ceb52 feat(website): loading time optimize 2026-03-03 21:38:29 +08:00
hobostay a47c72ce84 fix(actions): remove debug console.log statements
Remove [SCROLL DEBUG] console.log statements that were left over
from development in scrollVertically and scrollHorizontally
functions. These debug statements should not be in production code.
2026-03-03 21:36:59 +08:00
Simon dcc6cd1cf3 feat(website): reduce white screen time 2026-03-03 21:05:16 +08:00
dependabot[bot] 5fec6846b0 chore(deps-dev): bump vite-plugin-css-injected-by-js from 3.5.2 to 4.0.1
Bumps [vite-plugin-css-injected-by-js](https://github.com/marco-prontera/vite-plugin-css-injected-by-js) from 3.5.2 to 4.0.1.
- [Release notes](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/releases)
- [Commits](https://github.com/marco-prontera/vite-plugin-css-injected-by-js/compare/v3.5.2...v4.0.1)

---
updated-dependencies:
- dependency-name: vite-plugin-css-injected-by-js
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 21:00:45 +00:00
111 changed files with 4730 additions and 2054 deletions
+1
View File
@@ -23,3 +23,4 @@ Closes #(issue)
## Requirements / 要求 ## Requirements / 要求
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。 - [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
- [ ] This PR is NOT generated by a bot or AI agent acting autonomously. I have authored or meaningfully reviewed every change. / 此 PR 不是由 bot 或 AI 自主生成的,我已亲自编写或充分审查了每一处变更。
+1
View File
@@ -1,5 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"contenteditable",
"deepseek", "deepseek",
"historychange", "historychange",
"HITL", "HITL",
+33 -41
View File
@@ -1,21 +1,23 @@
# Contributing to Page-Agent # Contributing to PageAgent
Thank you for your interest in contributing to Page-Agent! We welcome contributions from everyone. ♥️ We welcome contributions from everyone.
## 🚀 Quick Start ## 🚀 Quick Start
### Development Setup ### Development Setup
1. **Prerequisites** 1. **Prerequisites**
- `macOS` / `Linux` / `WSL`
- `node.js >= 20` with `npm >= 10` - `node.js >= 20` with `npm >= 10`
- An editor that supports `ts/eslint/prettier` - An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well - Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
2. **Setup** 2. **Setup**
```bash ```bash
npm ci npm i
npm start # Start demo and documentation site npm start # Start demo and documentation site
npm run build # Build libs and website
``` ```
### Project Structure ### Project Structure
@@ -27,11 +29,11 @@ This is a **monorepo** with npm workspaces containing **4 main packages**:
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation - **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website` - **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required. > We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
>
- When developing. Use alias so that we don't have to pre-build. > - When developing. Use alias so that we don't have to pre-build.
- When bundling. Use external and disable ts `paths` alias to leave deps out. > - When bundling. Use external and disable ts `paths` alias.
- When bundling `IIFE` and `Website`. Bundle everything including local packages. > - When bundling `IIFE` and `Website`. Bundle everything together.
## 🤝 How to Contribute ## 🤝 How to Contribute
@@ -63,6 +65,7 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
- Update documentation as needed - Update documentation as needed
4. **Test Your Changes** 4. **Test Your Changes**
- Build and lint everything.
- Test in our demo website - Test in our demo website
- Test it on other websites if applicable - Test it on other websites if applicable
- `@TODO: test suite` - `@TODO: test suite`
@@ -90,14 +93,17 @@ We use a simplified monorepo solution with `native npm-workspace + ts reference
- Keep functions small and focused - Keep functions small and focused
- Add JSDoc for public APIs - Add JSDoc for public APIs
### Vibe coding with AI ### Vibe Coding with AI
- It's **recommended** to heavily rely on AI (aka "vibe coding") when maintaining **demo pages and tests**. > [Vibe coding](https://en.wikipedia.org/wiki/Vibe_coding)
- Be very careful if AI ever touched the core lib!!!
- Vibe coding is **RECOMMENDED** when maintaining **the demo, the website, the UI and tests**.
- We have a [website/AGENTS.md](packages/website/AGENTS.md) for that.
- Vibe coding is **NOT** allowed for the core lib!!!
- NEVER try to vibe coding the MV3 extension!!! It is HELL.
- Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI. - Review anything AI wrote before make a commit. You are the author of anything you commit. NOT AI.
- Update the [AI instructions](AGENTS.md) when structure changed.
If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it: If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
- claude-code (`CLAUDE.md`) - claude-code (`CLAUDE.md`)
@@ -135,26 +141,21 @@ If your lame AI assistant does not support [AGENTS.md](https://agents.md/). Add
LLM_MODEL_NAME="qwen3:14b" LLM_MODEL_NAME="qwen3:14b"
``` ```
> ⚠️ Add `*` to `OLLAMA_ORIGINS` (403). Models < 10B unlikely strong enough. Requires tool_call support. Set context length > 15k (default 4k will NOT work): `$env:OLLAMA_CONTEXT_LENGTH=64000; ollama serve` > @see https://alibaba.github.io/page-agent/docs/features/models#ollama for configuration
- Restart the dev server to load new env vars - **Restart the dev server** to load new env vars
- If not provided, the demo will the free testing proxy by default - If not provided, the demo will use the free testing proxy by default. By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
### Website Development
```bash
npm start
```
### Extension Development ### Extension Development
```bash ```bash
# make sure you ran `npm run build:libs` first
# and every time you changed the core libs
npm run dev -w @page-agent/ext npm run dev -w @page-agent/ext
npm run zip -w @page-agent/ext npm run zip -w @page-agent/ext
``` ```
- Load extension in Chrome via `chrome://extensions` -> **Load unpacked** - Update `packages/extension/docs/extension_api.md` for API integration details
- Use `packages/extension/docs/extension_api.md` (EN) or `packages/extension/docs/extension_api_zh.md` (ZH) for API integration details
### Testing on Other Websites ### Testing on Other Websites
@@ -172,7 +173,7 @@ npm run zip -w @page-agent/ext
- Click the bookmark on any page to load Page-Agent - Click the bookmark on any page to load Page-Agent
> Warning: AK in your local `.env` will be inlined in the iife script. > Warning: AK in your local `.env` will be inlined in the iife script. Be very careful when you distribute the script.
### Adding Documentation ### Adding Documentation
@@ -180,29 +181,20 @@ Ask an AI to help you add documentation to the `website/` package. Follow the ex
> Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated. > Our AGENTS.md file and guardrails are designed for this purpose. But please be careful and review anything AI generated.
## 🎯 Contribution Areas
We especially welcome contributions in:
- **Browser compatibility** improvements
- **Performance optimizations** for DOM processing
- **Documentation** and examples
- **Testing** and quality assurance
- **Accessibility** features
- **Internationalization** support
## 🚫 What We Don't Accept ## 🚫 What We Don't Accept
- Changes that break existing API compatibility (Discuss first) - Breaking changes and large PRs without prior discussion
- Heavy dependencies to core library - Heavy dependencies to core libs
- Contributions without proper testing - Contributions without proper testing
- Code that doesn't follow project conventions - Code that doesn't follow project conventions
- Dependencies or code with licenses incompatible with MIT
- Bot or AI-generated pull requests without meaningful human involvement
## 📄 Legal ## 📄 Legal
By contributing to this project, you agree that your contributions will be licensed under the MIT License. By contributing to this project, you agree that your contributions will be licensed under the MIT License.
> You may need to sign a github CLA before you create a PR. > CLA is optional.
## 💬 Questions? ## 💬 Questions?
@@ -210,4 +202,4 @@ By contributing to this project, you agree that your contributions will be licen
- Check existing documentation and issues first - Check existing documentation and issues first
- Be respectful and constructive in discussions - Be respectful and constructive in discussions
Thank you for helping make Page-Agent better! 🎉 Thank you for helping make PageAgent better! 🎉
+2 -2
View File
@@ -1,7 +1,7 @@
MIT License MIT License
Copyright (c) 2026 Alibaba Copyright (c) 2026 SimonLuvRamen
Copyright (c) 2026 Simon Copyright (c) 2026 Alibaba Group Holding Limited
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+22 -16
View File
@@ -5,15 +5,15 @@
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png"> <img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture> </picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language. The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
🌐 **English** | [中文](./docs/README-zh.md) 🌐 **English** | [中文](./docs/README-zh.md)
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a> <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video> <video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
--- ---
@@ -22,20 +22,18 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
- **🎯 Easy integration** - **🎯 Easy integration**
- No need for `browser extension` / `python` / `headless browser`. - No need for `browser extension` / `python` / `headless browser`.
- Just in-page javascript. Everything happens in your web page. - Just in-page javascript. Everything happens in your web page.
- The best tool for your agent to control web pages.
- **📖 Text-based DOM manipulation** - **📖 Text-based DOM manipulation**
- No screenshots. No OCR or multi-modal LLMs needed. - No screenshots. No multi-modal LLMs or special permissions needed.
- No special permissions required.
- **🧠 Bring your own LLMs** - **🧠 Bring your own LLMs**
- **🎨 Pretty UI with human-in-the-loop** - **🎨 Pretty UI with human-in-the-loop**
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.** - **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
## 💡 Use Cases ## 💡 Use Cases
- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite needed. - **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems. - **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier. - **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
- **Multi-page Agent** — Extend your agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension). - **Multi-page Agent** — Extend your own agent's reach across browser tabs with the optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
## 🚀 Quick Start ## 🚀 Quick Start
@@ -47,12 +45,12 @@ Fastest way to try PageAgent with our free Demo LLM:
<script src="{URL}" crossorigin="true"></script> <script src="{URL}" crossorigin="true"></script>
``` ```
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
| Mirrors | URL | | Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- | | ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.4.1/dist/iife/page-agent.demo.js | | Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.4.1/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
### NPM Installation ### NPM Installation
@@ -77,10 +75,12 @@ For more programmatic usage, see [📖 Documentations](https://alibaba.github.io
## 🤝 Contributing ## 🤝 Contributing
We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup and local development. We welcome contributions from the community! Follow our instructions in [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines.
Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing. Please read [Code of Conduct](docs/CODE_OF_CONDUCT.md) before contributing.
Contributions generated entirely by bots or agents without substantial human involvement will not be accepted, and bot accounts may be blocked.
## 👏 Acknowledgments ## 👏 Acknowledgments
This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**. This project builds upon the excellent work of **[`browser-use`](https://github.com/browser-use/browser-use)**.
@@ -90,12 +90,10 @@ This project builds upon the excellent work of **[`browser-use`](https://github.
``` ```
DOM processing components and prompt are derived from browser-use: DOM processing components and prompt are derived from browser-use:
Browser Use Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License Licensed under the MIT License
Original browser-use project: <https://github.com/browser-use/browser-use>
We gratefully acknowledge the browser-use project and its contributors for their We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make excellent work on web automation and DOM interaction patterns that helped make
this project possible. this project possible.
@@ -111,3 +109,11 @@ file and in the node_modules directory after installation.
--- ---
**⭐ Star this repo if you find PageAgent helpful!** **⭐ Star this repo if you find PageAgent helpful!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
+50
View File
@@ -0,0 +1,50 @@
# Security Policy
## Supported Versions
We provide security fixes on a best-effort basis for:
| Version | Supported |
| --------------------------------------------------------- | --------- |
| `main` | Yes |
| Latest npm release of `page-agent` and workspace packages | Yes |
| Older releases | No |
Please upgrade to the latest release before reporting an issue against an older build.
## Reporting a Vulnerability
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
Use GitHub's private vulnerability reporting flow:
- Open https://github.com/alibaba/page-agent/security/policy
- Click `Report a vulnerability`
If private reporting is unavailable, open a minimal public issue only to request a private contact channel. Do not include exploit details.
## What to Include
- Affected package or feature
- Exact version, commit, or build
- Browser, OS, and runtime environment
- Reproduction steps or a proof of concept
- Expected impact
## Scope
We prioritize reports that show a real security boundary failure, such as:
- Unauthorized access to data, tokens, or extension capabilities
- Bypassing explicit safety constraints
- Sensitive data exposure caused by default behavior
The following usually do not qualify by themselves:
- Unsafe custom integrations that ignore documented safeguards
- Intentionally embedding secrets into client-side builds
- Reports against unsupported older versions
## Disclosure
Please avoid public disclosure until maintainers have had a reasonable chance to investigate and ship a fix.
+31 -35
View File
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.1] - 2026-03-05
### Breaking Changes
- **`data-browser-use-ignore``data-page-agent-ignore`** - DOM ignore attribute renamed to match the project identity
- **Config types restructured** - `PageAgentConfig` split into `AgentConfig` + `PageAgentCoreConfig`; config definitions moved from `config/index.ts` to `types.ts`
- **Zod v3/v4 dual support** - Libraries now accept both `zod@^3.25` and `zod@^4.0` as peer dependencies
### Features
- **Experimental `llms.txt` support** - Agent can fetch and include a site's `llms.txt` in context. Enable via `experimentalLlmsTxt: true`
### Improvements
- Default `maxSteps` changed from 20 to 40 for better for complex tasks out of the box
- Added 400ms wait between agent steps for page reactions
- Increased click wait time (100ms → 200ms) for more reliable interactions
- Removed debug `console.log` statements from scroll actions
- Reset observations on new task start
- Improved logging across packages
### Extension v0.1.9
> PageAgent 1.5.1
- **Advanced config panel** - New collapsible section exposing Max Steps, System Instruction, and experimental `llms.txt` toggle
- Streamlined User Auth Token description
- Moved testing API notice below auth token section
---
## [1.4.0] - 2026-02-27 ## [1.4.0] - 2026-02-27
### Features ### Features
@@ -122,41 +153,6 @@ PageAgent is now ready for production use. The API is stable and breaking change
- **Ask User Tool** - Agent can ask users for clarification - **Ask User Tool** - Agent can ask users for clarification
- **i18n Support** - English and Chinese localization - **i18n Support** - English and Chinese localization
### Configuration
```typescript
interface PageAgentConfig {
// LLM Configuration (required)
baseURL: string
apiKey: string
model: string
temperature?: number
maxRetries?: number
customFetch?: typeof fetch
// Agent Configuration
language?: 'en-US' | 'zh-CN'
maxSteps?: number // default: 20
customTools?: Record<string, PageAgentTool> // experimental
instructions?: InstructionsConfig
transformPageContent?: (content: string) => string | Promise<string>
experimentalScriptExecutionTool?: boolean // default: false
// Lifecycle Hooks (experimental)
onBeforeTask?: (agent, result) => void
onAfterTask?: (agent, result) => void
onBeforeStep?: (agent, stepCount) => void
onAfterStep?: (agent, history) => void
onDispose?: (agent, reason?) => void
// Page Controller Configuration
enableMask?: boolean // default: true
viewportExpansion?: number
interactiveBlacklist?: Element[]
interactiveWhitelist?: Element[]
}
```
### Packages ### Packages
| Package | Description | | Package | Description |
+21 -17
View File
@@ -5,15 +5,15 @@
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png"> <img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
</picture> </picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent) [![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。 纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
🌐 [English](../README.md) | **中文** 🌐 [English](../README.md) | **中文**
👉 <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Documentation</b></a> <a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
<video id="demo-video" src="https://github.com/user-attachments/assets/11aed429-b69c-49d5-8982-fa99b4a0e9a8" controls crossorigin muted></video> <video id="demo-video" src="https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2" controls crossorigin muted></video>
--- ---
@@ -22,20 +22,18 @@
- **🎯 轻松集成** - **🎯 轻松集成**
- 无需 `浏览器插件` / `Python` / `无头浏览器` - 无需 `浏览器插件` / `Python` / `无头浏览器`
- 纯页面内 JavaScript,一切都在你的网页中完成。 - 纯页面内 JavaScript,一切都在你的网页中完成。
- The best tool for your agent to control web pages.
- **📖 基于文本的 DOM 操作** - **📖 基于文本的 DOM 操作**
- 无需截图,无需 OCR 或多模态模型。 - 无需截图,无需多模态模型或特殊权限
- 无需特殊权限。
- **🧠 用你自己的 LLM** - **🧠 用你自己的 LLM**
- **🎨 精美 UI,支持人机协同** - **🎨 精美 UI,支持人机协同**
- **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。** - **🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务。**
## 💡 应用场景 ## 💡 应用场景
- **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,不需要重写后端。 - **SaaS AI 副驾驶** — 几行代码为你的产品加上 AI 副驾驶,无需重写后端。
- **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。 - **智能表单填写** — 把 20 次点击变成一句话。ERP、CRM、管理后台的最佳拍档。
- **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。 - **无障碍增强** — 用自然语言让任何网页无障碍。语音指令、屏幕阅读器,零门槛。
- **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你的 Agent 跨标签页工作。 - **跨页面 Agent** — 通过可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),让你自己的 Agent 跨标签页工作。
## 🚀 快速开始 ## 🚀 快速开始
@@ -47,12 +45,12 @@
<script src="{URL}" crossorigin="true"></script> <script src="{URL}" crossorigin="true"></script>
``` ```
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
| Mirrors | URL | | Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- | | ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.4.1/dist/iife/page-agent.demo.js | | Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.4.1/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js |
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
### NPM 安装 ### NPM 安装
@@ -77,9 +75,9 @@ await agent.execute('点击登录按钮')
## 🤝 贡献 ## 🤝 贡献
欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解环境配置和本地开发说明 欢迎社区贡献!请参阅 [CONTRIBUTING.md](../CONTRIBUTING.md) 了解安装与贡献指南。请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md)
请在贡献前阅读[行为准则](CODE_OF_CONDUCT.md) 我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码,机器人账号可能被禁止参与互动
## 👏 致谢 ## 👏 致谢
@@ -90,12 +88,10 @@ await agent.execute('点击登录按钮')
``` ```
DOM processing components and prompt are derived from browser-use: DOM processing components and prompt are derived from browser-use:
Browser Use Browser Use <https://github.com/browser-use/browser-use>
Copyright (c) 2024 Gregor Zunic Copyright (c) 2024 Gregor Zunic
Licensed under the MIT License Licensed under the MIT License
Original browser-use project: <https://github.com/browser-use/browser-use>
We gratefully acknowledge the browser-use project and its contributors for their We gratefully acknowledge the browser-use project and its contributors for their
excellent work on web automation and DOM interaction patterns that helped make excellent work on web automation and DOM interaction patterns that helped make
this project possible. this project possible.
@@ -111,3 +107,11 @@ file and in the node_modules directory after installation.
--- ---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!** **⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
<a href="https://www.star-history.com/?repos=alibaba%2Fpage-agent&type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&theme=dark&legend=top-left&v=7" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=alibaba/page-agent&type=date&legend=top-left&v=7" />
</picture>
</a>
+22 -20
View File
@@ -1,42 +1,44 @@
# Terms of Use & Privacy # Terms of Use & Privacy
**Last updated:** February 2026 **Last updated:** March 2026
"We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). This document covers the Page Agent software itself and the testing services we provide — **not** any third-party product or service built with it. "We" in this document refers to the maintainers of the open-source Page Agent project (https://github.com/alibaba/page-agent). "The software" refers to Page Agent (the JavaScript library) and Page Agent Ext (the browser extension). This document covers the software itself and the testing API we provide — **not** any third-party product or service built with it.
--- ---
## 1. Open Source Software Privacy ## 1. Open Source Software Privacy
Page Agent (PageAgent.js and Page Agent Extension) is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and the maintainers do **not** have access to your browsing activity, page content, or task instructions through the software. The software is a **client-side only** tool with a "Bring Your Own Key" (BYOK) architecture. The software itself does **not** include any backend service. The software does **not** collect or transmit any user data on its own, and we do **not** have access to your browsing activity, page content, or task instructions through the software.
All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data. All data transmission occurs **only** between your browser and the LLM provider you configure. You are in full control of which provider receives your data.
- You choose which LLM provider to use The project is open source under the [MIT License](https://github.com/alibaba/page-agent/blob/main/LICENSE) and can be audited at: https://github.com/alibaba/page-agent
- You may configure your own API endpoint at any time
- The project is open source and can be audited: https://github.com/alibaba/page-agent
--- ---
## 2. Testing API and Demo Disclaimer & Terms of Use ## 2. Testing API and Demo Disclaimer & Terms of Use
To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of page-agent. To facilitate easy testing and technical evaluation, we provide a free testing LLM API. This API is used in the project homepage's live demo, the pre-built demo CDN bundles, and the browser extension's default configuration. Users may also use it independently for their own technical evaluation of the software.
This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this service, you agree to the following terms: This free testing API is provided **strictly for technical evaluation and R&D purposes only**. It must not be used in any production environment. By using this API, you agree to the following terms:
**No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information. - **Permitted Use Only**: This API must be used solely for technical evaluation of the software. Any other use — including integration into other products or services, unlawful activities, violation of the underlying LLM provider's usage policies, or automated scraping at scale — is strictly prohibited.
**Zero Data Retention**: We do not store, log, or use your prompts or webpage data (DOM) for model training. All data is processed in-transit and immediately discarded. However, we do temporarily process necessary, identity-agnostic network data (such as IP addresses) solely for rate-limiting and anti-abuse purposes. - **No Sensitive Data**: You are strictly prohibited from inputting any Personal Identifiable Information (PII), confidential business data, financial/medical records, or using this agent on web pages containing such sensitive information.
**Independent Infrastructure**: The `page-agent` open-source software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and no built-in backend. To facilitate easy testing, the maintainers have independently purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) FC and BaiLian Qwen models). This project is not affiliated with, nor endorsed by, Alibaba Cloud. - **Data Processing**: We do not store or log your prompts, webpage data (HTML), or any submitted content, nor do we use such data for model training. All data is processed in-transit and immediately discarded. We perform in-memory request validation to prevent abuse of the testing API, and temporarily process IP addresses for rate-limiting purposes. No data from these processes is retained. Data is processed through Alibaba Cloud infrastructure, which is subject to its own privacy policy.
**No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice. - **Independent Infrastructure**: The software is completely frontend-based with a "Bring Your Own Key" (BYOK) architecture and **no built-in backend**. To facilitate easy testing, the maintainers have purchased public cloud services from Alibaba Cloud China ([aliyun.com](https://www.aliyun.com) Function Compute and BaiLian Qwen models). This project is not a product of, nor endorsed by, Alibaba Cloud.
**"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this demo. - **No Guaranteed Availability**: This testing API may be rate-limited, degraded, or discontinued at any time without prior notice.
**Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the default BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama). - **"AS IS" & Limitation of Liability**: This service is provided strictly on an "AS IS" and "AS AVAILABLE" basis, without any warranties. The maintainers bear no liability for any data loss, service interruption, or legal consequences arising from your use of this service.
**Note**: This service processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this demo. - **Recommendation for Real Usage**: For secure and continuous usage, we strongly advise using the BYOK mode with your own legally compliant commercial LLM API keys, or connecting to local, offline models (e.g., Ollama).
**Note**: This free testing LLM API processes data via servers located in Mainland China. If you are located in a region with strict data localization laws (such as the EU/EEA), please do not use this API.
**Age Requirement**: The software and testing API are not intended for use by individuals under the age of 13 (or the minimum age of digital consent in your jurisdiction).
--- ---
@@ -49,19 +51,19 @@ The extension performs DOM analysis and automation actions **locally in your bro
Data is transmitted to external servers **only when you initiate an automation task**. When this occurs: Data is transmitted to external servers **only when you initiate an automation task**. When this occurs:
- Your task instructions (natural language commands) - Your task instructions (natural language commands)
- Simplified page structure (cleaned DOM) of all pages under the extension's control - Simplified page structure (cleaned HTML) of all pages under the extension's control
are sent to the LLM API endpoint configured in **your settings**. are sent to the LLM API endpoint configured in **your settings**.
> **Note:** The DOM cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks. > **Note:** The HTML cleaning process simplifies page structure for AI readability but **does not guarantee removal of sensitive information** (e.g., visible text, form values, or personal data on the page). Please be mindful of the page content when initiating tasks.
**If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply. **If you configure a third-party LLM provider** (e.g., OpenAI, Anthropic, or others), data is sent directly to that provider. Their privacy policies apply.
**If you use the built-in testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms. **If you use the testing API**, the terms in [Section 2](#2-testing-api-and-demo-disclaimer--terms-of-use) apply. By using the extension with the default testing API, you agree to those terms.
### Data Storage ### Data Storage
- **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` - **Local storage only**: Your configuration (API endpoint, API key, model selection) is stored in your browser via `chrome.storage.local` (or equivalent browser storage APIs)
- **No cloud sync**: Configuration is not synced to any external server - **No cloud sync**: Configuration is not synced to any external server
- **No analytics**: The extension does not include any analytics or tracking code - **No analytics**: The extension does not include any analytics or tracking code
@@ -76,7 +78,7 @@ are sent to the LLM API endpoint configured in **your settings**.
## Changes ## Changes
We may update these terms as the project evolves. We may update these terms at our discretion.
## Contact ## Contact
+1387 -717
View File
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -1,7 +1,7 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "1.4.1", "version": "1.6.0",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
@@ -9,6 +9,7 @@
"packages/llms", "packages/llms",
"packages/core", "packages/core",
"packages/page-agent", "packages/page-agent",
"packages/mcp",
"packages/extension", "packages/extension",
"packages/website" "packages/website"
], ],
@@ -24,25 +25,27 @@
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^20.19.0 || ^22.13.0 || >=24"
}, },
"scripts": { "scripts": {
"cleanup": "rm -rf packages/*/dist",
"start": "npm run dev --workspace=@page-agent/website", "start": "npm run dev --workspace=@page-agent/website",
"build:website": "npm run build:website --workspace=@page-agent/website", "dev:ext": "npm run dev -w @page-agent/ext",
"build:libs": "npm run build --workspaces --if-present",
"build": "npm run build:libs && npm run build:website",
"dev:demo": "npm run dev:demo --workspace=page-agent", "dev:demo": "npm run dev:demo --workspace=page-agent",
"build": "npm run build:libs && npm run build:website",
"build:libs": "npm run build --workspaces --if-present",
"build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
"version": "node scripts/sync-version.js", "version": "node scripts/sync-version.js",
"lint": "eslint .", "lint": "eslint .",
"cleanup": "rm -rf packages/*/dist",
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.4.2", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.4.2", "@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@microsoft/api-extractor": "^7.57.3", "@microsoft/api-extractor": "^7.57.7",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.3.0", "@types/node": "^25.5.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.3.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
@@ -52,15 +55,15 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-react-x": "^2.13.0", "eslint-plugin-react-x": "^2.13.0",
"globals": "^17.0.0", "globals": "^17.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.4", "lint-staged": "^16.4.0",
"prettier": "^3.8.0", "prettier": "^3.8.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.56.1", "typescript-eslint": "^8.57.1",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^4.0.1",
"vite-bundle-analyzer": "^1.3.6" "vite-bundle-analyzer": "^1.3.6"
}, },
"overrides": { "overrides": {
+9 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/core", "name": "@page-agent/core",
"private": false, "private": false,
"version": "1.4.1", "version": "1.6.0",
"type": "module", "type": "module",
"main": "./dist/esm/page-agent-core.js", "main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js", "module": "./dist/esm/page-agent-core.js",
@@ -44,8 +44,13 @@
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
"zod": "^4.3.5", "@page-agent/llms": "1.6.0",
"@page-agent/llms": "1.4.1", "@page-agent/page-controller": "1.6.0"
"@page-agent/page-controller": "1.4.1" },
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5"
} }
} }
+28 -17
View File
@@ -1,18 +1,18 @@
/** /**
* Copyright (C) 2025 Alibaba Group Holding Limited * Copyright (C) 2025 Alibaba Group Holding Limited
* Copyright (C) 2026 SimonLuvRamen
* All rights reserved. * All rights reserved.
*/ */
import { InvokeError, LLM, type Tool } from '@page-agent/llms' import { InvokeError, LLM, type Tool } from '@page-agent/llms'
import type { BrowserState, PageController } from '@page-agent/page-controller' import type { BrowserState, PageController } from '@page-agent/page-controller'
import chalk from 'chalk' import chalk from 'chalk'
import * as z from 'zod' import * as z from 'zod/v4'
import { type PageAgentConfig, type SupportedLanguage } from './config'
import { DEFAULT_MAX_STEPS } from './config/constants'
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw' import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
import { tools } from './tools' import { tools } from './tools'
import type { import type {
AgentActivity, AgentActivity,
AgentConfig,
AgentReflection, AgentReflection,
AgentStatus, AgentStatus,
AgentStepEvent, AgentStepEvent,
@@ -21,13 +21,13 @@ import type {
MacroToolInput, MacroToolInput,
MacroToolResult, MacroToolResult,
} from './types' } from './types'
import { assert, normalizeResponse, uid, waitFor } from './utils' import { assert, fetchLlmsTxt, normalizeResponse, uid, waitFor } from './utils'
export { type PageAgentConfig }
export type { SupportedLanguage }
export { tool, type PageAgentTool } from './tools' export { tool, type PageAgentTool } from './tools'
export type * from './types' export type * from './types'
export type PageAgentCoreConfig = AgentConfig & { pageController: PageController }
/** /**
* AI agent for browser automation. * AI agent for browser automation.
* *
@@ -60,7 +60,7 @@ export type * from './types'
*/ */
export class PageAgentCore extends EventTarget { export class PageAgentCore extends EventTarget {
readonly id = uid() readonly id = uid()
readonly config: PageAgentConfig & { maxSteps: number } readonly config: PageAgentCoreConfig & { maxSteps: number }
readonly tools: typeof tools readonly tools: typeof tools
/** PageController for DOM operations */ /** PageController for DOM operations */
readonly pageController: PageController readonly pageController: PageController
@@ -94,10 +94,10 @@ export class PageAgentCore extends EventTarget {
browserState: null as BrowserState | null, browserState: null as BrowserState | null,
} }
constructor(config: PageAgentConfig & { pageController: PageController }) { constructor(config: PageAgentCoreConfig) {
super() super()
this.config = { ...config, maxSteps: config.maxSteps || DEFAULT_MAX_STEPS } this.config = { ...config, maxSteps: config.maxSteps ?? 40 }
this.#llm = new LLM(this.config) this.#llm = new LLM(this.config)
this.tools = new Map(tools) this.tools = new Map(tools)
@@ -177,7 +177,7 @@ export class PageAgentCore extends EventTarget {
} }
/** /**
* Push a observation message to the history event stream. * Push an observation message to the history event stream.
* This will be visible in <agent_history> and remain persistent in memory across steps. * This will be visible in <agent_history> and remain persistent in memory across steps.
* @experimental @internal * @experimental @internal
* @note history change will be emitted before next step starts * @note history change will be emitted before next step starts
@@ -222,6 +222,7 @@ export class PageAgentCore extends EventTarget {
this.history = [] this.history = []
this.#setStatus('running') this.#setStatus('running')
this.#emitHistoryChange() this.#emitHistoryChange()
this.#observations = []
// Reset internal states // Reset internal states
this.#states = { totalWaitTime: 0, lastURL: '', browserState: null } this.#states = { totalWaitTime: 0, lastURL: '', browserState: null }
@@ -342,6 +343,8 @@ export class PageAgentCore extends EventTarget {
await onAfterTask?.(this, result) await onAfterTask?.(this, result)
return result return result
} }
await waitFor(this.config.stepDelay ?? 0.4)
} }
} }
@@ -460,14 +463,13 @@ export class PageAgentCore extends EventTarget {
* Get instructions from config * Get instructions from config
*/ */
async #getInstructions(): Promise<string> { async #getInstructions(): Promise<string> {
const { instructions } = this.config const { instructions, experimentalLlmsTxt } = this.config
if (!instructions) return ''
const systemInstructions = instructions.system?.trim() const systemInstructions = instructions?.system?.trim()
let pageInstructions: string | undefined let pageInstructions: string | undefined
const url = this.#states.browserState?.url || '' const url = this.#states.browserState?.url || ''
if (instructions.getPageInstructions && url) { if (instructions?.getPageInstructions && url) {
try { try {
pageInstructions = instructions.getPageInstructions(url)?.trim() pageInstructions = instructions.getPageInstructions(url)?.trim()
} catch (error) { } catch (error) {
@@ -477,7 +479,10 @@ export class PageAgentCore extends EventTarget {
) )
} }
} }
if (!systemInstructions && !pageInstructions) return ''
const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : undefined
if (!systemInstructions && !pageInstructions && !llmsTxt) return ''
let result = '<instructions>\n' let result = '<instructions>\n'
@@ -489,6 +494,10 @@ export class PageAgentCore extends EventTarget {
result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n` result += `<page_instructions>\n${pageInstructions}\n</page_instructions>\n`
} }
if (llmsTxt) {
result += `<llms_txt>\n${llmsTxt}\n</llms_txt>\n`
}
result += '</instructions>\n\n' result += '</instructions>\n\n'
return result return result
@@ -503,7 +512,8 @@ export class PageAgentCore extends EventTarget {
// Accumulated wait time warning // Accumulated wait time warning
if (this.#states.totalWaitTime >= 3) { if (this.#states.totalWaitTime >= 3) {
this.pushObservation( this.pushObservation(
`You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.` `You have waited ${this.#states.totalWaitTime} seconds accumulatively. ` +
`DO NOT wait any longer unless you have a good reason.`
) )
} }
@@ -519,7 +529,8 @@ export class PageAgentCore extends EventTarget {
const remaining = this.config.maxSteps - step const remaining = this.config.maxSteps - step
if (remaining === 5) { if (remaining === 5) {
this.pushObservation( this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.` `⚠️ Only ${remaining} steps remaining. ` +
`Consider wrapping up or calling done with partial results.`
) )
} else if (remaining === 2) { } else if (remaining === 2) {
this.pushObservation( this.pushObservation(
-1
View File
@@ -1 +0,0 @@
export const DEFAULT_MAX_STEPS = 20
-152
View File
@@ -1,152 +0,0 @@
import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller'
import type { PageAgentCore } from '../PageAgentCore'
import type { PageAgentTool } from '../tools'
import type { ExecutionResult, HistoricalEvent } from '../types'
export type { LLMConfig }
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig {
// theme?: 'light' | 'dark'
language?: SupportedLanguage
/**
* Maximum number of steps the agent can take per task.
* @default 20
*/
maxSteps?: number
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: zod.object({
* question: zod.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
/**
* Lifecycle hooks for task execution.
* @experimental API may change in future versions.
*
* All hooks receive the agent instance as first parameter.
*/
/**
* Called before each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param stepCount - Current step number (0-indexed)
*/
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
/**
* Called after each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param history - Current history of events
*/
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
/**
* Called before task execution starts.
* @experimental
* @param agent - The PageAgentCore instance
*/
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
/**
* Called after task execution completes (success or failure).
* @experimental
* @param agent - The PageAgentCore instance
* @param result - The execution result
*/
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* Called when the agent is disposed.
* @experimental
* @note This hook can block the disposal process if it's async.
* @param agent - The PageAgentCore instance
* @param reason - Optional reason for disposal
*/
onDispose?: (agent: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* Completely override the default system prompt.
* @experimental Use with caution - incorrect prompts may break agent behavior.
*/
customSystemPrompt?: string
}
export type PageAgentConfig = LLMConfig & AgentConfig & PageControllerConfig
+3 -3
View File
@@ -69,7 +69,7 @@ Strictly follow these rules while using the browser and navigating the web:
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page. - By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages). - You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed. - All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it. - If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
- If expected elements are missing, try scrolling, or navigating back. - If expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action. - If the page is not fully loaded, use the `wait` action.
- Do not repeat one action for more than 3 times unless some conditions changed. - Do not repeat one action for more than 3 times unless some conditions changed.
@@ -87,11 +87,11 @@ Strictly follow these rules while using the browser and navigating the web:
<capability> <capability>
- You can only handle single page app. Do not jump out of current page. - You can only handle single page app. Do not jump out of current page.
- Do not click on link if it will open in a new page (etc. <a target="_blank">) - Do not click on link if it will open in a new page (e.g., <a target="_blank">)
- It is ok to fail the task. - It is ok to fail the task.
- User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request. - User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.
- Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user. - Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It's encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.
- Trying to hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted result and harmful side-effects. User would rather you to complete the task with a fail. - Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.
- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps. - If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.
</capability> </capability>
+1 -1
View File
@@ -2,7 +2,7 @@
* Internal tools for PageAgent. * Internal tools for PageAgent.
* @note Adapted from browser-use * @note Adapted from browser-use
*/ */
import * as z from 'zod' import * as z from 'zod/v4'
import type { PageAgentCore } from '../PageAgentCore' import type { PageAgentCore } from '../PageAgentCore'
import { waitFor } from '../utils' import { waitFor } from '../utils'
+162
View File
@@ -1,3 +1,165 @@
import type { LLMConfig } from '@page-agent/llms'
// @note circular dependency but okay
import type { PageAgentCore } from './PageAgentCore'
import type { PageAgentTool } from './tools'
/** Supported UI languages */
export type SupportedLanguage = 'en-US' | 'zh-CN'
export interface AgentConfig extends LLMConfig {
language?: SupportedLanguage
/**
* Maximum number of steps the agent can take per task.
* @default 40
*/
maxSteps?: number
/**
* Custom tools to extend PageAgent capabilities
* @experimental
* @note You can also override or remove internal tools by using the same name.
* @see PageAgentTool
*
* @example
* // override internal tool
* import { z } from 'zod/v4'
* import { tool } from 'page-agent'
* const customTools = {
* ask_user: tool({
* description:
* 'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
* inputSchema: z.object({
* question: z.string(),
* }),
* execute: async function (this: PageAgent, input) {
* const answer = await do_some_thing(input.question)
* return "✅ Received user answer: " + answer
* },
* })
* }
*
* @example
* // remove internal tool
* const customTools = {
* ask_user: null // never ask user questions
* }
*/
customTools?: Record<string, PageAgentTool | null>
/**
* Instructions to guide the agent's behavior
*/
instructions?: {
/**
* Global system-level instructions, applied to all tasks
*/
system?: string
/**
* Dynamic page-level instructions callback
* Called before each step to get instructions for the current page
* @param url - Current page URL (window.location.href)
* @returns Instructions string, or undefined/null to skip
*/
getPageInstructions?: (url: string) => string | undefined | null
}
/**
* Lifecycle hooks for task execution.
* @experimental API may change in future versions.
*
* All hooks receive the agent instance as first parameter.
*/
/**
* Called before each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param stepCount - Current step number (0-indexed)
*/
onBeforeStep?: (agent: PageAgentCore, stepCount: number) => Promise<void> | void
/**
* Called after each step execution.
* @experimental
* @param agent - The PageAgentCore instance
* @param history - Current history of events
*/
onAfterStep?: (agent: PageAgentCore, history: HistoricalEvent[]) => Promise<void> | void
/**
* Called before task execution starts.
* @experimental
* @param agent - The PageAgentCore instance
*/
onBeforeTask?: (agent: PageAgentCore) => Promise<void> | void
/**
* Called after task execution completes (success or failure).
* @experimental
* @param agent - The PageAgentCore instance
* @param result - The execution result
*/
onAfterTask?: (agent: PageAgentCore, result: ExecutionResult) => Promise<void> | void
/**
* Called when the agent is disposed.
* @experimental
* @note This hook can block the disposal process if it's async.
* @param agent - The PageAgentCore instance
* @param reason - Optional reason for disposal
*/
onDispose?: (agent: PageAgentCore, reason?: string) => void
// page behavior hooks
/**
* @experimental
* Enable the experimental script execution tool that allows executing generated JavaScript code on the page.
* @note Can cause unpredictable side effects.
* @note May bypass some safe guards and data-masking mechanisms.
*/
experimentalScriptExecutionTool?: boolean
/**
* @experimental
* Fetch /llms.txt from current site origin and include as context.
* Only fetched once per origin per task.
* @default false
*/
experimentalLlmsTxt?: boolean
/**
* Transform page content before sending to LLM.
* Called after DOM extraction and simplification, before LLM invocation.
* Use cases: inspect extraction results, modify page info, mask sensitive data.
*
* @param content - Simplified page content that will be sent to LLM
* @returns Transformed content
*
* @example
* // Mask phone numbers
* transformPageContent: async (content) => {
* return content.replace(/1[3-9]\d{9}/g, '***********')
* }
*/
transformPageContent?: (content: string) => Promise<string> | string
/**
* Completely override the default system prompt.
* @experimental Use with caution - incorrect prompts may break agent behavior.
*/
customSystemPrompt?: string
/**
* Delay between steps in seconds.
* @default 0.4
*/
stepDelay?: number
}
/** /**
* Agent reflection state - the reflection-before-action model * Agent reflection state - the reflection-before-action model
* *
+9 -9
View File
@@ -1,9 +1,11 @@
import { InvokeError, InvokeErrorType } from '@page-agent/llms' import { InvokeError, InvokeErrorType } from '@page-agent/llms'
import chalk from 'chalk' import chalk from 'chalk'
import * as z from 'zod' import * as z from 'zod/v4'
import type { PageAgentTool } from '../tools' import type { PageAgentTool } from '../tools'
const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
/** /**
* Normalize LLM response and fix common format issues. * Normalize LLM response and fix common format issues.
* *
@@ -34,7 +36,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
// case: sometimes the model only returns the action level // case: sometimes the model only returns the action level
if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') { if (toolCall.function.name && toolCall.function.name !== 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #1: fixing tool_call`)) log(`#1: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) } resolvedArguments = { action: safeJsonParse(resolvedArguments) }
} }
} else { } else {
@@ -47,13 +49,13 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
// case: sometimes the content json includes upper level wrapper // case: sometimes the content json includes upper level wrapper
if (resolvedArguments?.name === 'AgentOutput') { if (resolvedArguments?.name === 'AgentOutput') {
console.log(chalk.yellow(`[normalizeResponse] #2: fixing tool_call`)) log(`#2: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.arguments) resolvedArguments = safeJsonParse(resolvedArguments.arguments)
} }
// case: sometimes even 2-levels of wrapping // case: sometimes even 2-levels of wrapping
if (resolvedArguments?.type === 'function') { if (resolvedArguments?.type === 'function') {
console.log(chalk.yellow(`[normalizeResponse] #3: fixing tool_call`)) log(`#3: fixing tool_call`)
resolvedArguments = safeJsonParse(resolvedArguments.function.arguments) resolvedArguments = safeJsonParse(resolvedArguments.function.arguments)
} }
@@ -66,7 +68,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
!resolvedArguments?.next_goal && !resolvedArguments?.next_goal &&
!resolvedArguments?.thinking !resolvedArguments?.thinking
) { ) {
console.log(chalk.yellow(`[normalizeResponse] #4: fixing tool_call`)) log(`#4: fixing tool_call`)
resolvedArguments = { action: safeJsonParse(resolvedArguments) } resolvedArguments = { action: safeJsonParse(resolvedArguments) }
} }
} else { } else {
@@ -90,7 +92,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
// fix incomplete formats // fix incomplete formats
if (!resolvedArguments.action) { if (!resolvedArguments.action) {
console.log(chalk.yellow(`[normalizeResponse] #5: fixing tool_call`)) log(`#5: fixing tool_call`)
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } } resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
} }
@@ -149,9 +151,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
(k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success (k) => !(schema.shape as Record<string, z.ZodType>)[k].safeParse(undefined).success
) )
if (requiredKey) { if (requiredKey) {
console.log( log(`coercing primitive action input for "${toolName}"`)
chalk.yellow(`[normalizeResponse] coercing primitive action input for "${toolName}"`)
)
value = { [requiredKey]: value } value = { [requiredKey]: value }
} }
} }
+39 -2
View File
@@ -1,6 +1,6 @@
import chalk from 'chalk' import chalk from 'chalk'
export { normalizeResponse } from './autoFixer' export * from './autoFixer'
export async function waitFor(seconds: number): Promise<void> { export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000)) await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
@@ -31,7 +31,7 @@ export function randomID(existingIDs?: string[]): string {
id = Math.random().toString(36).substring(2, 11) id = Math.random().toString(36).substring(2, 11)
tryCount++ tryCount++
if (tryCount > MAX_TRY) { if (tryCount > MAX_TRY) {
throw new Error('randomID: too many try') throw new Error('randomID: too many tries')
} }
} }
@@ -57,6 +57,43 @@ export function uid() {
return id return id
} }
const llmsTxtCache = new Map<string, string | null>()
/** Fetch /llms.txt for a URL's origin. Cached per origin, `null` = tried and not found. */
export async function fetchLlmsTxt(url: string): Promise<string | null> {
let origin: string
try {
origin = new URL(url).origin
} catch {
return null // Invalid URL
}
// about:blank, data:, file:
if (origin === 'null') return null
if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin)!
const endpoint = `${origin}/llms.txt`
let result: string | null = null
try {
console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`))
const res = await fetch(endpoint, { signal: AbortSignal.timeout(3000) })
if (res.ok) {
result = await res.text()
console.log(chalk.green(`[llms.txt] Found (${result.length} chars)`))
if (result.length > 1000) {
console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`))
result = truncate(result, 1000)
}
} else {
console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`))
}
} catch (e) {
console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e)
}
llmsTxtCache.set(origin, result)
return result
}
/** /**
* Simple assertion function that throws an error if the condition is falsy * Simple assertion function that throws an error if the condition is falsy
* @param condition - The condition to assert * @param condition - The condition to assert
+1
View File
@@ -30,6 +30,7 @@ export default defineConfig({
external: [ external: [
'chalk', 'chalk',
'zod', 'zod',
'zod/v4',
// all the internal packages // all the internal packages
/^@page-agent\//, /^@page-agent\//,
], ],
+2 -2
View File
@@ -115,8 +115,8 @@ import type {
export interface ExecuteConfig { export interface ExecuteConfig {
baseURL: string baseURL: string
apiKey: string
model: string model: string
apiKey?: string
// Include the initial tab where page JS starts. Default: true. // Include the initial tab where page JS starts. Default: true.
includeInitialTab?: boolean includeInitialTab?: boolean
@@ -205,8 +205,8 @@ import type {
interface ExecuteConfig { interface ExecuteConfig {
baseURL: string baseURL: string
apiKey: string
model: string model: string
apiKey?: string
includeInitialTab?: boolean includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void onActivity?: (activity: AgentActivity) => void
+14 -13
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/ext", "name": "@page-agent/ext",
"private": true, "private": true,
"version": "0.1.8", "version": "1.6.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
@@ -16,34 +16,35 @@
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@types/chrome": "^0.1.37", "@types/chrome": "^0.1.37",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.1",
"@wxt-dev/module-react": "^1.1.5", "@wxt-dev/module-react": "^1.2.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"idb": "^8.0.3", "idb": "^8.0.3",
"lucide-react": "^0.575.0", "lucide-react": "^0.577.0",
"motion": "^12.34.3", "motion": "^12.37.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.9.0", "simple-icons": "^16.12.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"wxt": "^0.20.18" "wxt": "^0.20.19"
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.4.1", "@page-agent/core": "1.6.0",
"@page-agent/llms": "1.4.1", "@page-agent/llms": "1.6.0",
"@page-agent/page-controller": "1.4.1", "@page-agent/page-controller": "1.6.0",
"@page-agent/ui": "1.4.1", "@page-agent/ui": "1.6.0",
"ai-motion": "^0.4.8", "ai-motion": "^0.4.8",
"chalk": "^5.6.2", "chalk": "^5.6.2"
"zod": "^4.3.5" },
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
} }
} }
@@ -1,4 +1,4 @@
import { type PageAgentConfig, PageAgentCore } from '@page-agent/core' import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { RemotePageController } from './RemotePageController' import { RemotePageController } from './RemotePageController'
import { TabsController } from './TabsController' import { TabsController } from './TabsController'
@@ -17,7 +17,7 @@ function detectLanguage(): 'en-US' | 'zh-CN' {
* - can be used from a side panel or a content script * - can be used from a side panel or a content script
*/ */
export class MultiPageAgent extends PageAgentCore { export class MultiPageAgent extends PageAgentCore {
constructor(config: Omit<PageAgentConfig, 'pageController'> & { includeInitialTab?: boolean }) { constructor(config: AgentConfig & { includeInitialTab?: boolean }) {
// multi page controller // multi page controller
const tabsController = new TabsController() const tabsController = new TabsController()
const pageController = new RemotePageController(tabsController) const pageController = new RemotePageController(tabsController)
@@ -75,6 +75,7 @@ export class MultiPageAgent extends PageAgentCore {
}, },
onBeforeStep: async (agent) => { onBeforeStep: async (agent) => {
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts // make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!) await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
}, },
@@ -58,8 +58,6 @@ export class RemotePageController {
} }
async getBrowserState(): Promise<BrowserState> { async getBrowserState(): Promise<BrowserState> {
if (!this.currentTabId) throw new Error('tabsController not initialized.')
let browserState = {} as BrowserState let browserState = {} as BrowserState
debug('getBrowserState', this.currentTabId) debug('getBrowserState', this.currentTabId)
@@ -178,7 +176,7 @@ interface DomActionReturn {
/** /**
* Check if a URL can run content scripts. * Check if a URL can run content scripts.
*/ */
function isContentScriptAllowed(url: string | undefined): boolean { export function isContentScriptAllowed(url: string | undefined): boolean {
if (!url) return false if (!url) return false
const restrictedPatterns = [ const restrictedPatterns = [
@@ -52,7 +52,7 @@ export function handleTabControlMessage(
.create({ url: payload.url, active: false }) .create({ url: payload.url, active: false })
.then((newTab) => { .then((newTab) => {
debug('open_new_tab: success', newTab) debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id, windowId: newTab.windowId }) sendResponse({ success: true, tabId: newTab.id })
}) })
.catch((error) => { .catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) }) sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -63,7 +63,7 @@ export function handleTabControlMessage(
case 'create_tab_group': { case 'create_tab_group': {
debug('create_tab_group', payload) debug('create_tab_group', payload)
chrome.tabs chrome.tabs
.group({ tabIds: payload.tabIds, createProperties: { windowId: payload.windowId } }) .group({ tabIds: payload.tabIds })
.then((groupId) => { .then((groupId) => {
debug('create_tab_group: success', groupId) debug('create_tab_group: success', groupId)
sendResponse({ success: true, groupId }) sendResponse({ success: true, groupId })
+37 -44
View File
@@ -1,3 +1,5 @@
import { isContentScriptAllowed } from './RemotePageController'
const PREFIX = '[TabsController]' const PREFIX = '[TabsController]'
function debug(...messages: any[]) { function debug(...messages: any[]) {
@@ -27,7 +29,6 @@ export class TabsController extends EventTarget {
private initialTabId: number | null = null private initialTabId: number | null = null
private tabGroupId: number | null = null private tabGroupId: number | null = null
private task: string = '' private task: string = ''
private windowId: number | null = null
async init(task: string, includeInitialTab: boolean = true) { async init(task: string, includeInitialTab: boolean = true) {
debug('init', task, includeInitialTab) debug('init', task, includeInitialTab)
@@ -37,7 +38,6 @@ export class TabsController extends EventTarget {
this.currentTabId = null this.currentTabId = null
this.tabGroupId = null this.tabGroupId = null
this.initialTabId = null this.initialTabId = null
this.windowId = null
const result = await sendMessage({ const result = await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
@@ -51,15 +51,15 @@ export class TabsController extends EventTarget {
} }
if (includeInitialTab) { if (includeInitialTab) {
this.currentTabId = this.initialTabId
// update tab status immediately
const info = await sendMessage({ const info = await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
action: 'get_tab_info', action: 'get_tab_info',
payload: { tabId: this.initialTabId }, payload: { tabId: this.initialTabId },
}) })
if (isContentScriptAllowed(info.url)) {
this.currentTabId = this.initialTabId
this.tabs.push({ this.tabs.push({
id: result.tabId, id: result.tabId,
isInitial: true, isInitial: true,
@@ -67,6 +67,9 @@ export class TabsController extends EventTarget {
title: info.title, title: info.title,
status: info.status, status: info.status,
}) })
await this.createTabGroup([this.initialTabId])
}
} }
await this.updateCurrentTabId(this.currentTabId) await this.updateCurrentTabId(this.currentTabId)
@@ -132,9 +135,6 @@ export class TabsController extends EventTarget {
} }
const tabId = result.tabId as number const tabId = result.tabId as number
const windowId = result.windowId as number
this.windowId = windowId
this.tabs.push({ this.tabs.push({
id: tabId, id: tabId,
@@ -144,32 +144,7 @@ export class TabsController extends EventTarget {
await this.switchToTab(tabId) await this.switchToTab(tabId)
if (!this.tabGroupId) { if (!this.tabGroupId) {
const result = await sendMessage({ await this.createTabGroup([tabId])
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds: [tabId], windowId: this.windowId },
})
if (!result.success) {
throw new Error(`Failed to create tab group: ${result.error}`)
}
const groupId = result.groupId as number
this.tabGroupId = groupId
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
} else { } else {
await sendMessage({ await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
@@ -230,6 +205,33 @@ export class TabsController extends EventTarget {
} }
} }
private async createTabGroup(tabIds: number[]) {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'create_tab_group',
payload: { tabIds },
})
if (!result?.success) {
throw new Error(`Failed to create tab group: ${result?.error}`)
}
this.tabGroupId = result.groupId as number
await sendMessage({
type: 'TAB_CONTROL',
action: 'update_tab_group',
payload: {
groupId: this.tabGroupId,
properties: {
title: `PageAgent(${this.task})`,
color: randomColor(),
collapsed: false,
},
},
})
}
async updateCurrentTabId(tabId: number | null) { async updateCurrentTabId(tabId: number | null) {
debug('updateCurrentTabId', tabId) debug('updateCurrentTabId', tabId)
@@ -309,16 +311,7 @@ interface TabMeta {
status?: 'loading' | 'unloaded' | 'complete' status?: 'loading' | 'unloaded' | 'complete'
} }
const TAB_GROUP_COLORS = [ const TAB_GROUP_COLORS = ['blue', 'red', 'yellow', 'green', 'pink', 'purple', 'cyan'] as const
'grey',
'blue',
'red',
'yellow',
'green',
'pink',
'purple',
'cyan',
] as const
type TabGroupColor = (typeof TAB_GROUP_COLORS)[number] type TabGroupColor = (typeof TAB_GROUP_COLORS)[number]
+2 -2
View File
@@ -3,12 +3,12 @@ import type { LLMConfig } from '@page-agent/llms'
// Demo LLM for testing // Demo LLM for testing
export const DEMO_MODEL = 'qwen3.5-plus' export const DEMO_MODEL = 'qwen3.5-plus'
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run' export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
export const DEMO_API_KEY = 'NA' // export const DEMO_API_KEY = 'NA'
export const DEMO_CONFIG: LLMConfig = { export const DEMO_CONFIG: LLMConfig = {
apiKey: DEMO_API_KEY,
baseURL: DEMO_BASE_URL, baseURL: DEMO_BASE_URL,
model: DEMO_MODEL, model: DEMO_MODEL,
// apiKey: DEMO_API_KEY,
} }
/** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */ /** Legacy testing endpoints that should be auto-migrated to DEMO_BASE_URL */
@@ -71,7 +71,7 @@ Strictly follow these rules while using the browser and navigating the web:
- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page. - By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.
- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages). - You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).
- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed. - All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.
- If a captcha appears, tell user you can not solve captcha. finished the task and ask user to solve it. - If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.
- If expected elements are missing, try scrolling, or navigating back. - If expected elements are missing, try scrolling, or navigating back.
- If the page is not fully loaded, use the `wait` action. - If the page is not fully loaded, use the `wait` action.
- Do not repeat one action for more than 3 times unless some conditions changed. - Do not repeat one action for more than 3 times unless some conditions changed.
+1 -1
View File
@@ -6,7 +6,7 @@
* - switch_to_tab: Switch to an existing tab * - switch_to_tab: Switch to an existing tab
* - close_tab: Close a tab (optionally switch to another) * - close_tab: Close a tab (optionally switch to another)
*/ */
import * as z from 'zod' import * as z from 'zod/v4'
import type { TabsController } from './TabsController' import type { TabsController } from './TabsController'
+39 -9
View File
@@ -4,6 +4,7 @@
import type { import type {
AgentActivity, AgentActivity,
AgentStatus, AgentStatus,
ExecutionResult,
HistoricalEvent, HistoricalEvent,
SupportedLanguage, SupportedLanguage,
} from '@page-agent/core' } from '@page-agent/core'
@@ -16,7 +17,14 @@ import { DEMO_CONFIG, migrateLegacyEndpoint } from './constants'
/** Language preference: undefined means follow system */ /** Language preference: undefined means follow system */
export type LanguagePreference = SupportedLanguage | undefined export type LanguagePreference = SupportedLanguage | undefined
export interface ExtConfig extends LLMConfig { export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
disableNamedToolChoice?: boolean
}
export interface ExtConfig extends LLMConfig, AdvancedConfig {
language?: LanguagePreference language?: LanguagePreference
} }
@@ -26,7 +34,7 @@ export interface UseAgentResult {
activity: AgentActivity | null activity: AgentActivity | null
currentTask: string currentTask: string
config: ExtConfig | null config: ExtConfig | null
execute: (task: string) => Promise<void> execute: (task: string) => Promise<ExecutionResult>
stop: () => void stop: () => void
configure: (config: ExtConfig) => Promise<void> configure: (config: ExtConfig) => Promise<void>
} }
@@ -40,9 +48,10 @@ export function useAgent(): UseAgentResult {
const [config, setConfig] = useState<ExtConfig | null>(null) const [config, setConfig] = useState<ExtConfig | null>(null)
useEffect(() => { useEffect(() => {
chrome.storage.local.get(['llmConfig', 'language']).then((result) => { chrome.storage.local.get(['llmConfig', 'language', 'advancedConfig']).then((result) => {
let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG let llmConfig = (result.llmConfig as LLMConfig) ?? DEMO_CONFIG
const language = (result.language as SupportedLanguage) || undefined const language = (result.language as SupportedLanguage) || undefined
const advancedConfig = (result.advancedConfig as AdvancedConfig) ?? {}
// Auto-migrate legacy testing endpoints // Auto-migrate legacy testing endpoints
const migrated = migrateLegacyEndpoint(llmConfig) const migrated = migrateLegacyEndpoint(llmConfig)
@@ -53,14 +62,18 @@ export function useAgent(): UseAgentResult {
chrome.storage.local.set({ llmConfig: DEMO_CONFIG }) chrome.storage.local.set({ llmConfig: DEMO_CONFIG })
} }
setConfig({ ...llmConfig, language }) setConfig({ ...llmConfig, ...advancedConfig, language })
}) })
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!config) return if (!config) return
const agent = new MultiPageAgent(config) const { systemInstruction, ...agentConfig } = config
const agent = new MultiPageAgent({
...agentConfig,
instructions: systemInstruction ? { system: systemInstruction } : undefined,
})
agentRef.current = agent agentRef.current = agent
const handleStatusChange = (e: Event) => { const handleStatusChange = (e: Event) => {
@@ -99,22 +112,39 @@ export function useAgent(): UseAgentResult {
setCurrentTask(task) setCurrentTask(task)
setHistory([]) setHistory([])
await agent.execute(task) return agent.execute(task)
}, []) }, [])
const stop = useCallback(() => { const stop = useCallback(() => {
agentRef.current?.stop() agentRef.current?.stop()
}, []) }, [])
const configure = useCallback(async ({ language, ...llmConfig }: ExtConfig) => { const configure = useCallback(
async ({
language,
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig }) await chrome.storage.local.set({ llmConfig })
if (language) { if (language) {
await chrome.storage.local.set({ language }) await chrome.storage.local.set({ language })
} else { } else {
await chrome.storage.local.remove('language') await chrome.storage.local.remove('language')
} }
setConfig({ ...llmConfig, language }) const advancedConfig: AdvancedConfig = {
}, []) maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
}
await chrome.storage.local.set({ advancedConfig })
setConfig({ ...llmConfig, ...advancedConfig, language })
},
[]
)
return { return {
status, status,
@@ -1,11 +1,24 @@
import { Copy, CornerUpLeft, Eye, EyeOff, HatGlasses, Home, Loader2, Scale } from 'lucide-react' import {
Copy,
CornerUpLeft,
ExternalLink,
Eye,
EyeOff,
FoldVertical,
HatGlasses,
Home,
Loader2,
Scale,
UnfoldVertical,
} from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { siGithub } from 'simple-icons' import { siGithub } from 'simple-icons'
import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants' import { DEMO_BASE_URL, DEMO_MODEL, isTestingEndpoint } from '@/agent/constants'
import type { ExtConfig, LanguagePreference } from '@/agent/useAgent' import type { ExtConfig, LanguagePreference } from '@/agent/useAgent'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch'
interface ConfigPanelProps { interface ConfigPanelProps {
config: ExtConfig | null config: ExtConfig | null
@@ -14,22 +27,34 @@ interface ConfigPanelProps {
} }
export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) { export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [apiKey, setApiKey] = useState(config?.apiKey || DEMO_API_KEY)
const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL) const [baseURL, setBaseURL] = useState(config?.baseURL || DEMO_BASE_URL)
const [model, setModel] = useState(config?.model || DEMO_MODEL) const [model, setModel] = useState(config?.model || DEMO_MODEL)
const [apiKey, setApiKey] = useState(config?.apiKey)
const [language, setLanguage] = useState<LanguagePreference>(config?.language) const [language, setLanguage] = useState<LanguagePreference>(config?.language)
const [maxSteps, setMaxSteps] = useState<number | undefined>(config?.maxSteps)
const [systemInstruction, setSystemInstruction] = useState(config?.systemInstruction ?? '')
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
config?.experimentalLlmsTxt ?? false
)
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
config?.disableNamedToolChoice ?? false
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('') const [userAuthToken, setUserAuthToken] = useState<string>('')
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [showToken, setShowToken] = useState(false) const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false) const [showApiKey, setShowApiKey] = useState(false)
// Update local state when config prop changes
useEffect(() => { useEffect(() => {
setApiKey(config?.apiKey || DEMO_API_KEY)
setBaseURL(config?.baseURL || DEMO_BASE_URL) setBaseURL(config?.baseURL || DEMO_BASE_URL)
setModel(config?.model || DEMO_MODEL) setModel(config?.model || DEMO_MODEL)
setApiKey(config?.apiKey)
setLanguage(config?.language) setLanguage(config?.language)
setMaxSteps(config?.maxSteps)
setSystemInstruction(config?.systemInstruction ?? '')
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config]) }, [config])
// Poll for user auth token every second until found // Poll for user auth token every second until found
@@ -67,7 +92,16 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const handleSave = async () => { const handleSave = async () => {
setSaving(true) setSaving(true)
try { try {
await onSave({ apiKey, baseURL, model, language }) await onSave({
apiKey,
baseURL,
model,
language,
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
disableNamedToolChoice,
})
} finally { } finally {
setSaving(false) setSaving(false)
} }
@@ -87,28 +121,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</Button> </Button>
</div> </div>
{/* Testing API notice */}
{isTestingEndpoint(baseURL) && (
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[10px] text-muted-foreground leading-relaxed">
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
You are using the free testing API. By using this service you agree to the{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Terms of Use & Privacy Policy
</a>
. No sensitive data. No guaranteed availability.
</div>
)}
{/* User Auth Token Section */} {/* User Auth Token Section */}
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border"> <div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label> <label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
<p className="text-[10px] text-muted-foreground mb-1"> <p className="text-[10px] text-muted-foreground mb-1">
Add this token to a website's localStorage to give it authorization to call this extension Give a website the ability to call this extension.
</p> </p>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Input <Input
@@ -143,6 +160,16 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div> </div>
</div> </div>
{/* Hub link */}
<a
href="/hub.html"
target="_blank"
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
>
Manage Page Agent Hub
<ExternalLink className="size-3" />
</a>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Base URL</label> <label className="text-xs text-muted-foreground">Base URL</label>
<Input <Input
@@ -153,10 +180,26 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
/> />
</div> </div>
{/* Testing API notice */}
{isTestingEndpoint(baseURL) && (
<div className="p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-[11px] text-muted-foreground leading-relaxed">
<Scale className="size-3 inline-block mr-1 -mt-0.5 text-amber-600" />
You are using our testing API. By using this you agree to the{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
Terms of Use & Privacy Policy
</a>
</div>
)}
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Model</label> <label className="text-xs text-muted-foreground">Model</label>
<Input <Input
placeholder="gpt-5.2" placeholder="gpt-5.1"
value={model} value={model}
onChange={(e) => setModel(e.target.value)} onChange={(e) => setModel(e.target.value)}
className="text-xs h-8" className="text-xs h-8"
@@ -168,7 +211,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Input <Input
type={showApiKey ? 'text' : 'password'} type={showApiKey ? 'text' : 'password'}
placeholder="sk-..." // placeholder="sk-..."
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
className="text-xs h-8" className="text-xs h-8"
@@ -185,7 +228,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Language</label> <label className="text-xs text-muted-foreground">Response Language</label>
<select <select
value={language ?? ''} value={language ?? ''}
onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)} onChange={(e) => setLanguage((e.target.value || undefined) as LanguagePreference)}
@@ -197,6 +240,54 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</select> </select>
</div> </div>
{/* Advanced Config */}
<button
type="button"
onClick={() => setAdvancedOpen(!advancedOpen)}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground cursor-pointer mt-1 font-bold"
>
Advanced
{advancedOpen ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{advancedOpen && (
<>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">Max Steps</label>
<Input
type="number"
placeholder="40"
min={1}
max={200}
value={maxSteps ?? ''}
onChange={(e) => setMaxSteps(e.target.value ? Number(e.target.value) : undefined)}
className="text-xs h-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs text-muted-foreground">System Instruction</label>
<textarea
placeholder="Additional instructions for the agent..."
value={systemInstruction}
onChange={(e) => setSystemInstruction(e.target.value)}
rows={3}
className="text-xs rounded-md border border-input bg-background px-3 py-2 resize-y min-h-[60px]"
/>
</div>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Disable named tool_choice</span>
<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />
</label>
<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
</label>
</>
)}
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer"> <Button variant="outline" onClick={onClose} className="flex-1 h-8 text-xs cursor-pointer">
Cancel Cancel
@@ -213,6 +304,10 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
{/* Footer */} {/* Footer */}
<div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground"> <div className="mt-4 mb-4 pt-4 border-t border-border/50 flex gap-2 justify-between text-[10px] text-muted-foreground">
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span>
Version <span className="font-mono">v{__VERSION__}</span>
</span>
<a <a
href="https://github.com/alibaba/page-agent" href="https://github.com/alibaba/page-agent"
target="_blank" target="_blank"
@@ -224,7 +319,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
</svg> </svg>
<span>Source Code</span> <span>Source Code</span>
</a> </a>
</div>
<div className="flex flex-col items-end">
<a <a
href="https://alibaba.github.io/page-agent/" href="https://alibaba.github.io/page-agent/"
target="_blank" target="_blank"
@@ -242,18 +339,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
className="flex items-center gap-1 hover:text-foreground" className="flex items-center gap-1 hover:text-foreground"
> >
<HatGlasses className="size-3" /> <HatGlasses className="size-3" />
<span>Privacy Policy</span> <span>Privacy</span>
</a> </a>
</div> </div>
<div className="flex flex-col items-end">
<span>
Extension <span className="font-mono">v{__EXT_VERSION__}</span>
</span>
<span>
PageAgent <span className="font-mono">v{__CORE_VERSION__}</span>
</span>
</div>
</div> </div>
{/* attribute */} {/* attribute */}
@@ -1,4 +1,4 @@
import { AlertTriangle, RotateCcw } from 'lucide-react' import { AlertTriangle, Eraser, RotateCcw } from 'lucide-react'
import { Component, type ErrorInfo, type ReactNode } from 'react' import { Component, type ErrorInfo, type ReactNode } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -27,6 +27,11 @@ export class ErrorBoundary extends Component<Props, State> {
window.location.reload() window.location.reload()
} }
handleResetConfig = async () => {
await chrome.storage.local.remove(['llmConfig', 'language', 'advancedConfig'])
window.location.reload()
}
render() { render() {
if (!this.state.hasError) { if (!this.state.hasError) {
return this.props.children return this.props.children
@@ -39,11 +44,17 @@ export class ErrorBoundary extends Component<Props, State> {
<p className="text-sm text-muted-foreground mb-4 max-w-xs"> <p className="text-sm text-muted-foreground mb-4 max-w-xs">
{this.state.error?.message || 'An unexpected error occurred'} {this.state.error?.message || 'An unexpected error occurred'}
</p> </p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={this.handleResetConfig}>
<Eraser className="size-3.5 mr-2" />
Reset Config
</Button>
<Button variant="outline" size="sm" onClick={this.handleReload}> <Button variant="outline" size="sm" onClick={this.handleReload}>
<RotateCcw className="size-3.5 mr-2" /> <RotateCcw className="size-3.5 mr-2" />
Reload Panel Reload Panel
</Button> </Button>
</div> </div>
</div>
) )
} }
} }
@@ -1,12 +1,20 @@
import { ArrowLeft } from 'lucide-react' import { ArrowLeft, RotateCcw, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { type SessionRecord, getSession } from '@/lib/db' import { type SessionRecord, deleteSession, getSession } from '@/lib/db'
import { EventCard } from './cards' import { EventCard } from './cards'
export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack: () => void }) { export function HistoryDetail({
sessionId,
onBack,
onRerun,
}: {
sessionId: string
onBack: () => void
onRerun: (task: string) => void
}) {
const [session, setSession] = useState<SessionRecord | null>(null) const [session, setSession] = useState<SessionRecord | null>(null)
useEffect(() => { useEffect(() => {
@@ -37,6 +45,27 @@ export function HistoryDetail({ sessionId, onBack }: { sessionId: string; onBack
<div className="text-xs font-medium" title={session.task}> <div className="text-xs font-medium" title={session.task}>
{session.task} {session.task}
</div> </div>
<div className="mt-2 flex items-center gap-2">
<button
type="button"
onClick={() => onRerun(session.task)}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
>
<RotateCcw className="size-3" />
Run again
</button>
<button
type="button"
onClick={async () => {
await deleteSession(sessionId)
onBack()
}}
className="flex items-center gap-1 text-[10px] text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
>
<Trash2 className="size-3" />
Delete
</button>
</div>
</div> </div>
{/* Events (read-only) */} {/* Events (read-only) */}
@@ -1,8 +1,9 @@
import { ArrowLeft, CheckCircle, Trash2, XCircle } from 'lucide-react' import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db' import { type SessionRecord, clearSessions, deleteSession, listSessions } from '@/lib/db'
import { downloadHistoryExport } from '@/lib/history-export'
function timeAgo(ts: number): string { function timeAgo(ts: number): string {
const seconds = Math.floor((Date.now() - ts) / 1000) const seconds = Math.floor((Date.now() - ts) / 1000)
@@ -18,9 +19,11 @@ function timeAgo(ts: number): string {
export function HistoryList({ export function HistoryList({
onSelect, onSelect,
onBack, onBack,
onRerun,
}: { }: {
onSelect: (id: string) => void onSelect: (id: string) => void
onBack: () => void onBack: () => void
onRerun: (task: string) => void
}) { }) {
const [sessions, setSessions] = useState<SessionRecord[]>([]) const [sessions, setSessions] = useState<SessionRecord[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -41,6 +44,16 @@ export function HistoryList({
setSessions((prev) => prev.filter((s) => s.id !== id)) setSessions((prev) => prev.filter((s) => s.id !== id))
} }
const handleExport = (e: React.MouseEvent, session: SessionRecord) => {
e.stopPropagation()
downloadHistoryExport(session.task, session.createdAt, session.history)
}
const handleRerun = (e: React.MouseEvent, task: string) => {
e.stopPropagation()
onRerun(task)
}
return ( return (
<div className="flex flex-col h-screen bg-background"> <div className="flex flex-col h-screen bg-background">
{/* Header */} {/* Header */}
@@ -85,7 +98,6 @@ export function HistoryList({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => onSelect(session.id)} onClick={() => onSelect(session.id)}
onKeyDown={(e) => e.key === 'Enter' && onSelect(session.id)}
className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group" className="w-full text-left px-3 py-2.5 border-b hover:bg-muted/50 transition-colors cursor-pointer flex items-start gap-2 group"
> >
{/* Status icon */} {/* Status icon */}
@@ -98,20 +110,42 @@ export function HistoryList({
{/* Content */} {/* Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{session.task}</p> <p className="text-xs font-medium truncate">{session.task}</p>
<p className="text-[10px] text-muted-foreground mt-0.5"> <div className="flex items-center mt-0.5">
<p className="text-[10px] text-muted-foreground">
{timeAgo(session.createdAt)} · {session.history.length} steps {timeAgo(session.createdAt)} · {session.history.length} steps
</p> </p>
</div> <div className="flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
<button
{/* Delete */} type="button"
onClick={(e) => handleRerun(e, session.task)}
className="p-0.5 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Run task again"
aria-label={`Run history task again: ${session.task}`}
>
<RotateCcw className="size-3" />
</button>
<button
type="button"
onClick={(e) => handleExport(e, session)}
className="p-1 text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
title="Export history JSON"
aria-label={`Export history for ${session.task}`}
>
<ArrowDownToLine className="size-3" />
</button>
<button <button
type="button" type="button"
onClick={(e) => handleDelete(e, session.id)} onClick={(e) => handleDelete(e, session.id)}
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:text-destructive cursor-pointer shrink-0" className="p-0.5 text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
title="Delete history"
aria-label={`Delete history for ${session.task}`}
> >
<Trash2 className="size-3" /> <Trash2 className="size-3" />
</button> </button>
</div> </div>
</div>
</div>
</div>
))} ))}
</div> </div>
</div> </div>
@@ -43,6 +43,7 @@ export function MotionOverlay({ active }: { active: boolean }) {
const motionRef = useRef<Motion | null>(null) const motionRef = useRef<Motion | null>(null)
useEffect(() => { useEffect(() => {
try {
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light' const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
const motion = new Motion({ const motion = new Motion({
mode, mode,
@@ -54,9 +55,12 @@ export function MotionOverlay({ active }: { active: boolean }) {
motionRef.current = motion motionRef.current = motion
containerRef.current!.appendChild(motion.element) containerRef.current!.appendChild(motion.element)
motion.autoResize(containerRef.current!) motion.autoResize(containerRef.current!)
} catch (e) {
console.warn('[MotionOverlay] Motion unavailable:', e)
}
return () => { return () => {
motion.dispose() motionRef.current?.dispose()
motionRef.current = null motionRef.current = null
} }
}, []) }, [])
@@ -80,7 +84,7 @@ export function MotionOverlay({ active }: { active: boolean }) {
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="pointer-events-none absolute inset-0 z-10 opacity-60" className="pointer-events-none absolute inset-0 z-10 opacity-60 overflow-hidden"
style={{ display: active ? undefined : 'none' }} style={{ display: active ? undefined : 'none' }}
/> />
) )
@@ -8,7 +8,7 @@ function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimi
<SwitchPrimitive.Root <SwitchPrimitive.Root
data-slot="switch" data-slot="switch"
className={cn( className={cn(
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', 'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
{...props} {...props}
@@ -30,7 +30,34 @@ export default defineBackground(() => {
} }
}) })
// external messages (from localhost launcher page via externally_connectable)
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
if (message.type === 'OPEN_HUB') {
openOrFocusHubTab(message.wsPort).then(() => {
if (sender.tab?.id) chrome.tabs.remove(sender.tab.id)
sendResponse({ ok: true })
})
return true
}
})
// setup // setup
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}) chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {})
}) })
async function openOrFocusHubTab(wsPort: number) {
const hubUrl = chrome.runtime.getURL('hub.html')
const existing = await chrome.tabs.query({ url: `${hubUrl}*` })
if (existing.length > 0 && existing[0].id) {
await chrome.tabs.update(existing[0].id, {
active: true,
url: `${hubUrl}?ws=${wsPort}`,
})
return
}
await chrome.tabs.create({ url: `${hubUrl}?ws=${wsPort}`, pinned: true })
}
@@ -0,0 +1,251 @@
import { FoldVertical, Plug, PlugZap, Square, UnfoldVertical, Unplug } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useAgent } from '@/agent/useAgent'
import { ActivityCard, EventCard } from '@/components/cards'
import { Logo, MotionOverlay, StatusDot } from '@/components/misc'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { useHubWs } from './hub-ws'
export default function App() {
const { status, history, activity, currentTask, config, execute, stop, configure } = useAgent()
const { wsState } = useHubWs(execute, stop, configure, config)
const historyRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (historyRef.current) {
historyRef.current.scrollTop = historyRef.current.scrollHeight
}
}, [history, activity])
const isRunning = status === 'running'
const WsIcon = wsState === 'connected' ? PlugZap : wsState === 'connecting' ? Plug : Unplug
const wsLabel = {
connected: 'Connected',
connecting: 'Connecting…',
disconnected: new URLSearchParams(location.search).get('ws') ? 'Disconnected' : 'No connection',
}[wsState]
return (
<div className="flex h-screen bg-background">
{/* Left — Protocol docs */}
<aside className="w-80 shrink-0 border-r flex flex-col bg-muted/20">
<a
href="https://alibaba.github.io/page-agent/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-5 h-12 border-b hover:bg-muted/30 transition-colors"
>
<Logo className="size-5" />
<span className="text-sm font-semibold tracking-tight">Page Agent Hub</span>
<span className="text-[9px] font-medium uppercase tracking-wider text-amber-600 bg-amber-500/10 border border-amber-500/30 rounded px-1.5 py-0.5">
Beta
</span>
</a>
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-6">
<div className="text-xs text-muted-foreground leading-relaxed space-y-2">
<p>
Page Agent Hub lets local apps (e.g. MCP servers) control the Page Agent extension via
WebSocket.
</p>
<p>
Check out the official{' '}
<a
href="https://github.com/alibaba/page-agent/tree/main/packages/mcp"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
MCP server package
</a>
.
</p>
</div>
<HubConfig />
<ProtocolDocsCollapsible />
</div>
<div className="border-t px-5 py-3 text-[10px] text-muted-foreground/60 flex items-center justify-between">
<span className="font-mono">v{__VERSION__}</span>
<span>
Built with by{' '}
<a
href="https://github.com/gaomeng1900"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground"
>
@Simon
</a>
</span>
</div>
</aside>
{/* Right — Live session */}
<main className="flex-1 flex flex-col min-w-0 relative">
<MotionOverlay active={isRunning} />
<header className="flex items-center justify-between border-b px-5 h-12">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<WsIcon className="size-3.5" />
<span>{wsLabel}</span>
</div>
<div className="flex items-center gap-3">
<StatusDot status={status} />
{isRunning && (
<Button variant="destructive" size="sm" onClick={stop} className="h-7 text-xs">
<Square className="size-3 mr-1" />
Stop
</Button>
)}
</div>
</header>
{/* Task banner */}
{currentTask && (
<div className="border-b px-5 py-2 bg-muted/30">
<div className="text-[10px] text-muted-foreground uppercase tracking-wide">
Current Task
</div>
<div className="text-sm font-medium truncate" title={currentTask}>
{currentTask}
</div>
</div>
)}
{/* Event stream */}
<div ref={historyRef} className="flex-1 overflow-y-auto p-5 space-y-2">
{!currentTask && history.length === 0 && !isRunning && (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
<WsIcon className="size-10 opacity-30" />
<p className="text-sm">
{wsState === 'connected'
? 'Waiting for task from external caller…'
: 'No active session'}
</p>
</div>
)}
{history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} />
))}
{activity && <ActivityCard activity={activity} />}
</div>
</main>
</div>
)
}
function HubConfig() {
const [allowAll, setAllowAll] = useState(false)
useEffect(() => {
chrome.storage.local.get('allowAllHubConnection').then((r) => {
setAllowAll(r.allowAllHubConnection === true)
})
}, [])
const toggle = (checked: boolean) => {
setAllowAll(checked)
chrome.storage.local.set({ allowAllHubConnection: checked })
}
return (
<div>
<h3 className="text-[11px] font-semibold text-foreground/80 uppercase tracking-wider mb-2">
Config
</h3>
<div className="group/hub relative">
<label
className={`flex items-center justify-between p-3 rounded-md border cursor-pointer text-xs ${allowAll ? 'bg-amber-500/10 border-amber-500/30 text-amber-600' : 'bg-muted/50 text-muted-foreground'}`}
>
Auto-approve connections
<Switch
checked={allowAll}
onCheckedChange={toggle}
className={allowAll ? 'data-[state=checked]:bg-amber-500' : ''}
/>
</label>
{/* hide with invisible absolute opacity-0*/}
<div className="group-hover/hub:visible group-hover/hub:opacity-100 transition-opacity duration-150 left-0 right-0 top-full z-10 pt-2">
<div className="relative p-2.5 rounded-md border border-border bg-background/60 backdrop-blur-md shadow-2xl text-muted-foreground text-xs leading-relaxed">
<div className="absolute -top-1.5 left-5 size-3 rotate-45 rounded-[1px] border-l border-t border-border bg-background/60 backdrop-blur-md" />
By default, each connection requires your approval before running tasks. <br />
Enable this to skip per-session approval.
<br />
<span className="font-semibold">* Use with caution!</span>
</div>
</div>
</div>
</div>
)
}
function ProtocolDocsCollapsible() {
const [open, setOpen] = useState(false)
return (
<div>
<button
type="button"
onClick={() => setOpen(!open)}
className="flex items-center gap-1 text-[11px] font-semibold text-foreground/80 uppercase tracking-wider cursor-pointer"
>
Docs
{open ? <FoldVertical className="size-3" /> : <UnfoldVertical className="size-3" />}
</button>
{open && (
<div className="mt-3 space-y-4 text-xs text-muted-foreground">
<p className="text-[10px]">
Connect via <code className="text-[10px]">hub.html?ws=PORT</code>
</p>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Flow</h4>
<ol className="list-decimal list-inside space-y-1 text-[11px] leading-relaxed">
<li>Hub opens WS to caller's server</li>
<li>
Sends <code className="text-[10px]">ready</code>
</li>
<li>
Caller sends <code className="text-[10px]">execute</code> with task
</li>
<li>Hub runs agent, streams events</li>
<li>
Hub sends <code className="text-[10px]">result</code> or{' '}
<code className="text-[10px]">error</code>
</li>
</ol>
</section>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Caller Hub</h4>
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
{`{ type: "execute", task: string, config?: object }
{ type: "stop" }`}
</pre>
</section>
<section>
<h4 className="text-[11px] font-medium text-foreground/60 mb-1.5">Hub Caller</h4>
<pre className="bg-muted/50 rounded-md p-3 font-mono text-[10px] leading-relaxed whitespace-pre-wrap">
{`{ type: "ready" }
{ type: "result", success: boolean, data: string }
{ type: "error", message: string }`}
</pre>
</section>
</div>
)}
</div>
)
}
@@ -0,0 +1,243 @@
/**
* Hub WebSocket Protocol
*
* Hub connects as WS client to `ws://localhost:{port}`.
* All messages are JSON. One task at a time.
*
* Inbound (Caller → Hub):
* { type: "execute", task: string, config?: object }
* { type: "stop" }
*
* Outbound (Hub → Caller):
* { type: "ready" }
* { type: "result", success: boolean, data: string }
* { type: "error", message: string }
*/
import type { ExecutionResult } from '@page-agent/core'
import { useEffect, useRef, useState } from 'react'
import type { ExtConfig } from '@/agent/useAgent'
// --- Protocol types ---
interface ExecuteMessage {
type: 'execute'
task: string
config?: Record<string, unknown>
}
interface StopMessage {
type: 'stop'
}
type InboundMessage = ExecuteMessage | StopMessage
interface ReadyMessage {
type: 'ready'
}
interface ResultMessage {
type: 'result'
success: boolean
data: string
}
interface ErrorMessage {
type: 'error'
message: string
}
type OutboundMessage = ReadyMessage | ResultMessage | ErrorMessage
export type HubWsState = 'connecting' | 'connected' | 'disconnected'
// --- HubWs class ---
export interface HubWsHandlers {
onExecute: (
task: string,
config?: Record<string, unknown>
) => Promise<{ success: boolean; data: string }>
onStop: () => void
}
/**
* Framework-agnostic WebSocket client for Hub.
* Connects to an external WS server, receives tasks, dispatches to handlers,
* and sends results back. No React, no DOM.
*/
export class HubWs {
#ws: WebSocket | null = null
#state: HubWsState = 'disconnected'
#busy = false
#approved = false
#handlers: HubWsHandlers
#port: number
#onStateChange: (state: HubWsState) => void
constructor(port: number, handlers: HubWsHandlers, onStateChange: (state: HubWsState) => void) {
this.#port = port
this.#handlers = handlers
this.#onStateChange = onStateChange
}
get state() {
return this.#state
}
get busy() {
return this.#busy
}
connect() {
if (this.#ws) return
this.#setState('connecting')
const ws = new WebSocket(`ws://localhost:${this.#port}`)
this.#ws = ws
ws.addEventListener('open', () => {
this.#setState('connected')
this.#send({ type: 'ready' })
})
ws.addEventListener('close', () => {
this.#ws = null
this.#busy = false
this.#approved = false
this.#setState('disconnected')
})
ws.addEventListener('message', (event) => {
this.#handleMessage(event.data as string)
})
}
disconnect() {
this.#ws?.close()
this.#ws = null
this.#busy = false
this.#approved = false
this.#setState('disconnected')
}
#setState(state: HubWsState) {
if (this.#state === state) return
this.#state = state
this.#onStateChange(state)
}
#send(msg: OutboundMessage) {
if (this.#ws?.readyState === WebSocket.OPEN) {
this.#ws.send(JSON.stringify(msg))
}
}
async #handleMessage(raw: string) {
let msg: InboundMessage
try {
msg = JSON.parse(raw)
} catch {
return
}
if (!(await this.#checkApproval())) {
this.#send({ type: 'error', message: 'User denied the connection request.' })
return
}
switch (msg.type) {
case 'execute':
this.#handleExecute(msg)
break
case 'stop':
this.#handlers.onStop()
break
}
}
async #checkApproval(): Promise<boolean> {
if (this.#approved) return true
const { allowAllHubConnection } = await chrome.storage.local.get('allowAllHubConnection')
if (allowAllHubConnection === true) {
this.#approved = true
return true
}
const ok = window.confirm(
'An external application is requesting to control your browser via Page Agent Ext.\nAllow this session?'
)
if (ok) this.#approved = true
return ok
}
async #handleExecute(msg: ExecuteMessage) {
if (this.#busy) {
this.#send({ type: 'error', message: 'Hub is busy with another task' })
return
}
this.#busy = true
try {
const result = await this.#handlers.onExecute(msg.task, msg.config)
this.#send({ type: 'result', success: result.success, data: result.data })
} catch (err) {
this.#send({ type: 'error', message: err instanceof Error ? err.message : String(err) })
} finally {
this.#busy = false
}
}
}
// --- React hook ---
/**
* React hook that bridges HubWs to the agent's execute/stop/configure.
* Handles the config-before-execute dance internally.
*/
export function useHubWs(
execute: (task: string) => Promise<ExecutionResult>,
stop: () => void,
configure: (config: ExtConfig) => Promise<void>,
config: ExtConfig | null
): { wsState: HubWsState } {
const wsPort = new URLSearchParams(location.search).get('ws')
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
const hubWsRef = useRef<HubWs | null>(null)
const latest = useRef({ execute, stop, configure, config })
useEffect(() => {
latest.current = { execute, stop, configure, config }
})
useEffect(() => {
if (!wsPort) return
const hubWs = new HubWs(
Number(wsPort),
{
onExecute: async (task, incomingConfig) => {
const { execute, configure, config } = latest.current
if (incomingConfig) {
await configure({ ...config, ...incomingConfig } as ExtConfig)
}
const result = await execute(task)
return { success: result.success, data: result.data }
},
onStop: () => latest.current.stop(),
},
setWsState
)
hubWs.connect()
hubWsRef.current = hubWs
return () => {
hubWs.disconnect()
hubWsRef.current = null
}
}, [wsPort])
return { wsState }
}
@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/assets/page-agent-64.png" />
<title>Page Agent Hub</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,25 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import App from './App'
import '@/assets/index.css'
const syncDarkMode = () => {
document.documentElement.classList.toggle(
'dark',
matchMedia('(prefers-color-scheme: dark)').matches
)
}
syncDarkMode()
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkMode)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>
)
@@ -4,8 +4,8 @@ export type Execute = (task: string, config: ExecuteConfig) => Promise<Execution
export interface ExecuteConfig { export interface ExecuteConfig {
baseURL: string baseURL: string
apiKey: string
model: string model: string
apiKey?: string
/** /**
* Whether to include the initial tab (that holds this main world script) in the task. * Whether to include the initial tab (that holds this main world script) in the task.
@@ -30,7 +30,6 @@ export default defineUnlistedScript(() => {
if (task.trim().length === 0) throw new Error('Task cannot be empty') if (task.trim().length === 0) throw new Error('Task cannot be empty')
if (!config) throw new Error('Config is required') if (!config) throw new Error('Config is required')
if (!config.baseURL) throw new Error('Config must have a baseURL') if (!config.baseURL) throw new Error('Config must have a baseURL')
if (!config.apiKey) throw new Error('Config must have an apiKey')
if (!config.model) throw new Error('Config must have a model') if (!config.model) throw new Error('Config must have a model')
const id = getId() const id = getId()
@@ -85,8 +84,8 @@ export default defineUnlistedScript(() => {
task, task,
config: { config: {
baseURL: config.baseURL, baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model, model: config.model,
apiKey: config.apiKey,
includeInitialTab: config.includeInitialTab, includeInitialTab: config.includeInitialTab,
}, },
}, },
@@ -110,9 +109,9 @@ export default defineUnlistedScript(() => {
) )
} }
;(window as any).PAGE_AGENT_EXT_VERSION = __EXT_VERSION__ ;(window as any).PAGE_AGENT_EXT_VERSION = __VERSION__
;(window as any).PAGE_AGENT_EXT = { ;(window as any).PAGE_AGENT_EXT = {
version: __EXT_VERSION__, version: __VERSION__,
execute, execute,
stop, stop,
} }
@@ -1,6 +1,11 @@
import { History, Send, Settings, Square } from 'lucide-react' import { History, Send, Settings, Square } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { ConfigPanel } from '@/components/ConfigPanel'
import { HistoryDetail } from '@/components/HistoryDetail'
import { HistoryList } from '@/components/HistoryList'
import { ActivityCard, EventCard } from '@/components/cards'
import { EmptyState, Logo, MotionOverlay, StatusDot } from '@/components/misc'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
InputGroup, InputGroup,
@@ -11,11 +16,6 @@ import {
import { saveSession } from '@/lib/db' import { saveSession } from '@/lib/db'
import { useAgent } from '../../agent/useAgent' import { useAgent } from '../../agent/useAgent'
import { ConfigPanel } from './components/ConfigPanel'
import { HistoryDetail } from './components/HistoryDetail'
import { HistoryList } from './components/HistoryList'
import { ActivityCard, EventCard } from './components/cards'
import { EmptyState, Logo, MotionOverlay, StatusDot } from './components/misc'
type View = type View =
| { name: 'chat' } | { name: 'chat' }
@@ -56,19 +56,27 @@ export default function App() {
} }
}, [history, activity]) }, [history, activity])
const handleSubmit = useCallback( const runTask = useCallback(
(e?: React.SyntheticEvent) => { (task: string) => {
e?.preventDefault() const normalizedTask = task.trim()
if (!inputValue.trim() || status === 'running') return if (!normalizedTask || status === 'running') return
const taskToExecute = inputValue.trim()
setInputValue('') setInputValue('')
setView({ name: 'chat' })
execute(taskToExecute).catch((error) => { execute(normalizedTask).catch((error) => {
console.error('[SidePanel] Failed to execute task:', error) console.error('[SidePanel] Failed to execute task:', error)
}) })
}, },
[inputValue, status, execute] [execute, status]
)
const handleSubmit = useCallback(
(e?: React.SyntheticEvent) => {
e?.preventDefault()
runTask(inputValue)
},
[inputValue, runTask]
) )
const handleStop = useCallback(() => { const handleStop = useCallback(() => {
@@ -103,12 +111,19 @@ export default function App() {
<HistoryList <HistoryList
onSelect={(id) => setView({ name: 'history-detail', sessionId: id })} onSelect={(id) => setView({ name: 'history-detail', sessionId: id })}
onBack={() => setView({ name: 'chat' })} onBack={() => setView({ name: 'chat' })}
onRerun={runTask}
/> />
) )
} }
if (view.name === 'history-detail') { if (view.name === 'history-detail') {
return <HistoryDetail sessionId={view.sessionId} onBack={() => setView({ name: 'history' })} /> return (
<HistoryDetail
sessionId={view.sessionId}
onBack={() => setView({ name: 'history' })}
onRerun={runTask}
/>
)
} }
// --- Chat view --- // --- Chat view ---
@@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from '@/components/ErrorBoundary'
import App from './App' import App from './App'
import { ErrorBoundary } from './components/ErrorBoundary'
import '@/assets/index.css' import '@/assets/index.css'
@@ -0,0 +1,60 @@
import type { HistoricalEvent } from '@page-agent/core'
const EXPORT_FILE_PREFIX = 'page-agent-history'
const MAX_TASK_SLUG_LENGTH = 40
export function serializeHistoryExport(history: HistoricalEvent[]): string {
return `${JSON.stringify(history, null, 2)}\n`
}
export function buildHistoryExportFilename(task: string, createdAt: number): string {
const taskSlug = sanitizeTaskForFilename(task)
const timestamp = formatTimestampForFilename(createdAt)
return taskSlug
? `${EXPORT_FILE_PREFIX}-${taskSlug}-${timestamp}.json`
: `${EXPORT_FILE_PREFIX}-${timestamp}.json`
}
export function downloadHistoryExport(
task: string,
createdAt: number,
history: HistoricalEvent[]
): void {
const filename = buildHistoryExportFilename(task, createdAt)
const content = serializeHistoryExport(history)
const blob = new Blob([content], { type: 'application/json;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function sanitizeTaskForFilename(task: string): string {
return task
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, MAX_TASK_SLUG_LENGTH)
}
function formatTimestampForFilename(createdAt: number): string {
const date = new Date(createdAt)
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`
}
function pad(value: number): string {
return value.toString().padStart(2, '0')
}
+1 -2
View File
@@ -1,2 +1 @@
declare const __EXT_VERSION__: string declare const __VERSION__: string
declare const __CORE_VERSION__: string
+8 -2
View File
@@ -19,8 +19,7 @@ export default defineConfig({
vite: () => ({ vite: () => ({
plugins: [tailwindcss()], plugins: [tailwindcss()],
define: { define: {
__EXT_VERSION__: JSON.stringify(pkg.version), __VERSION__: JSON.stringify(pkg.version),
__CORE_VERSION__: JSON.stringify(pkg.dependencies['@page-agent/core']),
}, },
optimizeDeps: { optimizeDeps: {
force: true, force: true,
@@ -37,7 +36,11 @@ export default defineConfig({
}, },
}, },
}), }),
zip: {
artifactTemplate: 'page-agent-ext-{{version}}-{{browser}}.zip',
},
manifest: { manifest: {
key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbzT0iTYeYlnCvDJIGDnGU8oarJgZILDzSfLi/ufuSxXEPDKuMyD892GhvrMCZNVHS11Sh6NYUOc/PcUOhtaR2urHtcNkrpSJNV10zUamY7fxBdVEkOucfyLu8INVy+teis62MoRWYPaUPkfZUjrLGW8MsZ9aFzARfu9GGDEp2EAYsWDN6w6vyz9LJ82pm542EWnVT4MjmDPgvYFCWGBtaU/dfHD+GAX6URJFapsCvryVURKJ+76c/GO9/I3EX1IBfbY6dec78bLCMvVxiTmiv36KyGPwX1OpakW8IiCpXWdbAxjm+plbYlp5t5zTyyoE3sOSFeXsBH0Kg27o8GcvQIDAQAB',
default_locale: 'en', default_locale: 'en',
name: '__MSG_extName__', name: '__MSG_extName__',
description: '__MSG_extDescription__', description: '__MSG_extDescription__',
@@ -59,5 +62,8 @@ export default defineConfig({
side_panel: { side_panel: {
default_path: 'sidepanel/index.html', default_path: 'sidepanel/index.html',
}, },
externally_connectable: {
matches: ['http://localhost/*'],
},
}, },
}) })
+7 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "1.4.1", "version": "1.6.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",
@@ -37,7 +37,12 @@
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5" "zod": "^4.3.5"
} }
} }
+9 -6
View File
@@ -1,7 +1,7 @@
/** /**
* OpenAI Client implementation * OpenAI Client implementation
*/ */
import * as z from 'zod' import * as z from 'zod/v4'
import { InvokeError, InvokeErrorType } from './errors' import { InvokeError, InvokeErrorType } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types' import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
@@ -29,16 +29,19 @@ export class OpenAIClient implements LLMClient {
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t)) const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))
// Build request body // Build request body
let toolChoice: unknown = 'required'
if (options?.toolChoiceName && !this.config.disableNamedToolChoice) {
toolChoice = { type: 'function', function: { name: options.toolChoiceName } }
}
const requestBody: Record<string, unknown> = { const requestBody: Record<string, unknown> = {
model: this.config.model, model: this.config.model,
temperature: this.config.temperature, temperature: this.config.temperature,
messages, messages,
tools: openaiTools, tools: openaiTools,
parallel_tool_calls: false, parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool tool_choice: toolChoice,
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
} }
modelPatch(requestBody) modelPatch(requestBody)
@@ -50,7 +53,7 @@ export class OpenAIClient implements LLMClient {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`, ...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
}, },
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
signal: abortSignal, signal: abortSignal,
+4 -3
View File
@@ -8,19 +8,20 @@ export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> { export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these) // Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.apiKey || !config.model) { if (!config.baseURL || !config.model) {
throw new Error( throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, apiKey, model. ' + '[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models' 'See: https://alibaba.github.io/page-agent/docs/features/models'
) )
} }
return { return {
baseURL: config.baseURL, baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model, model: config.model,
apiKey: config.apiKey || '',
temperature: config.temperature ?? DEFAULT_TEMPERATURE, temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES, maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
} }
} }
+8 -2
View File
@@ -1,7 +1,7 @@
/** /**
* Core types for LLM integration * Core types for LLM integration
*/ */
import type * as z from 'zod' import type * as z from 'zod/v4'
/** /**
* Message format - OpenAI standard (industry standard) * Message format - OpenAI standard (industry standard)
@@ -89,12 +89,18 @@ export interface InvokeResult<TResult = unknown> {
*/ */
export interface LLMConfig { export interface LLMConfig {
baseURL: string baseURL: string
apiKey: string
model: string model: string
apiKey?: string
temperature?: number temperature?: number
maxRetries?: number maxRetries?: number
/**
* remove the tool_choice field from the request.
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
*/
disableNamedToolChoice?: boolean
/** /**
* Custom fetch function for LLM API requests. * Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc. * Use this to customize headers, credentials, proxy, etc.
+16 -4
View File
@@ -2,13 +2,11 @@
* Utility functions for LLM integration * Utility functions for LLM integration
*/ */
import chalk from 'chalk' import chalk from 'chalk'
import * as z from 'zod' import * as z from 'zod/v4'
import type { Tool } from './types' import type { Tool } from './types'
function debug(message: string) { const debug = console.debug.bind(console, chalk.gray('[LLM]'))
console.debug(chalk.gray('[LLM]'), message)
}
/** /**
* Convert Zod schema to OpenAI tool format * Convert Zod schema to OpenAI tool format
@@ -75,6 +73,11 @@ export function modelPatch(body: Record<string, any>) {
} else if (modelName.startsWith('gpt-51')) { } else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning') debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none' body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-54')) {
debug(
'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'
)
delete body.reasoning_effort
} else if (modelName.startsWith('gpt-5-mini')) { } else if (modelName.startsWith('gpt-5-mini')) {
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1') debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
body.reasoning_effort = 'low' body.reasoning_effort = 'low'
@@ -90,6 +93,15 @@ export function modelPatch(body: Record<string, any>) {
body.reasoning_effort = 'minimal' body.reasoning_effort = 'minimal'
} }
if (modelName.startsWith('minimax')) {
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
// MiniMax API rejects temperature = 0; clamp to a small positive value
body.temperature = Math.max(body.temperature || 0, 0.01)
if (body.temperature > 1) body.temperature = 1
// MiniMax does not support parallel_tool_calls
delete body.parallel_tool_calls
}
return body return body
} }
+1 -1
View File
@@ -25,7 +25,7 @@ export default defineConfig({
}, },
outDir: resolve(__dirname, 'dist', 'lib'), outDir: resolve(__dirname, 'dist', 'lib'),
rollupOptions: { rollupOptions: {
external: ['chalk', 'zod'], external: ['chalk', 'zod', 'zod/v4'],
}, },
minify: false, minify: false,
sourcemap: true, sourcemap: true,
+94
View File
@@ -0,0 +1,94 @@
# @page-agent/mcp
MCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.
## Prerequisites
- Node.js >= 20
- [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome
- An LLM API key (OpenAI-compatible)
## Installation
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"page-agent": {
"command": "npx",
"args": ["-y", "@page-agent/mcp"],
"env": {
"LLM_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"LLM_API_KEY": "sk-xxx",
"LLM_MODEL_NAME": "qwen3.5-plus"
}
}
}
}
```
### Cursor / Copilot
Same format — add the config to the MCP settings of your client.
## MCP Tools
| Tool | Input | Description |
| -------------- | ------------------ | ---------------------------------------------------- |
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
| `get_status` | — | Returns `{ connected, busy }` |
| `stop_task` | — | Stop the currently running task. |
## Environment Variables
| Variable | Default | Description |
| ---------------- | ------- | --------------------- |
| `LLM_BASE_URL` | — | LLM API base URL |
| `LLM_API_KEY` | — | LLM API key |
| `LLM_MODEL_NAME` | — | Model name |
| `PORT` | `38401` | HTTP + WebSocket port |
## How It Works
```
┌──────────────┐ stdio ┌──────────────────┐ WebSocket ┌──────────────┐
│ Claude / │◄────────►│ @page-agent/mcp │◄────────────►│ Hub tab │
│ Copilot │ (MCP) │ (Node.js) │ (localhost) │ (extension) │
└──────────────┘ └──────────────────┘ └──────┬───────┘
│ │
│ HTTP │ useAgent
▼ ▼
┌──────────────────┐ ┌──────────────┐
│ Launcher page │ │ MultiPage │
│ (localhost:PORT) │ │ Agent │
└──────────────────┘ └──────────────┘
```
1. Agent client starts the MCP server via stdio (`npx @page-agent/mcp`).
2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.
3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).
4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.
The hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.
## Architecture
Pure JS ESM, no build step. Source files are the published artifacts.
```
src/
├── index.js # CLI entry: MCP server (stdio) + opens launcher
├── hub-bridge.js # HTTP server + WebSocket bridge to hub tab
└── launcher.html # Bootstrap page: detects extension, triggers hub open
```
## Dev
```bash
npm run build:libs
npm run dev:ext
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
```
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@page-agent/mcp",
"private": false,
"version": "1.6.0",
"type": "module",
"bin": {
"page-agent-mcp": "src/index.js"
},
"files": [
"src/"
],
"description": "MCP server for controlling the browser via Page Agent extension",
"keywords": [
"page-agent",
"mcp",
"browser-automation",
"chrome-extension"
],
"author": "Simon<gaomeng1900>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/alibaba/page-agent.git",
"directory": "packages/mcp"
},
"homepage": "https://alibaba.github.io/page-agent/",
"engines": {
"node": ">=20"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"ws": "^8.19.0",
"zod": "^4.3.5"
}
}
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs'
import http from 'node:http'
import { fileURLToPath } from 'node:url'
import { WebSocketServer } from 'ws'
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
const launcherTemplate = readFileSync(
fileURLToPath(new URL('./launcher.html', import.meta.url)),
'utf-8'
)
/**
* HTTP + WebSocket bridge to the hub.html extension tab.
* - HTTP serves the launcher page (triggers extension to open hub)
* - WS carries execute/stop commands and result/error responses
*/
export class HubBridge {
/** @type {number} */
port
/** @type {http.Server} */
#httpServer
/** @type {WebSocketServer} */
#wss
/** @type {import('ws').WebSocket | null} */
#hub = null
/** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */
#pendingTask = null
/** @param {number} port */
constructor(port) {
this.port = port
this.#httpServer = http.createServer((_req, res) => {
const html = launcherTemplate
.replaceAll('__EXT_ID__', EXT_ID)
.replaceAll('__STORE_URL__', STORE_URL)
.replaceAll('__WS_PORT__', String(port))
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(html)
})
this.#wss = new WebSocketServer({ server: this.#httpServer })
this.#wss.on('connection', (ws) => this.#onConnection(ws))
}
/** @returns {Promise<void>} */
async start() {
return new Promise((resolve, reject) => {
this.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
if (err.code === 'EADDRINUSE') {
reject(
new Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)
)
} else {
reject(err)
}
})
this.#httpServer.listen(this.port, () => {
console.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`)
resolve()
})
})
}
get connected() {
return this.#hub?.readyState === 1
}
get busy() {
return this.#pendingTask !== null
}
/**
* @param {string} task
* @param {Record<string, unknown>} [config]
* @returns {Promise<{success: boolean, data: string}>}
*/
async executeTask(task, config) {
if (!this.connected) throw new Error('Hub is not connected. Is the extension running?')
if (this.#pendingTask) throw new Error('Agent is already running a task.')
return new Promise((resolve, reject) => {
this.#pendingTask = { resolve, reject }
this.#hub.send(JSON.stringify({ type: 'execute', task, config }))
})
}
stopTask() {
if (this.connected) {
this.#hub.send(JSON.stringify({ type: 'stop' }))
}
}
// TODO: Add version checking
/** @param {import('ws').WebSocket} ws */
#onConnection(ws) {
if (this.#hub && this.#hub.readyState === 1) {
ws.close(4000, 'Another hub is already connected')
return
}
this.#hub = ws
console.error('[page-agent-mcp] Hub connected')
ws.on('message', (/** @type {Buffer} */ rawData) => {
/** @type {{ type: string, success?: boolean, data?: string, message?: string }} */
let msg
try {
msg = JSON.parse(rawData.toString('utf-8'))
} catch {
return
}
if (msg.type === 'result') {
this.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })
this.#pendingTask = null
} else if (msg.type === 'error') {
this.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))
this.#pendingTask = null
}
})
ws.on('close', () => {
console.error('[page-agent-mcp] Hub disconnected')
if (this.#hub === ws) this.#hub = null
if (this.#pendingTask) {
this.#pendingTask.reject(new Error('Hub disconnected while task was running'))
this.#pendingTask = null
}
})
}
}
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { exec } from 'node:child_process'
import { platform } from 'node:os'
import * as z from 'zod/v4'
import { HubBridge } from './hub-bridge.js'
const env = process.env
const port = parseInt(env.PORT || '38401')
/** @type {Record<string, string>} */
const llmConfig = {}
if (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL
if (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME
if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
// --- Hub bridge (HTTP + WebSocket) ---
const hub = new HubBridge(port)
await hub.start()
// Open launcher in default browser
const url = `http://localhost:${port}`
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start ""' : 'xdg-open'
exec(`${cmd} "${url}"`, (err) => {
if (err) console.error(`[page-agent-mcp] Could not open browser: ${err.message}`)
})
// --- MCP server (stdio) ---
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
mcpServer.registerTool(
'execute_task',
{
description:
'Execute a browser automation task described in natural language. ' +
'The Page Agent extension will control the browser to complete the task. ' +
'Blocks until the task is complete.',
inputSchema: { task: z.string().describe('Task description in natural language') },
},
async ({ task }) => {
try {
const config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined
const result = await hub.executeTask(task, config)
return {
content: [
{
type: 'text',
text: result.success
? `Task completed successfully.\n\n${result.data}`
: `Task failed.\n\n${result.data}`,
},
],
}
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true,
}
}
}
)
mcpServer.registerTool(
'get_status',
{
description: 'Check the current status of the Page Agent hub connection and agent.',
},
async () => ({
content: [
{
type: 'text',
text: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),
},
],
})
)
mcpServer.registerTool(
'stop_task',
{
description: 'Stop the currently running browser automation task.',
},
async () => {
hub.stopTask()
return { content: [{ type: 'text', text: 'Stop signal sent.' }] }
}
)
const transport = new StdioServerTransport()
await mcpServer.connect(transport)
console.error('[page-agent-mcp] MCP server ready (stdio)')
+237
View File
@@ -0,0 +1,237 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png" />
<title>Page Agent MCP Launcher</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
background: #09090b;
color: #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.card {
text-align: center;
max-width: 480px;
padding: 3rem 2rem;
}
.logo {
width: 72px;
height: 72px;
border-radius: 18px;
margin-bottom: 1rem;
}
.badge {
font-size: 0.6875rem;
font-weight: 500;
color: #52525b;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
h1 {
font-size: 1.35rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.sub {
font-size: 0.875rem;
color: #a1a1aa;
line-height: 1.7;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #27272a;
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 1.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
a {
color: #60a5fa;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.install {
display: none;
}
.install.show {
display: block;
}
.tips {
margin-top: 1.75rem;
text-align: left;
background: #18181b;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.25rem 1.5rem;
}
.tips li {
font-size: 0.8125rem;
color: #a1a1aa;
line-height: 1.7;
margin-left: 1rem;
}
.tips li + li {
margin-top: 0.35rem;
}
.store-btn {
display: inline-flex;
align-items: center;
gap: 0.625rem;
margin-top: 1.5rem;
padding: 0.625rem 1.5rem;
background: #2563eb;
color: #fff;
border-radius: 10px;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.15s;
}
.store-btn:hover {
background: #1d4ed8;
text-decoration: none;
}
.store-btn img {
width: 20px;
height: 20px;
}
.links {
margin-top: 1.75rem;
display: flex;
justify-content: center;
gap: 1.5rem;
font-size: 0.8125rem;
}
.links a {
color: #71717a;
transition: color 0.15s;
}
.links a:hover {
color: #a1a1aa;
text-decoration: none;
}
</style>
</head>
<body>
<div class="card">
<img
class="logo"
src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
alt="Page Agent"
/>
<div class="badge">Page Agent MCP Launcher</div>
<div id="connecting">
<div class="spinner"></div>
<h1 data-i18n="connecting_title">Connecting to Page Agent</h1>
<p class="sub" data-i18n="connecting_sub">Opening the hub in your browser…</p>
</div>
<div id="install" class="install">
<h1 data-i18n="install_title">Extension Required</h1>
<p class="sub" data-i18n="install_sub">
Page Agent requires the latest browser extension to work.
</p>
<a class="store-btn" href="__STORE_URL__" target="_blank">
<img
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
alt=""
/>
<span data-i18n="install_btn">Install from Chrome Web Store</span>
</a>
<ul class="tips">
<li data-i18n="tip_outdated">
If the extension is outdated, please update it to the latest version.
</li>
<li data-i18n="tip_other_browser">
If the extension is not installed in this browser, open this page from the
browser that has it installed.
</li>
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
</ul>
</div>
<div class="links">
<a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank" data-i18n="link_docs">Docs</a>
<a href="https://github.com/alibaba/page-agent/issues" target="_blank" data-i18n="link_issues">Report an Issue</a>
</div>
</div>
<script>
{
const EXT_ID = '__EXT_ID__'
const wsPort = __WS_PORT__
const zh = {
connecting_title: '正在连接 Page Agent',
connecting_sub: '正在浏览器中打开 Hub…',
install_title: '需要安装浏览器插件',
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
install_btn: '从 Chrome 应用商店安装',
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
tip_other_browser:
'如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
tip_refresh: '安装或更新后,请刷新此页面。',
link_docs: '文档',
link_issues: '问题反馈',
}
if (/^zh\b/i.test(navigator.language)) {
document.querySelectorAll('[data-i18n]').forEach((el) => {
const key = el.getAttribute('data-i18n')
if (zh[key]) el.textContent = zh[key]
})
}
const showInstall = () => {
document.getElementById('connecting').style.display = 'none'
document.getElementById('install').classList.add('show')
}
try {
if (!globalThis.chrome?.runtime?.sendMessage) {
showInstall()
} else {
chrome.runtime.sendMessage(
EXT_ID,
{ type: 'OPEN_HUB', wsPort },
(response) => {
if (chrome.runtime.lastError || !response?.ok) showInstall()
}
)
}
} catch {
showInstall()
}
}
</script>
</body>
</html>
+11 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "page-agent", "name": "page-agent",
"private": false, "private": false,
"version": "1.4.1", "version": "1.6.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",
@@ -44,11 +44,16 @@
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.4.1", "@page-agent/core": "1.6.0",
"@page-agent/llms": "1.4.1", "@page-agent/llms": "1.6.0",
"@page-agent/page-controller": "1.4.1", "@page-agent/page-controller": "1.6.0",
"@page-agent/ui": "1.4.1", "@page-agent/ui": "1.6.0",
"chalk": "^5.6.2", "chalk": "^5.6.2"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"devDependencies": {
"zod": "^4.3.5" "zod": "^4.3.5"
} }
} }
+5 -4
View File
@@ -2,12 +2,13 @@
* Copyright (C) 2025 Alibaba Group Holding Limited * Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved. * All rights reserved.
*/ */
import { type PageAgentConfig, PageAgentCore, type PageAgentTool, tool } from '@page-agent/core' import { type AgentConfig, PageAgentCore } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller' import { PageController, type PageControllerConfig } from '@page-agent/page-controller'
import { Panel } from '@page-agent/ui' import { Panel } from '@page-agent/ui'
export type { PageAgentConfig, PageAgentTool } export * from '@page-agent/core'
export { tool }
export type PageAgentConfig = AgentConfig & PageControllerConfig
export class PageAgent extends PageAgentCore { export class PageAgent extends PageAgentCore {
panel: Panel panel: Panel
+1
View File
@@ -30,6 +30,7 @@ export default defineConfig({
external: [ external: [
'chalk', 'chalk',
'zod', 'zod',
'zod/v4',
// all the internal packages // all the internal packages
/^@page-agent\//, /^@page-agent\//,
], ],
+1 -1
View File
@@ -9,7 +9,7 @@ import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const __dirname = dirname(fileURLToPath(import.meta.url)) const __dirname = dirname(fileURLToPath(import.meta.url))
// Load .env from repo root // Load .env from repo root
dotenvConfig({ path: resolve(__dirname, '../../.env') }) dotenvConfig({ path: resolve(__dirname, '../../.env'), quiet: true })
// UMD Bundle for CDN // UMD Bundle for CDN
// - alias all local packages so that they can be build in // - alias all local packages so that they can be build in
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "1.4.1", "version": "1.6.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",
@@ -14,17 +14,16 @@ import {
scrollVertically, scrollVertically,
selectOptionElement, selectOptionElement,
} from './actions' } from './actions'
import { VIEWPORT_EXPANSION } from './constants'
import * as dom from './dom' import * as dom from './dom'
import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type' import type { FlatDomTree, InteractiveElementDomNode } from './dom/dom_tree/type'
import { getPageInfo } from './dom/getPageInfo' import { getPageInfo } from './dom/getPageInfo'
import { patchReact } from './patches/react' import { patchReact } from './patches/react'
import { isAnchorElement } from './utils'
/** /**
* Configuration for PageController * Configuration for PageController
*/ */
export interface PageControllerConfig extends dom.DomConfig { export interface PageControllerConfig extends dom.DomConfig {
viewportExpansion?: number
/** Enable visual mask overlay during operations (default: false) */ /** Enable visual mask overlay during operations (default: false) */
enableMask?: boolean enableMask?: boolean
} }
@@ -131,7 +130,7 @@ export class PageController extends EventTarget {
const url = window.location.href const url = window.location.href
const title = document.title const title = document.title
const pi = getPageInfo() const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION const viewportExpansion = dom.resolveViewportExpansion(this.config.viewportExpansion)
await this.updateTree() await this.updateTree()
@@ -245,7 +244,7 @@ export class PageController extends EventTarget {
await clickElement(element) await clickElement(element)
// Handle links that open in new tabs // Handle links that open in new tabs
if (element instanceof HTMLAnchorElement && element.target === '_blank') { if (isAnchorElement(element) && element.target === '_blank') {
return { return {
success: true, success: true,
message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`, message: `✅ Clicked element (${elemText ?? index}). ⚠️ Link opened in a new tab.`,
+122 -134
View File
@@ -3,24 +3,15 @@
* All rights reserved. * All rights reserved.
*/ */
import type { InteractiveElementDomNode } from './dom/dom_tree/type' import type { InteractiveElementDomNode } from './dom/dom_tree/type'
import {
// ======= general utils ======= getNativeValueSetter,
isHTMLElement,
async function waitFor(seconds: number): Promise<void> { isInputElement,
await new Promise((resolve) => setTimeout(resolve, seconds * 1000)) isSelectElement,
} isTextAreaElement,
movePointerToElement,
// ======= dom utils ======= waitFor,
} from './utils'
export async function movePointerToElement(element: HTMLElement) {
const rect = element.getBoundingClientRect()
const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
window.dispatchEvent(new CustomEvent('PageAgent::MovePointerTo', { detail: { x, y } }))
await waitFor(0.3)
}
/** /**
* Get the HTMLElement by index from a selectorMap. * Get the HTMLElement by index from a selectorMap.
@@ -39,7 +30,7 @@ export function getElementByIndex(
throw new Error(`Element at index ${index} does not have a reference`) throw new Error(`Element at index ${index} does not have a reference`)
} }
if (!(element instanceof HTMLElement)) { if (!isHTMLElement(element)) {
throw new Error(`Element at index ${index} is not an HTMLElement`) throw new Error(`Element at index ${index} is not an HTMLElement`)
} }
@@ -54,6 +45,9 @@ function blurLastClickedElement() {
lastClickedElement.dispatchEvent( lastClickedElement.dispatchEvent(
new MouseEvent('mouseout', { bubbles: true, cancelable: true }) new MouseEvent('mouseout', { bubbles: true, cancelable: true })
) )
lastClickedElement.dispatchEvent(
new MouseEvent('mouseleave', { bubbles: false, cancelable: true })
)
lastClickedElement = null lastClickedElement = null
} }
} }
@@ -65,9 +59,15 @@ export async function clickElement(element: HTMLElement) {
blurLastClickedElement() blurLastClickedElement()
lastClickedElement = element lastClickedElement = element
await scrollIntoViewIfNeeded(element) await scrollIntoViewIfNeeded(element)
// Scroll the iframe element itself into view if needed
const frame = element.ownerDocument.defaultView?.frameElement
if (frame) await scrollIntoViewIfNeeded(frame)
await movePointerToElement(element) await movePointerToElement(element)
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer')) window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
await waitFor(0.1) await waitFor(0.1)
// hover it // hover it
@@ -86,56 +86,110 @@ export async function clickElement(element: HTMLElement) {
// dispatch a click event // dispatch a click event
// element.click() // element.click()
await waitFor(0.1) // Wait to ensure click event processing completes await waitFor(0.2) // Wait to ensure click event processing completes
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
)!.set!
// eslint-disable-next-line @typescript-eslint/unbound-method
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
)!.set!
/**
* create a synthetic keyboard event
* with key keycode code
*/
export async function createSyntheticInputEvent(elem: HTMLElement, key: string) {
elem.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key }))
await waitFor(0.01)
if (elem instanceof HTMLInputElement || elem instanceof HTMLTextAreaElement) {
elem.dispatchEvent(new Event('beforeinput', { bubbles: true }))
await waitFor(0.01)
elem.dispatchEvent(new Event('input', { bubbles: true }))
await waitFor(0.01)
}
elem.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key }))
} }
export async function inputTextElement(element: HTMLElement, text: string) { export async function inputTextElement(element: HTMLElement, text: string) {
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) { const isContentEditable = element.isContentEditable
throw new Error('Element is not an input or textarea') if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {
throw new Error('Element is not an input, textarea, or contenteditable')
} }
await clickElement(element) await clickElement(element)
if (element instanceof HTMLTextAreaElement) { if (isContentEditable) {
nativeTextAreaValueSetter.call(element, text) // Contenteditable support (partial)
} else { // Not supported:
nativeInputValueSetter.call(element, text) // - Monaco/CodeMirror: Require direct JS instance access. No universal way to obtain.
// - Draft.js: Not responsive to synthetic/execCommand/Range/DataTransfer. Unmaintained.
//
// Strategy: Try Plan A (synthetic events) first, then verify and fall back
// to Plan B (execCommand) if the text wasn't actually inserted.
//
// Plan A: Dispatch synthetic events
// Works: React contenteditable, Quill.
// Fails: Slate.js, some contenteditable editors that ignore synthetic events.
// Sequence: beforeinput -> mutation -> input -> change -> blur
// Dispatch beforeinput + mutation + input for clearing
if (
element.dispatchEvent(
new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'deleteContent',
})
)
) {
element.innerText = ''
element.dispatchEvent(
new InputEvent('input', {
bubbles: true,
inputType: 'deleteContent',
})
)
} }
const inputEvent = new Event('input', { bubbles: true }) // Dispatch beforeinput + mutation + input for insertion (important for React apps)
element.dispatchEvent(inputEvent) if (
element.dispatchEvent(
new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: text,
})
)
) {
element.innerText = text
element.dispatchEvent(
new InputEvent('input', {
bubbles: true,
inputType: 'insertText',
data: text,
})
)
}
await waitFor(0.1) // Wait to ensure input event processing completes // Verify Plan A worked by checking if the text was actually inserted
const planASucceeded = element.innerText.trim() === text.trim()
if (!planASucceeded) {
// Plan B: execCommand fallback (deprecated but widely supported)
// Works: Quill, Slate.js, react contenteditable components.
// This approach integrates with the browser's undo stack and is handled
// natively by most rich-text editors.
element.focus()
// Select all existing content and delete it
const doc = element.ownerDocument
const selection = (doc.defaultView || window).getSelection()
const range = doc.createRange()
range.selectNodeContents(element)
selection?.removeAllRanges()
selection?.addRange(range)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('delete', false)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('insertText', false, text)
}
// Dispatch change event (for good measure)
element.dispatchEvent(new Event('change', { bubbles: true }))
// Trigger blur for validation
element.blur()
} else {
getNativeValueSetter(element as HTMLInputElement | HTMLTextAreaElement).call(element, text)
}
// Only dispatch shared input event for non-contenteditable (contenteditable has its own)
if (!isContentEditable) {
element.dispatchEvent(new Event('input', { bubbles: true }))
}
await waitFor(0.1)
blurLastClickedElement() blurLastClickedElement()
} }
@@ -144,7 +198,7 @@ export async function inputTextElement(element: HTMLElement, text: string) {
* @todo browser-use version is very complex and supports menu tags, need to follow up * @todo browser-use version is very complex and supports menu tags, need to follow up
*/ */
export async function selectOptionElement(selectElement: HTMLSelectElement, optionText: string) { export async function selectOptionElement(selectElement: HTMLSelectElement, optionText: string) {
if (!(selectElement instanceof HTMLSelectElement)) { if (!isSelectElement(selectElement)) {
throw new Error('Element is not a select element') throw new Error('Element is not a select element')
} }
@@ -161,14 +215,18 @@ export async function selectOptionElement(selectElement: HTMLSelectElement, opti
await waitFor(0.1) // Wait to ensure change event processing completes await waitFor(0.1) // Wait to ensure change event processing completes
} }
export async function scrollIntoViewIfNeeded(element: HTMLElement) { interface ScrollableElement extends Element {
const el = element as any scrollIntoViewIfNeeded?: (centerIfNeeded?: boolean) => void
if (el.scrollIntoViewIfNeeded) { }
export async function scrollIntoViewIfNeeded(element: Element) {
const el = element as ScrollableElement
if (typeof el.scrollIntoViewIfNeeded === 'function') {
el.scrollIntoViewIfNeeded() el.scrollIntoViewIfNeeded()
// await waitFor(0.5) // Animation playback // await waitFor(0.5) // Animation playback
} else { } else {
// @todo visibility check // @todo visibility check
el.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' }) element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
// await waitFor(0.5) // Animation playback // await waitFor(0.5) // Animation playback
} }
} }
@@ -181,11 +239,6 @@ export async function scrollVertically(
// Element-specific scrolling if element is provided // Element-specific scrolling if element is provided
if (element) { if (element) {
const targetElement = element const targetElement = element
console.log(
'[SCROLL DEBUG] Starting direct container scroll for element:',
targetElement.tagName
)
let currentElement = targetElement as HTMLElement | null let currentElement = targetElement as HTMLElement | null
let scrollSuccess = false let scrollSuccess = false
let scrolledElement: HTMLElement | null = null let scrolledElement: HTMLElement | null = null
@@ -198,19 +251,6 @@ export async function scrollVertically(
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY) const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
console.log(
'[SCROLL DEBUG] Checking element:',
currentElement.tagName,
'hasScrollableY:',
hasScrollableY,
'canScrollVertically:',
canScrollVertically,
'scrollHeight:',
currentElement.scrollHeight,
'clientHeight:',
currentElement.clientHeight
)
if (hasScrollableY && canScrollVertically) { if (hasScrollableY && canScrollVertically) {
const beforeScroll = currentElement.scrollTop const beforeScroll = currentElement.scrollTop
const maxScroll = currentElement.scrollHeight - currentElement.clientHeight const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
@@ -228,27 +268,10 @@ export async function scrollVertically(
const afterScroll = currentElement.scrollTop const afterScroll = currentElement.scrollTop
const actualScrollDelta = afterScroll - beforeScroll const actualScrollDelta = afterScroll - beforeScroll
console.log(
'[SCROLL DEBUG] Scroll attempt:',
currentElement.tagName,
'before:',
beforeScroll,
'after:',
afterScroll,
'delta:',
actualScrollDelta
)
if (Math.abs(actualScrollDelta) > 0.5) { if (Math.abs(actualScrollDelta) > 0.5) {
scrollSuccess = true scrollSuccess = true
scrolledElement = currentElement scrolledElement = currentElement
scrollDelta = actualScrollDelta scrollDelta = actualScrollDelta
console.log(
'[SCROLL DEBUG] Successfully scrolled container:',
currentElement.tagName,
'delta:',
actualScrollDelta
)
break break
} }
} }
@@ -344,11 +367,6 @@ export async function scrollHorizontally(
// Element-specific scrolling if element is provided // Element-specific scrolling if element is provided
if (element) { if (element) {
const targetElement = element const targetElement = element
console.log(
'[SCROLL DEBUG] Starting direct container scroll for element:',
targetElement.tagName
)
let currentElement = targetElement as HTMLElement | null let currentElement = targetElement as HTMLElement | null
let scrollSuccess = false let scrollSuccess = false
let scrolledElement: HTMLElement | null = null let scrolledElement: HTMLElement | null = null
@@ -361,19 +379,6 @@ export async function scrollHorizontally(
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX) const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
console.log(
'[SCROLL DEBUG] Checking element:',
currentElement.tagName,
'hasScrollableX:',
hasScrollableX,
'canScrollHorizontally:',
canScrollHorizontally,
'scrollWidth:',
currentElement.scrollWidth,
'clientWidth:',
currentElement.clientWidth
)
if (hasScrollableX && canScrollHorizontally) { if (hasScrollableX && canScrollHorizontally) {
const beforeScroll = currentElement.scrollLeft const beforeScroll = currentElement.scrollLeft
const maxScroll = currentElement.scrollWidth - currentElement.clientWidth const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
@@ -391,27 +396,10 @@ export async function scrollHorizontally(
const afterScroll = currentElement.scrollLeft const afterScroll = currentElement.scrollLeft
const actualScrollDelta = afterScroll - beforeScroll const actualScrollDelta = afterScroll - beforeScroll
console.log(
'[SCROLL DEBUG] Scroll attempt:',
currentElement.tagName,
'before:',
beforeScroll,
'after:',
afterScroll,
'delta:',
actualScrollDelta
)
if (Math.abs(actualScrollDelta) > 0.5) { if (Math.abs(actualScrollDelta) > 0.5) {
scrollSuccess = true scrollSuccess = true
scrolledElement = currentElement scrolledElement = currentElement
scrollDelta = actualScrollDelta scrollDelta = actualScrollDelta
console.log(
'[SCROLL DEBUG] Successfully scrolled container:',
currentElement.tagName,
'delta:',
actualScrollDelta
)
break break
} }
} }
-16
View File
@@ -1,16 +0,0 @@
/**
* Copyright (C) 2025 Alibaba Group Holding Limited
* All rights reserved.
*/
/**
* Viewport expansion for DOM tree extraction.
* -1 means full page (no viewport restriction)
* 0 means viewport only
* positive values expand the viewport by that many pixels
*
* @note Since isTopElement depends on elementFromPoint,
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
*/
// export const VIEWPORT_EXPANSION = 100
export const VIEWPORT_EXPANSION = -1
@@ -17,6 +17,7 @@
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
* @edit improve `sampleRect`, filter out rects with 0 area * @edit improve `sampleRect`, filter out rects with 0 area
* @edit exclude aria-hidden elements * @edit exclude aria-hidden elements
* @edit make sure attributes exist for interactive candidates.
*/ */
export default ( export default (
@@ -1446,7 +1447,7 @@ export default (
/** /**
* @edit add `data-browser-use-ignore` attribute * @edit add `data-browser-use-ignore` attribute
*/ */
if (node.dataset?.browserUseIgnore === 'true') { if (node.dataset?.browserUseIgnore === 'true' || node.dataset?.pageAgentIgnore === 'true') {
return null // Skip this node and its children return null // Skip this node and its children
} }
@@ -1608,6 +1609,18 @@ export default (
* @edit direct dom ref * @edit direct dom ref
*/ */
nodeData.ref = node nodeData.ref = node
/**
* @edit make sure attributes exist for interactive candidates.
* @note if the element failed the isInteractiveCandidate, attributes would be empty.
*/
if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
const attributeNames = node.getAttributeNames?.() || []
for (const name of attributeNames) {
const value = node.getAttribute(name)
nodeData.attributes[name] = value
}
}
} }
} }
} }
+64 -15
View File
@@ -1,4 +1,3 @@
import { VIEWPORT_EXPANSION } from '../constants'
import domTree from './dom_tree/index.js' import domTree from './dom_tree/index.js'
import { import {
ElementDomNode, ElementDomNode,
@@ -7,7 +6,23 @@ import {
TextDomNode, TextDomNode,
} from './dom_tree/type' } from './dom_tree/type'
/**
* Viewport expansion for DOM tree extraction.
* -1 means full page (no viewport restriction)
* 0 means viewport only
* positive values expand the viewport by that many pixels
*
* @note Since isTopElement depends on elementFromPoint,
* it returns null when out of viewport, this feature has no practical use, only differ between -1 and 0
*/
const DEFAULT_VIEWPORT_EXPANSION = -1
export function resolveViewportExpansion(viewportExpansion?: number): number {
return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION
}
export interface DomConfig { export interface DomConfig {
viewportExpansion?: number
interactiveBlacklist?: (Element | (() => Element))[] interactiveBlacklist?: (Element | (() => Element))[]
interactiveWhitelist?: (Element | (() => Element))[] interactiveWhitelist?: (Element | (() => Element))[]
includeAttributes?: string[] includeAttributes?: string[]
@@ -21,6 +36,8 @@ export interface DomConfig {
const newElementsCache = new WeakMap<HTMLElement, string>() const newElementsCache = new WeakMap<HTMLElement, string>()
export function getFlatTree(config: DomConfig): FlatDomTree { export function getFlatTree(config: DomConfig): FlatDomTree {
const viewportExpansion = resolveViewportExpansion(config.viewportExpansion)
const interactiveBlacklist = [] as Element[] const interactiveBlacklist = [] as Element[]
for (const item of config.interactiveBlacklist || []) { for (const item of config.interactiveBlacklist || []) {
if (typeof item === 'function') { if (typeof item === 'function') {
@@ -43,7 +60,7 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
doHighlightElements: true, doHighlightElements: true,
debugMode: true, debugMode: true,
focusHighlightIndex: -1, focusHighlightIndex: -1,
viewportExpansion: VIEWPORT_EXPANSION, viewportExpansion,
interactiveBlacklist, interactiveBlacklist,
interactiveWhitelist, interactiveWhitelist,
highlightOpacity: config.highlightOpacity ?? 0.0, highlightOpacity: config.highlightOpacity ?? 0.0,
@@ -74,6 +91,43 @@ export function getFlatTree(config: DomConfig): FlatDomTree {
return elements return elements
} }
const globRegexCache = new Map<string, RegExp>()
function globToRegex(pattern: string): RegExp {
let regex = globRegexCache.get(pattern)
if (!regex) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`)
globRegexCache.set(pattern, regex)
}
return regex
}
function matchAttributes(
attrs: Record<string, string>,
patterns: string[]
): Record<string, string> {
const result: Record<string, string> = {}
for (const pattern of patterns) {
if (pattern.includes('*')) {
const regex = globToRegex(pattern)
for (const key of Object.keys(attrs)) {
if (regex.test(key) && attrs[key].trim()) {
result[key] = attrs[key].trim()
}
}
} else {
const value = attrs[pattern]
if (value && value.trim()) {
result[pattern] = value.trim()
}
}
}
return result
}
/** /**
* elementsToString 使 * elementsToString 使
*/ */
@@ -140,10 +194,13 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
// for jump check // for jump check
'target', 'target',
// absolute 定位的下拉菜单 // absolute position dropdown menu
'aria-haspopup', 'aria-haspopup',
'aria-controls', 'aria-controls',
'aria-owns', 'aria-owns',
// content editable
'contenteditable',
] ]
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES] const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
@@ -245,23 +302,15 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
let attributesHtmlStr = '' let attributesHtmlStr = ''
if (includeAttrs.length > 0 && node.attributes) { if (includeAttrs.length > 0 && node.attributes) {
const attributesToInclude: Record<string, string> = {} const attributesToInclude = matchAttributes(node.attributes, includeAttrs)
// Filter attributes
for (const key of includeAttrs) {
const value = node.attributes[key]
if (value && value.trim() !== '') {
attributesToInclude[key] = value.trim()
}
}
// Remove duplicate values (for attributes longer than 5 chars) // Remove duplicate values (for attributes longer than 5 chars)
const orderedKeys = includeAttrs.filter((key) => key in attributesToInclude) const keys = Object.keys(attributesToInclude)
if (orderedKeys.length > 1) { if (keys.length > 1) {
const keysToRemove = new Set<string>() const keysToRemove = new Set<string>()
const seenValues: Record<string, string> = {} const seenValues: Record<string, string> = {}
for (const key of orderedKeys) { for (const key of keys) {
const value = attributesToInclude[key] const value = attributesToInclude[key]
if (value.length > 5) { if (value.length > 5) {
if (value in seenValues) { if (value in seenValues) {
@@ -7,3 +7,7 @@
display: none; display: none;
} }
.wrapper.visible {
display: block;
}
@@ -8,13 +8,7 @@ import cursorStyles from './cursor.module.css'
export class SimulatorMask { export class SimulatorMask {
shown: boolean = false shown: boolean = false
wrapper = document.createElement('div') wrapper = document.createElement('div')
motion = new Motion({ motion: Motion | null = null
mode: isPageDark() ? 'dark' : 'light',
styles: {
position: 'absolute',
inset: '0',
},
})
#cursor = document.createElement('div') #cursor = document.createElement('div')
@@ -28,9 +22,19 @@ export class SimulatorMask {
this.wrapper.id = 'page-agent-runtime_simulator-mask' this.wrapper.id = 'page-agent-runtime_simulator-mask'
this.wrapper.className = styles.wrapper this.wrapper.className = styles.wrapper
this.wrapper.setAttribute('data-browser-use-ignore', 'true') this.wrapper.setAttribute('data-browser-use-ignore', 'true')
this.wrapper.setAttribute('data-page-agent-ignore', 'true')
this.wrapper.appendChild(this.motion.element) try {
this.motion.autoResize(this.wrapper) const motion = new Motion({
mode: isPageDark() ? 'dark' : 'light',
styles: { position: 'absolute', inset: '0' },
})
this.motion = motion
this.wrapper.appendChild(motion.element)
motion.autoResize(this.wrapper)
} catch (e) {
console.warn('[SimulatorMask] Motion overlay unavailable:', e)
}
// Capture all mouse, keyboard, and wheel events // Capture all mouse, keyboard, and wheel events
this.wrapper.addEventListener('click', (e) => { this.wrapper.addEventListener('click', (e) => {
@@ -144,10 +148,10 @@ export class SimulatorMask {
if (this.shown) return if (this.shown) return
this.shown = true this.shown = true
this.motion.start() this.motion?.start()
this.motion.fadeIn() this.motion?.fadeIn()
this.wrapper.style.display = 'block' this.wrapper.classList.add(styles.visible)
// Initialize cursor position // Initialize cursor position
this.#currentCursorX = window.innerWidth / 2 this.#currentCursorX = window.innerWidth / 2
@@ -162,18 +166,18 @@ export class SimulatorMask {
if (!this.shown) return if (!this.shown) return
this.shown = false this.shown = false
this.motion.fadeOut() this.motion?.fadeOut()
this.motion.pause() this.motion?.pause()
this.#cursor.classList.remove(cursorStyles.clicking) this.#cursor.classList.remove(cursorStyles.clicking)
setTimeout(() => { setTimeout(() => {
this.wrapper.style.display = 'none' this.wrapper.classList.remove(styles.visible)
}, 800) // Match the animation duration }, 800) // Match the animation duration
} }
dispose() { dispose() {
this.motion.dispose() this.motion?.dispose()
this.wrapper.remove() this.wrapper.remove()
} }
} }
@@ -0,0 +1,62 @@
// ======= type guards =======
// @note instanceof fails for elements inside iframes
export function isHTMLElement(el: unknown): el is HTMLElement {
// @todo either specify to HTMLElement or allow Element here.
return !!el && (el as Node).nodeType === 1
}
export function isInputElement(el: Element): el is HTMLInputElement {
return el?.nodeType === 1 && el.tagName === 'INPUT'
}
export function isTextAreaElement(el: Element): el is HTMLTextAreaElement {
return el?.nodeType === 1 && el.tagName === 'TEXTAREA'
}
export function isSelectElement(el: Element): el is HTMLSelectElement {
return el?.nodeType === 1 && el.tagName === 'SELECT'
}
export function isAnchorElement(el: Element): el is HTMLAnchorElement {
return el?.nodeType === 1 && el.tagName === 'A'
}
// ======= iframe helpers =======
/** Iframe offset for translating element coordinates to top-frame viewport. */
export function getIframeOffset(element: HTMLElement): { x: number; y: number } {
const frame = element.ownerDocument.defaultView?.frameElement as HTMLElement | null
if (!frame) return { x: 0, y: 0 }
const rect = frame.getBoundingClientRect()
return { x: rect.left, y: rect.top }
}
/**
* Get native value setter from the element's own prototype (iframe-safe).
* @note for React
*/
export function getNativeValueSetter(element: HTMLInputElement | HTMLTextAreaElement) {
// eslint-disable-next-line @typescript-eslint/unbound-method
return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element) as object, 'value')!
.set as (v: string) => void
}
// ======= general utils =======
export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
}
// ======= dom utils =======
export async function movePointerToElement(element: HTMLElement) {
const rect = element.getBoundingClientRect()
const offset = getIframeOffset(element)
const x = rect.left + rect.width / 2 + offset.x
const y = rect.top + rect.height / 2 + offset.y
window.dispatchEvent(new CustomEvent('PageAgent::MovePointerTo', { detail: { x, y } }))
await waitFor(0.3)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "1.4.1", "version": "1.6.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",
+1
View File
@@ -373,6 +373,7 @@ export class Panel {
wrapper.id = 'page-agent-runtime_agent-panel' wrapper.id = 'page-agent-runtime_agent-panel'
wrapper.className = styles.wrapper wrapper.className = styles.wrapper
wrapper.setAttribute('data-browser-use-ignore', 'true') wrapper.setAttribute('data-browser-use-ignore', 'true')
wrapper.setAttribute('data-page-agent-ignore', 'true')
wrapper.innerHTML = ` wrapper.innerHTML = `
<div class="${styles.background}"></div> <div class="${styles.background}"></div>
+4 -4
View File
@@ -18,7 +18,7 @@ interface CardOptions {
export function createCard({ icon, content, meta, type }: CardOptions): string { export function createCard({ icon, content, meta, type }: CardOptions): string {
const typeClass = type ? styles[type] : '' const typeClass = type ? styles[type] : ''
const contentHtml = Array.isArray(content) const contentHtml = Array.isArray(content)
? `<div class="${styles.reflectionLines}">${content.join('')}</div>` ? `<div class="${styles.reflectionLines}">${content.map((line) => `<span>${escapeHtml(line)}</span>`).join('')}</div>`
: `<span>${escapeHtml(content)}</span>` : `<span>${escapeHtml(content)}</span>`
return ` return `
@@ -40,13 +40,13 @@ export function createReflectionLines(reflection: {
}): string[] { }): string[] {
const lines: string[] = [] const lines: string[] = []
if (reflection.evaluation_previous_goal) { if (reflection.evaluation_previous_goal) {
lines.push(`<div>🔍 ${escapeHtml(reflection.evaluation_previous_goal)}</div>`) lines.push(`🔍 ${reflection.evaluation_previous_goal}`)
} }
if (reflection.memory) { if (reflection.memory) {
lines.push(`<div>💾 ${escapeHtml(reflection.memory)}</div>`) lines.push(`💾 ${reflection.memory}`)
} }
if (reflection.next_goal) { if (reflection.next_goal) {
lines.push(`<div>🎯 ${escapeHtml(reflection.next_goal)}</div>`) lines.push(`🎯 ${reflection.next_goal}`)
} }
return lines return lines
} }
+1 -1
View File
@@ -62,7 +62,7 @@ Located in `src/components/ui/`:
src/ src/
├── pages/ ├── pages/
│ ├── home/ │ ├── home/
│ │ ├── HomeContent.tsx # Homepage sections │ │ ├── index.tsx # Homepage
│ │ └── ...Section.tsx │ │ └── ...Section.tsx
│ └── docs/ │ └── docs/
│ ├── index.tsx # Docs route switch │ ├── index.tsx # Docs route switch
+13 -2
View File
@@ -45,13 +45,24 @@
</script> </script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root">
<div id="sk">
<p class="sk-text" id="sk-text">Loading...</p>
<style>
#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}
@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}
.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}
@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}
</style>
</div>
</div>
<script type="module" src="./src/main.tsx"></script> <script type="module" src="./src/main.tsx"></script>
<script> <script>
// Dynamically update html lang attribute based on i18n detection
const updateHtmlLang = () => { const updateHtmlLang = () => {
const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN' const lang = localStorage.getItem('i18nextLng') || navigator.language || 'zh-CN'
document.documentElement.lang = lang document.documentElement.lang = lang
const el = document.getElementById('sk-text')
if (el) el.textContent = lang.startsWith('zh') ? '加载中...' : 'Loading...'
} }
updateHtmlLang() updateHtmlLang()
window.addEventListener('storage', updateHtmlLang) window.addEventListener('storage', updateHtmlLang)
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/website", "name": "@page-agent/website",
"private": true, "private": true,
"version": "1.4.1", "version": "1.6.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
@@ -19,13 +19,13 @@
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.577.0",
"motion": "^12.34.3", "motion": "^12.37.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.9.0", "simple-icons": "^16.12.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://alibaba.github.io/page-agent/sitemap.xml
@@ -5,10 +5,9 @@
*/ */
import * as React from 'react' import * as React from 'react'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Badge } from './badge'
// ============================================================================ // ============================================================================
// Types // Types
// ============================================================================ // ============================================================================
@@ -35,6 +34,8 @@ export interface APIReferenceProps {
description?: React.ReactNode description?: React.ReactNode
/** Property definitions */ /** Property definitions */
properties: PropDefinition[] properties: PropDefinition[]
/** Display variant: 'properties' for fields, 'methods' for methods */
variant?: 'properties' | 'methods'
/** Additional CSS classes */ /** Additional CSS classes */
className?: string className?: string
} }
@@ -43,7 +44,14 @@ export interface APIReferenceProps {
// Component // Component
// ============================================================================ // ============================================================================
export function APIReference({ title, description, properties, className }: APIReferenceProps) { export function APIReference({
title,
description,
properties,
variant = 'properties',
className,
}: APIReferenceProps) {
const isMethodsVariant = variant === 'methods'
return ( return (
<div className={cn('my-6', className)}> <div className={cn('my-6', className)}>
{title && ( {title && (
@@ -58,10 +66,10 @@ export function APIReference({ title, description, properties, className }: APIR
<thead> <thead>
<tr className="bg-gray-50 dark:bg-gray-800/50"> <tr className="bg-gray-50 dark:bg-gray-800/50">
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300"> <th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
Property {isMethodsVariant ? 'Method' : 'Property'}
</th> </th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300"> <th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300">
Type {isMethodsVariant ? 'Return Type' : 'Type'}
</th> </th>
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell"> <th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-300 hidden md:table-cell">
Default Default
@@ -120,7 +128,7 @@ function PropRow({ name, type, required, defaultValue, description, status }: Pr
{/* Type */} {/* Type */}
<td className="px-4 py-3 align-top"> <td className="px-4 py-3 align-top">
<code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded whitespace-nowrap"> <code className="font-mono text-xs text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded wrap-break-word">
{type} {type}
</code> </code>
</td> </td>
@@ -42,7 +42,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
return ( return (
<div <div
className={`group relative ${containerClasses} rounded-xl border shadow-2xl overflow-hidden ${className}`} className={`group relative ${containerClasses} rounded-xl border shadow-2xl my-4 overflow-hidden ${className}`}
> >
{/* 编辑器顶部栏 */} {/* 编辑器顶部栏 */}
{showHeader && ( {showHeader && (
+33 -4
View File
@@ -1,4 +1,4 @@
import { siGithub } from 'simple-icons' import { siGithub, siX } from 'simple-icons'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
@@ -12,18 +12,47 @@ export default function Footer() {
> >
<div className="max-w-7xl mx-auto px-6 py-6"> <div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0"> <div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<p className="text-gray-600 dark:text-gray-300 text-sm"> <div className="text-gray-600 dark:text-gray-300 text-sm text-center md:text-left">
<p>
<a
href="https://x.com/simonluvramen"
target="_blank"
rel="noopener noreferrer"
className="inline-block bg-[linear-gradient(60deg,#39b6ff_0%,#bd45fb_33%,#ff5733_66%,#ffd600_100%)] bg-clip-text text-xs leading-none text-transparent font-mono transition-opacity duration-200 hover:opacity-85"
>
Simon.
</a>
</p>
<p className="text-gray-600 dark:text-gray-300 text-xs mt-0.5">
© 2026 page-agent. All rights reserved. © 2026 page-agent. All rights reserved.
</p> </p>
<div className="flex items-center space-x-6"> </div>
<div className="flex items-center">
<a <a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md" href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 text-sm" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 text-sm mr-4"
> >
{isZh ? '使用条款与隐私' : 'Terms & Privacy'} {isZh ? '使用条款与隐私' : 'Terms & Privacy'}
</a> </a>
<a
href="https://x.com/simonluvramen"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition-colors duration-200 mr-4"
aria-label="X (Twitter)"
>
<svg
role="img"
viewBox="0 0 24 24"
className="w-4 h-4 fill-current"
aria-hidden="true"
>
<path d={siX.path} />
</svg>
</a>
<a <a
href="https://github.com/alibaba/page-agent" href="https://github.com/alibaba/page-agent"
target="_blank" target="_blank"
+18 -5
View File
@@ -3,6 +3,7 @@ import { useState } from 'react'
import { siGithub } from 'simple-icons' import { siGithub } from 'simple-icons'
import { Link } from 'wouter' import { Link } from 'wouter'
import { formatStars, useGitHubStars } from '@/hooks/useGitHubStars'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
import LanguageSwitcher from './LanguageSwitcher' import LanguageSwitcher from './LanguageSwitcher'
@@ -12,6 +13,7 @@ import { HyperText } from './ui/hyper-text'
export default function Header() { export default function Header() {
const { isZh } = useLanguage() const { isZh } = useLanguage()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const stars = useGitHubStars()
return ( return (
<> <>
@@ -34,8 +36,11 @@ export default function Header() {
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200" className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
/> />
<div> <div>
<span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white block leading-tight"> <span className="text-base sm:text-xl font-bold text-gray-900 dark:text-white leading-tight flex items-baseline gap-1.5">
page-agent page-agent
<span className="hidden sm:inline text-[10px] font-mono font-normal text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
</span>
</span> </span>
<HyperText <HyperText
as="p" as="p"
@@ -66,7 +71,7 @@ export default function Header() {
href="https://github.com/alibaba/page-agent" href="https://github.com/alibaba/page-agent"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0" className="flex items-center gap-1 p-2 rounded-lg text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 shrink-0"
aria-label="GitHub" aria-label="GitHub"
> >
<svg <svg
@@ -77,6 +82,9 @@ export default function Header() {
> >
<path d={siGithub.path} /> <path d={siGithub.path} />
</svg> </svg>
{stars !== null && (
<span className="text-sm tabular-nums"> {formatStars(stars)}</span>
)}
</a> </a>
</nav> </nav>
@@ -86,9 +94,6 @@ export default function Header() {
role="navigation" role="navigation"
aria-label={isZh ? '文档' : 'Docs'} aria-label={isZh ? '文档' : 'Docs'}
> >
<span className="text-xs font-mono text-gray-400 dark:text-gray-500 tabular-nums before:content-['v']">
{import.meta.env.VERSION}
</span>
<Link <Link
href="/docs/introduction/overview" href="/docs/introduction/overview"
className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
@@ -112,6 +117,9 @@ export default function Header() {
<path d={siGithub.path} /> <path d={siGithub.path} />
</svg> </svg>
GitHub GitHub
{stars !== null && (
<span className="text-sm font-medium tabular-nums "> {formatStars(stars)}</span>
)}
</a> </a>
<ThemeSwitcher /> <ThemeSwitcher />
<LanguageSwitcher /> <LanguageSwitcher />
@@ -161,6 +169,11 @@ export default function Header() {
<path d={siGithub.path} /> <path d={siGithub.path} />
</svg> </svg>
GitHub GitHub
{stars !== null && (
<span className="text-xs tabular-nums text-gray-400 dark:text-gray-500">
{formatStars(stars)}
</span>
)}
</a> </a>
<div className="flex items-center gap-3 px-3 py-2"> <div className="flex items-center gap-3 px-3 py-2">
<ThemeSwitcher /> <ThemeSwitcher />
@@ -40,7 +40,7 @@ export default function LanguageSwitcher() {
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300" className="flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
aria-label={isZh ? '切换语言' : 'Switch language'} aria-label={isZh ? '切换语言' : 'Switch language'}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-haspopup="true" aria-haspopup="true"
@@ -81,7 +81,7 @@ export default function LanguageSwitcher() {
<button <button
key={lang.code} key={lang.code}
onClick={() => handleLanguageChange(lang.code)} onClick={() => handleLanguageChange(lang.code)}
className={`w-full flex items-center gap-2 px-4 py-2 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${ className={`flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
language === lang.code language === lang.code
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300' ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-700 dark:text-gray-300' : 'text-gray-700 dark:text-gray-300'
@@ -52,7 +52,7 @@ export default function ThemeSwitcher() {
return ( return (
<button <button
onClick={toggleTheme} onClick={toggleTheme}
className="relative inline-flex items-center h-8 w-16 rounded-full transition-colors duration-300 ease-in-out focus:outline-none" className="relative inline-flex h-8 w-16 cursor-pointer items-center rounded-full transition-colors duration-300 ease-in-out focus:outline-none"
style={{ style={{
backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe', backgroundColor: theme === 'dark' ? '#1e293b' : '#e0f2fe',
}} }}
@@ -62,10 +62,11 @@ export default function ThemeSwitcher() {
> >
{/* 滑块 */} {/* 滑块 */}
<span <span
className="inline-block h-6 w-6 transform rounded-full transition-all duration-300 ease-in-out shadow-md" className={`inline-block h-6 w-6 rounded-full transition-all duration-300 ease-in-out shadow-md ${
theme === 'dark' ? 'translate-x-9' : 'translate-x-1'
}`}
style={{ style={{
backgroundColor: theme === 'dark' ? '#475569' : '#fbbf24', backgroundColor: theme === 'dark' ? '#475569' : '#fbbf24',
transform: theme === 'dark' ? 'translateX(2.25rem)' : 'translateX(0.25rem)',
}} }}
> >
{/* 图标 */} {/* 图标 */}
+3 -3
View File
@@ -1,10 +1,10 @@
// Demo build (auto-init with demo LLM, for quick testing) // Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL = export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.4.1/dist/iife/page-agent.demo.js' 'https://cdn.jsdelivr.net/npm/page-agent@1.6.0/dist/iife/page-agent.demo.js'
export const CDN_DEMO_CN_URL = export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.4.1/files/dist/iife/page-agent.demo.js' 'https://registry.npmmirror.com/page-agent/1.6.0/files/dist/iife/page-agent.demo.js'
// Demo LLM for website testing (homepage quick trial uses flash) // Demo LLM for website testing (homepage quick trial uses flash)
export const DEMO_MODEL = 'qwen3.5-flash' export const DEMO_MODEL = 'qwen3.5-flash'
export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run' export const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
export const DEMO_API_KEY = 'NA' // export const DEMO_API_KEY = ''
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react'
const STATS_URL = 'https://page-agent.github.io/gh-stats/stats.json'
let cached: number | null = null
export function useGitHubStars() {
const [stars, setStars] = useState(cached)
useEffect(() => {
if (cached !== null) return
fetch(STATS_URL)
.then((r) => r.json())
.then((data) => {
cached = data.stargazers_count ?? null
setStars(cached)
})
.catch(() => {})
}, [])
return stars
}
export function formatStars(n: number): string {
if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, '')}k`
return String(n)
}
+1
View File
@@ -1,3 +1,4 @@
@config '../tailwind.config.js';
@import 'tailwindcss'; @import 'tailwindcss';
@import 'tw-animate-css'; @import 'tw-animate-css';
@@ -0,0 +1,9 @@
import { useEffect } from 'react'
const DEFAULT_TITLE = 'PageAgent - The GUI Agent Living in Your Webpage'
export function useDocumentTitle(title?: string) {
useEffect(() => {
document.title = title ? `${title} - PageAgent` : DEFAULT_TITLE
}, [title])
}
-15
View File
@@ -1,15 +0,0 @@
import FeaturesSection from './home/FeaturesSection'
import HeroSection from './home/HeroSection'
import OneMoreThingSection from './home/OneMoreThingSection'
import ScenariosSection from './home/ScenariosSection'
export default function HomePage() {
return (
<>
<HeroSection />
<FeaturesSection />
<ScenariosSection />
<OneMoreThingSection />
</>
)
}
+10 -6
View File
@@ -4,6 +4,7 @@ import { Link, useLocation } from 'wouter'
import { SparklesText } from '@/components/ui/sparkles-text' import { SparklesText } from '@/components/ui/sparkles-text'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
import { useDocumentTitle } from '@/lib/useDocumentTitle'
interface DocsLayoutProps { interface DocsLayoutProps {
children: ReactNode children: ReactNode
@@ -34,10 +35,6 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
title: isZh ? '故障排查' : 'Troubleshooting', title: isZh ? '故障排查' : 'Troubleshooting',
path: '/introduction/troubleshooting', path: '/introduction/troubleshooting',
}, },
{
title: '🚧 ' + (isZh ? '最佳实践' : 'Best Practices'),
path: '/integration/best-practices',
},
], ],
}, },
{ {
@@ -50,7 +47,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
{ title: isZh ? 'Chrome 扩展' : 'Chrome Extension', path: '/features/chrome-extension' }, { title: isZh ? 'Chrome 扩展' : 'Chrome Extension', path: '/features/chrome-extension' },
{ {
title: isZh ? '接入第三方 Agent' : 'Third-party Agent', title: isZh ? '接入第三方 Agent' : 'Third-party Agent',
path: '/integration/third-party-agent', path: '/features/third-party-agent',
}, },
], ],
}, },
@@ -59,15 +56,22 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
items: [ items: [
{ title: 'PageAgent', path: '/advanced/page-agent' }, { title: 'PageAgent', path: '/advanced/page-agent' },
{ title: 'PageAgentCore', path: '/advanced/page-agent-core' }, { title: 'PageAgentCore', path: '/advanced/page-agent-core' },
{ title: 'PageController', path: '/advanced/page-controller' },
{ title: isZh ? '自定义 UI' : 'Custom UI', path: '/advanced/custom-ui' }, { title: isZh ? '自定义 UI' : 'Custom UI', path: '/advanced/custom-ui' },
{ {
title: '🚧 ' + (isZh ? '安全与权限' : 'Security & Permissions'), title: '🚧 ' + (isZh ? '安全与权限' : 'Security & Permissions'),
path: '/integration/security-permissions', path: '/advanced/security-permissions',
}, },
], ],
}, },
] ]
const activeTitle = navigationSections
.flatMap((s) => s.items)
.find((item) => item.path === location)?.title
useDocumentTitle(activeTitle)
return ( return (
<div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto"> <div className="max-w-7xl mx-auto px-6 py-8 overflow-x-auto">
<div className="flex gap-8 min-w-225"> <div className="flex gap-8 min-w-225">
@@ -1,6 +1,6 @@
import { APIDivider, APIReference } from '@/components/APIReference'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { Heading } from '@/components/Heading' import { Heading } from '@/components/Heading'
import { APIDivider, APIReference } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
export default function CustomUIDocs() { export default function CustomUIDocs() {
@@ -1,6 +1,8 @@
import { Link } from 'wouter'
import { APIDivider, APIReference, TypeRef } from '@/components/APIReference'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { Heading } from '@/components/Heading' import { Heading } from '@/components/Heading'
import { APIDivider, APIReference, TypeRef } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
export default function PageAgentCoreDocs() { export default function PageAgentCoreDocs() {
@@ -50,19 +52,13 @@ const agent = new PageAgentCore({
baseURL: 'https://api.openai.com/v1', baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key', apiKey: 'your-api-key',
model: 'gpt-5.2', model: 'gpt-5.2',
language: 'en-US',
}) })
// Listen to events for UI display // Listen to events for UI display
agent.addEventListener('statuschange', () => { agent.addEventListener('statuschange', () => {
console.log('Status:', agent.status) console.log('Status:', agent.status)
}) })
agent.addEventListener('historychange', () => {
console.log('History:', agent.history)
})
agent.addEventListener('activity', (e) => { agent.addEventListener('activity', (e) => {
const activity = (e as CustomEvent).detail const activity = (e as CustomEvent).detail
console.log('Activity:', activity.type) console.log('Activity:', activity.type)
@@ -75,14 +71,54 @@ const result = await agent.execute('Fill in the form with test data')`}
<APIDivider title={isZh ? '配置' : 'Configuration'} /> <APIDivider title={isZh ? '配置' : 'Configuration'} />
{/* LLM Configuration */} {/* Configuration */}
<section className="mb-10"> <section className="mb-10">
<Heading id="llmconfig">LLMConfig</Heading> <Heading id="configuration">PageAgentCoreConfig</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh {isZh
? '配置与大语言模型的连接参数。支持 OpenAI 兼容的 API。' ? 'PageAgentCoreConfig = AgentConfig & { pageController: PageController }。AgentConfig 包含以下配置项:'
: 'Configure connection parameters for the language model. Supports OpenAI-compatible APIs.'} : 'PageAgentCoreConfig = AgentConfig & { pageController: PageController }. AgentConfig contains the following options:'}
</p> </p>
{/* PageController */}
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
PageController
</h3>
<APIReference
properties={[
{
name: 'pageController',
type: 'PageController',
required: true,
description: isZh ? (
<>
<Link
href="/advanced/page-controller"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
PageController
</Link>{' '}
DOM
</>
) : (
<>
<Link
href="/advanced/page-controller"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
PageController
</Link>{' '}
instance for DOM operations and element interaction.
</>
),
},
]}
/>
{/* LLM Config */}
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
{isZh ? 'LLM 配置' : 'LLM Config'}
</h3>
<APIReference <APIReference
properties={[ properties={[
{ {
@@ -93,12 +129,6 @@ const result = await agent.execute('Fill in the form with test data')`}
? 'LLM API 的基础 URL(如 https://api.openai.com/v1' ? 'LLM API 的基础 URL(如 https://api.openai.com/v1'
: 'Base URL of the LLM API (e.g., https://api.openai.com/v1)', : 'Base URL of the LLM API (e.g., https://api.openai.com/v1)',
}, },
{
name: 'apiKey',
type: 'string',
required: true,
description: isZh ? 'API 密钥' : 'API key for authentication',
},
{ {
name: 'model', name: 'model',
type: 'string', type: 'string',
@@ -107,10 +137,15 @@ const result = await agent.execute('Fill in the form with test data')`}
? '模型名称(如 gpt-5.2, anthropic/claude-4.5-haiku' ? '模型名称(如 gpt-5.2, anthropic/claude-4.5-haiku'
: 'Model name (e.g., gpt-5.2, anthropic/claude-4.5-haiku)', : 'Model name (e.g., gpt-5.2, anthropic/claude-4.5-haiku)',
}, },
{
name: 'apiKey',
type: 'string',
required: false,
description: 'LLM AK',
},
{ {
name: 'temperature', name: 'temperature',
type: 'number', type: 'number',
defaultValue: '0',
description: isZh description: isZh
? '模型温度参数,控制输出随机性' ? '模型温度参数,控制输出随机性'
: 'Model temperature, controls output randomness', : 'Model temperature, controls output randomness',
@@ -121,6 +156,14 @@ const result = await agent.execute('Fill in the form with test data')`}
defaultValue: '3', defaultValue: '3',
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure', description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
}, },
{
name: 'disableNamedToolChoice',
type: 'boolean',
defaultValue: 'false',
description: isZh
? '禁用命名 tool_choice,始终使用 "required" 字符串。适用于不支持 tool_choice 对象格式的 LLM 服务。'
: 'Disable named tool_choice, always use "required" string. For LLM services that don\'t support the object format of tool_choice.',
},
{ {
name: 'customFetch', name: 'customFetch',
type: 'typeof fetch', type: 'typeof fetch',
@@ -130,16 +173,11 @@ const result = await agent.execute('Fill in the form with test data')`}
}, },
]} ]}
/> />
</section>
{/* Agent Configuration */} {/* Agent Config */}
<section className="mb-10"> <h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
<Heading id="agentconfig">AgentConfig</Heading> {isZh ? 'Agent 配置' : 'Agent Config'}
<p className="text-gray-600 dark:text-gray-400 mb-4"> </h3>
{isZh
? '配置 Agent 的行为、生命周期钩子和扩展能力。'
: 'Configure agent behavior, lifecycle hooks, and extension capabilities.'}
</p>
<APIReference <APIReference
properties={[ properties={[
{ {
@@ -151,7 +189,7 @@ const result = await agent.execute('Fill in the form with test data')`}
{ {
name: 'maxSteps', name: 'maxSteps',
type: 'number', type: 'number',
defaultValue: '20', defaultValue: '40',
description: isZh ? '每个任务的最大步骤数' : 'Maximum number of steps per task', description: isZh ? '每个任务的最大步骤数' : 'Maximum number of steps per task',
}, },
{ {
@@ -166,8 +204,8 @@ const result = await agent.execute('Fill in the form with test data')`}
name: 'instructions', name: 'instructions',
type: 'InstructionsConfig', type: 'InstructionsConfig',
description: isZh description: isZh
? '指导 Agent 行为的指令配置' ? '指导 Agent 行为的指令配置,见下方类型定义'
: 'Instructions to guide agent behavior', : 'Instructions to guide agent behavior, see type definition below',
}, },
{ {
name: 'transformPageContent', name: 'transformPageContent',
@@ -177,7 +215,15 @@ const result = await agent.execute('Fill in the form with test data')`}
: 'Transform page content before sending to LLM, useful for data masking', : 'Transform page content before sending to LLM, useful for data masking',
}, },
{ {
name: 'experimentalScriptExecutionTool', name: 'customSystemPrompt',
type: 'string',
status: 'experimental',
description: isZh
? '完全覆盖默认系统提示词。谨慎使用。'
: 'Completely override the default system prompt. Use with caution.',
},
{
name: 'experimentalScript\nExecutionTool',
type: 'boolean', type: 'boolean',
defaultValue: 'false', defaultValue: 'false',
status: 'experimental', status: 'experimental',
@@ -185,134 +231,58 @@ const result = await agent.execute('Fill in the form with test data')`}
? '启用实验性 JavaScript 执行工具' ? '启用实验性 JavaScript 执行工具'
: 'Enable experimental JavaScript execution tool', : 'Enable experimental JavaScript execution tool',
}, },
]}
/>
<h3 className="text-lg font-semibold mt-6 mb-3">InstructionsConfig</h3>
<APIReference
properties={[
{ {
name: 'system', name: 'experimentalLlmsTxt',
type: 'string', type: 'boolean',
defaultValue: 'false',
status: 'experimental',
description: isZh description: isZh
? '全局系统级指令,应用于所有任务' ? '从当前站点根目录获取 /llms.txt 并作为上下文提供给 LLM'
: 'Global system-level instructions, applied to all tasks', : 'Fetch /llms.txt from site origin and include as LLM context',
},
{
name: 'getPageInstructions',
type: '(url: string) => string | undefined | null',
description: isZh
? '动态页面级指令回调,在每个步骤前调用'
: 'Dynamic page-level instructions callback, called before each step',
}, },
]} ]}
/> />
</section>
{/* Lifecycle Hooks */} {/* Lifecycle Hooks */}
<section className="mb-10"> <h3 className="text-lg font-semibold mt-6 mb-3 text-gray-800 dark:text-gray-200">
<Heading id="lifecycle-hooks">{isZh ? '生命周期钩子' : 'Lifecycle Hooks'}</Heading> {isZh ? '生命周期钩子' : 'Lifecycle Hooks'}
<span className="ml-2 text-xs font-normal text-amber-600 dark:text-amber-400">
experimental
</span>
</h3>
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4"> <div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
<p className="text-amber-800 dark:text-amber-200 text-sm"> <p className="text-amber-800 dark:text-amber-200 text-sm">
<strong> {isZh ? '警告' : 'Warning'}:</strong>{' '}
{isZh {isZh
? '这些接口高度实验性,可能在未来版本中发生变化。建议优先使用事件系统(Events)来监听 Agent 状态。' ? '这些接口高度实验性,可能在未来版本中发生变化。'
: 'These APIs are highly experimental and may change in future versions. Prefer using the Events system for monitoring agent state.'} : 'These APIs are highly experimental and may change in future versions. '}
</p> </p>
</div> </div>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '所有生命周期钩子都接收 agent 实例作为第一个参数,便于在回调中访问 Agent 状态和方法。'
: 'All lifecycle hooks receive the agent instance as first parameter, making it easy to access agent state and methods in callbacks.'}
</p>
<APIReference <APIReference
properties={[ properties={[
{ {
name: 'onBeforeStep', name: 'onBeforeStep',
type: '(agent: PageAgentCore, stepCount: number) => void | Promise<void>', type: '(agent, stepCount) => void | Promise<void>',
description: isZh ? '每个步骤执行前调用' : 'Called before each step execution', description: isZh ? '每个步骤执行前调用' : 'Called before each step execution',
status: 'experimental',
}, },
{ {
name: 'onAfterStep', name: 'onAfterStep',
type: '(agent: PageAgentCore, history: HistoricalEvent[]) => void | Promise<void>', type: '(agent, history) => void | Promise<void>',
description: isZh ? '每个步骤执行后调用' : 'Called after each step execution', description: isZh ? '每个步骤执行后调用' : 'Called after each step execution',
status: 'experimental',
}, },
{ {
name: 'onBeforeTask', name: 'onBeforeTask',
type: '(agent: PageAgentCore) => void | Promise<void>', type: '(agent) => void | Promise<void>',
description: isZh ? '任务开始前调用' : 'Called before task starts', description: isZh ? '任务开始前调用' : 'Called before task starts',
status: 'experimental',
}, },
{ {
name: 'onAfterTask', name: 'onAfterTask',
type: '(agent: PageAgentCore, result: ExecutionResult) => void | Promise<void>', type: '(agent, result) => void | Promise<void>',
description: isZh ? '任务结束后调用' : 'Called after task ends', description: isZh ? '任务结束后调用' : 'Called after task ends',
status: 'experimental',
}, },
{ {
name: 'onDispose', name: 'onDispose',
type: '(agent: PageAgentCore, reason?: string) => void', type: '(agent, reason?) => void',
description: isZh ? 'Agent 销毁时调用' : 'Called when agent is disposed', description: isZh ? 'Agent 销毁时调用' : 'Called when agent is disposed',
status: 'experimental',
},
]}
/>
</section>
{/* PageController Configuration */}
<section className="mb-10">
<Heading id="pagecontrollerconfig">PageControllerConfig</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '配置 DOM 提取、元素交互和视觉反馈。'
: 'Configure DOM extraction, element interaction, and visual feedback.'}
</p>
<APIReference
properties={[
{
name: 'pageController',
type: 'PageController',
status: 'experimental',
description: isZh
? '自定义 PageController 实例。如不提供,将创建默认实例。'
: 'Custom PageController instance. If not provided, a default one will be created.',
},
{
name: 'enableMask',
type: 'boolean',
defaultValue: 'true',
description: isZh
? '启用视觉遮罩覆盖层,阻止用户在自动化期间操作'
: 'Enable visual mask overlay that blocks user interaction during automation',
},
{
name: 'viewportExpansion',
type: 'number',
defaultValue: '0',
description: isZh
? '视口扩展像素数,-1 表示提取整个页面'
: 'Viewport expansion in pixels, -1 means extract entire page',
},
{
name: 'interactiveBlacklist',
type: '(Element | (() => Element))[]',
description: isZh ? '要排除的交互元素列表' : 'Elements to exclude from interaction',
},
{
name: 'interactiveWhitelist',
type: '(Element | (() => Element))[]',
description: isZh
? '要强制包含的交互元素列表'
: 'Elements to force include for interaction',
},
{
name: 'includeAttributes',
type: 'string[]',
description: isZh
? '在 DOM 提取中包含的额外属性'
: 'Additional attributes to include in DOM extraction',
}, },
]} ]}
/> />
@@ -369,16 +339,24 @@ const result = await agent.execute('Fill in the form with test data')`}
<section className="mb-10"> <section className="mb-10">
<Heading id="methods">{isZh ? '方法' : 'Methods'}</Heading> <Heading id="methods">{isZh ? '方法' : 'Methods'}</Heading>
<APIReference <APIReference
variant="methods"
properties={[ properties={[
{ {
name: 'execute(task: string)', name: 'execute(task)',
type: 'Promise<ExecutionResult>', type: 'Promise<ExecutionResult>',
description: isZh description: isZh
? '执行任务并返回结果。包含 success、data 和 history 字段。' ? '执行任务并返回结果。包含 success、data 和 history 字段。'
: 'Execute a task and return result. Contains success, data, and history fields.', : 'Execute a task and return result. Contains success, data, and history fields.',
}, },
{ {
name: 'dispose(reason?: string)', name: 'stop()',
type: 'void',
description: isZh
? '停止当前任务。Agent 仍可复用。'
: 'Stop the current task. Agent remains reusable.',
},
{
name: 'dispose()',
type: 'void', type: 'void',
description: isZh description: isZh
? '销毁 Agent 并清理资源' ? '销毁 Agent 并清理资源'
@@ -443,11 +421,8 @@ const result = await agent.execute('Fill in the form with test data')`}
<CodeEditor <CodeEditor
language="typescript" language="typescript"
code={`interface ExecutionResult { code={`interface ExecutionResult {
/** Whether the task completed successfully */
success: boolean success: boolean
/** Result description from the agent */
data: string data: string
/** Full execution history */
history: HistoricalEvent[] history: HistoricalEvent[]
}`} }`}
/> />
@@ -467,50 +442,21 @@ const result = await agent.execute('Fill in the form with test data')`}
/> />
</section> </section>
<APIDivider title={isZh ? '无头模式' : 'Headless Mode'} /> {/* InstructionsConfig */}
{/* Headless Usage */}
<section className="mb-10"> <section className="mb-10">
<Heading id="headless-mode">{isZh ? '无头模式' : 'Headless Mode'}</Heading> <Heading id="instructionsconfig">InstructionsConfig</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '在非 DOM 环境中,你必须实现自定义的 PageController(例如远程操作页面或 Puppeteer)。'
: 'In non-DOM environments, you must implement a custom PageController (e.g., remote page control or Puppeteer).'}
</p>
<CodeEditor <CodeEditor
language="typescript" language="typescript"
code={`import { PageAgentCore } from '@page-agent/core' code={`interface InstructionsConfig {
import type { PageController } from '@page-agent/page-controller' /** Global system-level instructions, applied to all tasks */
system?: string
class MyRemotePageController implements PageController { /**
// Implement required methods for DOM extraction and interaction * Dynamic page-level instructions callback.
} * Called before each step to get instructions for the current page.
*/
const agent = new PageAgentCore({ getPageInstructions?: (url: string) => string | undefined
pageController: new MyRemotePageController(), }`}
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
language: 'en-US',
})
// Listen to events for UI display
agent.addEventListener('statuschange', () => {
console.log('Status:', agent.status)
})
agent.addEventListener('historychange', () => {
console.log('History:', agent.history)
})
agent.addEventListener('activity', (e) => {
const activity = (e as CustomEvent).detail
console.log('Activity:', activity.type)
})
// Execute task
const result = await agent.execute('Fill in the form with test data')`}
/> />
</section> </section>
</div> </div>
@@ -2,7 +2,6 @@ import { Link } from 'wouter'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { Heading } from '@/components/Heading' import { Heading } from '@/components/Heading'
import { APIReference, TypeRef } from '@/components/ui/api-reference'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
export default function PageAgentDocs() { export default function PageAgentDocs() {
@@ -14,8 +13,8 @@ export default function PageAgentDocs() {
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh {isZh
? 'PageAgent 是带有内置 UI 面板的完整 Agent 类。它继承自 PageAgentCore,并自动创建交互面板。' ? 'PageAgent 是带有内置 UI 面板的完整 Agent 类。它继承自 PageAgentCore,并自动创建交互面板和 PageController。'
: 'PageAgent is the complete Agent class with built-in UI panel. It extends PageAgentCore and automatically creates an interactive panel.'} : 'PageAgent is the complete Agent class with built-in UI panel. It extends PageAgentCore and automatically creates an interactive panel and PageController.'}
</p> </p>
{/* When to use */} {/* When to use */}
@@ -29,6 +28,11 @@ export default function PageAgentDocs() {
: 'In most cases, you should use PageAgent. It provides a complete out-of-the-box experience:'} : 'In most cases, you should use PageAgent. It provides a complete out-of-the-box experience:'}
</p> </p>
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-6"> <ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-6">
<li>
{isZh
? '自动创建 PageController,处理 DOM 提取和元素操作'
: 'Automatically creates PageController for DOM extraction and element actions'}
</li>
<li> <li>
{isZh {isZh
? '内置 UI 面板,显示任务进度、Agent 思考过程和操作结果' ? '内置 UI 面板,显示任务进度、Agent 思考过程和操作结果'
@@ -74,9 +78,8 @@ console.log(result.history) // Full execution history`}
<CodeEditor <CodeEditor
language="typescript" language="typescript"
code={`class PageAgent extends PageAgentCore { code={`class PageAgent extends PageAgentCore {
/** The UI panel instance */
panel: Panel panel: Panel
pageController: PageController
constructor(config: PageAgentConfig) constructor(config: PageAgentConfig)
}`} }`}
/> />
@@ -90,7 +93,21 @@ console.log(result.history) // Full execution history`}
> >
PageAgentCore PageAgentCore
</Link> </Link>
API PageAgentCore {' '}
<Link
href="/advanced/page-agent-core#configuration"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
AgentConfig
</Link>{' '}
{' '}
<Link
href="/advanced/page-controller#configuration"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
PageControllerConfig
</Link>
</> </>
) : ( ) : (
<> <>
@@ -101,71 +118,34 @@ console.log(result.history) // Full execution history`}
> >
PageAgentCore PageAgentCore
</Link> </Link>
. All core methods and events are available. See PageAgentCore docs for detailed API . All core methods and events are available. Config merges{' '}
reference.
</>
)}
</p>
</section>
{/* Configuration */}
<section className="mb-10">
<Heading id="configuration">{isZh ? '配置' : 'Configuration'}</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? 'PageAgent 使用与 PageAgentCore 相同的配置接口。'
: 'PageAgent uses the same configuration interface as PageAgentCore.'}
</p>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh ? (
<>
{' '}
<Link <Link
href="/advanced/page-agent-core" href="/advanced/page-agent-core#configuration"
className="text-blue-600 dark:text-blue-400 hover:underline" className="text-blue-600 dark:text-blue-400 hover:underline"
> >
PageAgentCore AgentConfig
</Link>
</>
) : (
<>
See{' '}
<Link
href="/advanced/page-agent-core"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
PageAgentCore configuration docs
</Link>{' '} </Link>{' '}
for complete reference. and{' '}
<Link
href="/advanced/page-controller#configuration"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
PageControllerConfig
</Link>
.
</> </>
)} )}
</p> </p>
</section> </section>
{/* Panel Property */} {/* Panel */}
<section className="mb-10"> <section className="mb-10">
<Heading id="panel-property">{isZh ? 'Panel 属性' : 'Panel Property'}</Heading> <Heading id="panel">{isZh ? 'UI 面板' : 'UI Panel'}</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4"> <p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh {isZh
? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性访问它来控制 UI' ? 'PageAgent 自动创建一个 Panel 实例。你可以通过 panel 属性控制 UI'
: 'PageAgent automatically creates a Panel instance. You can access it via the panel property to control the UI:'} : 'PageAgent automatically creates a Panel instance. You can control the UI via the panel property:'}
</p> </p>
<APIReference
properties={[
{
name: 'panel',
type: 'Panel',
required: true,
description: isZh
? '内置的 UI 面板实例,用于显示任务进度和接收用户输入。'
: 'The built-in UI panel instance for displaying task progress and receiving user input.',
},
]}
/>
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? 'Panel 方法' : 'Panel Methods'}</h3>
<CodeEditor <CodeEditor
language="typescript" language="typescript"
code={`// Show/hide the panel code={`// Show/hide the panel
@@ -212,16 +192,16 @@ agent.panel.dispose()`}
</tr> </tr>
<tr className="bg-white dark:bg-gray-900"> <tr className="bg-white dark:bg-gray-900">
<td className="px-4 py-3 text-gray-600 dark:text-gray-400"> <td className="px-4 py-3 text-gray-600 dark:text-gray-400">
{isZh ? 'Headless 模式' : 'Headless Mode'} {isZh ? '自动创建 PageController' : 'Auto-creates PageController'}
</td> </td>
<td className="px-4 py-3 text-center text-gray-400 dark:text-gray-600">-</td>
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400"></td> <td className="px-4 py-3 text-center text-green-600 dark:text-green-400"></td>
<td className="px-4 py-3 text-center text-gray-400 dark:text-gray-600">-</td>
</tr> </tr>
<tr className="bg-white dark:bg-gray-900"> <tr className="bg-white dark:bg-gray-900">
<td className="px-4 py-3 text-gray-600 dark:text-gray-400"> <td className="px-4 py-3 text-gray-600 dark:text-gray-400">
{isZh ? '自定义 PageController' : 'Custom PageController'} {isZh ? 'Headless 模式' : 'Headless Mode'}
</td> </td>
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400"></td> <td className="px-4 py-3 text-center text-gray-400 dark:text-gray-600">-</td>
<td className="px-4 py-3 text-center text-green-600 dark:text-green-400"></td> <td className="px-4 py-3 text-center text-green-600 dark:text-green-400"></td>
</tr> </tr>
<tr className="bg-white dark:bg-gray-900"> <tr className="bg-white dark:bg-gray-900">
@@ -0,0 +1,286 @@
import { Link } from 'wouter'
import { APIDivider, APIReference } from '@/components/APIReference'
import CodeEditor from '@/components/CodeEditor'
import { Heading } from '@/components/Heading'
import { useLanguage } from '@/i18n/context'
export default function PageControllerDocs() {
const { isZh } = useLanguage()
return (
<div>
<h1 className="text-4xl font-bold mb-6">PageController</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh
? 'PageController 负责 DOM 提取和元素交互,独立于 LLM。它将页面状态结构化为 LLM 可消费的格式,并执行元素级操作。'
: 'PageController handles DOM extraction and element interaction, independent of LLM. It structures page state into LLM-consumable format and executes element-level actions.'}
</p>
{/* Basic Usage */}
<section className="mb-10">
<Heading id="basic-usage">{isZh ? '基本用法' : 'Basic Usage'}</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? 'PageAgent 接受 PageController 配置项:'
: 'PageAgent accepts PageController options:'}
</p>
<CodeEditor
language="typescript"
code={`import { PageAgent } from 'page-agent'
const agent = new PageAgent({
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
// PageController options
enableMask: true,
viewportExpansion: 0,
})`}
/>
<p className="text-gray-600 dark:text-gray-400 mt-4">
{isZh
? 'PageAgentCore 接受 PageController 实例:'
: 'PageAgentCore accepts a PageController instance:'}
</p>
<CodeEditor
language="typescript"
code={`import { PageAgentCore } from '@page-agent/core'
import { PageController } from '@page-agent/page-controller'
const pageController = new PageController({
enableMask: true,
viewportExpansion: -1, // extract full page
})
const agent = new PageAgentCore({
pageController,
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
})`}
/>
</section>
<APIDivider title={isZh ? '配置' : 'Configuration'} />
{/* Configuration */}
<section className="mb-10">
<Heading id="configuration">PageControllerConfig</Heading>
<APIReference
properties={[
{
name: 'enableMask',
type: 'boolean',
defaultValue: 'false',
description: isZh
? '启用视觉遮罩覆盖层,在自动化期间阻止用户操作页面。通过 PageAgent 创建时默认为 true。'
: 'Enable visual mask overlay that blocks user interaction during automation. Defaults to true when created via PageAgent.',
},
{
name: 'viewportExpansion',
type: 'number',
defaultValue: '0',
description: isZh
? '向视口外扩展提取的像素数。设为 -1 表示提取整个页面。'
: 'Pixels to expand extraction beyond viewport. Set to -1 to extract the entire page.',
},
{
name: 'interactiveBlacklist',
type: '(Element | (() => Element))[]',
description: isZh
? '要排除的交互元素列表。支持元素引用或返回元素的函数(延迟求值)。'
: 'Elements to exclude from interaction. Supports element references or functions returning elements (lazy evaluation).',
},
{
name: 'interactiveWhitelist',
type: '(Element | (() => Element))[]',
description: isZh
? '要强制包含的交互元素列表。支持元素引用或返回元素的函数。'
: 'Elements to force include for interaction. Supports element references or functions returning elements.',
},
{
name: 'includeAttributes',
type: 'string[]',
description: isZh
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
},
]}
/>
</section>
<APIDivider title={isZh ? '方法' : 'Methods'} />
{/* Methods */}
<section className="mb-10">
<Heading id="methods">{isZh ? '方法' : 'Methods'}</Heading>
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? '状态查询' : 'State Queries'}</h3>
<APIReference
variant="methods"
properties={[
{
name: 'getBrowserState()',
type: 'Promise<BrowserState>',
description: isZh
? '获取结构化的浏览器状态(URL、标题、简化 HTML 等),自动调用 updateTree() 刷新 DOM。这是 Agent 在每步使用的主要方法。'
: 'Get structured browser state (URL, title, simplified HTML, etc.), automatically calls updateTree() to refresh DOM. This is the primary method the agent uses each step.',
},
{
name: 'updateTree()',
type: 'Promise<string>',
description: isZh
? '刷新 DOM 树并返回简化 HTML。通常不需要手动调用 —— getBrowserState() 会自动调用。'
: 'Refresh DOM tree and return simplified HTML. Usually not needed manually — getBrowserState() calls it automatically.',
},
{
name: 'getCurrentUrl()',
type: 'Promise<string>',
description: isZh ? '获取当前页面 URL。' : 'Get current page URL.',
},
]}
/>
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? '元素操作' : 'Element Actions'}</h3>
<APIReference
variant="methods"
properties={[
{
name: 'clickElement(index)',
type: 'Promise<ActionResult>',
description: isZh
? '按索引点击元素。索引来自简化 HTML 中的 [N] 标记。'
: 'Click element by index. Index comes from [N] markers in simplified HTML.',
},
{
name: 'inputText(index, text)',
type: 'Promise<ActionResult>',
description: isZh ? '向输入框元素填入文本。' : 'Input text into a form element.',
},
{
name: 'selectOption(index, optionText)',
type: 'Promise<ActionResult>',
description: isZh ? '在下拉框中选择选项。' : 'Select option in a dropdown element.',
},
{
name: 'scroll(options)',
type: 'Promise<ActionResult>',
description: isZh
? '垂直滚动页面或指定元素。'
: 'Scroll page or specific element vertically.',
},
{
name: 'scrollHorizontally(options)',
type: 'Promise<ActionResult>',
description: isZh
? '水平滚动页面或指定元素。'
: 'Scroll page or specific element horizontally.',
},
]}
/>
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? '遮罩控制' : 'Mask Control'}</h3>
<APIReference
variant="methods"
properties={[
{
name: 'showMask()',
type: 'Promise<void>',
description: isZh
? '显示视觉遮罩。需要 enableMask: true。'
: 'Show visual mask overlay. Requires enableMask: true.',
},
{
name: 'hideMask()',
type: 'Promise<void>',
description: isZh ? '隐藏视觉遮罩。' : 'Hide visual mask overlay.',
},
]}
/>
<h3 className="text-lg font-semibold mt-6 mb-3">{isZh ? '生命周期' : 'Lifecycle'}</h3>
<APIReference
variant="methods"
properties={[
{
name: 'dispose()',
type: 'void',
description: isZh
? '清理所有资源(DOM 高亮、遮罩等)。Agent 销毁时自动调用。'
: 'Clean up all resources (DOM highlights, mask, etc.). Called automatically when agent disposes.',
},
]}
/>
</section>
<APIDivider title={isZh ? '类型定义' : 'Type Definitions'} />
{/* BrowserState */}
<section className="mb-10">
<Heading id="browser-state">BrowserState</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? 'getBrowserState() 返回的结构化浏览器状态,直接用于构建 LLM prompt。'
: 'Structured browser state returned by getBrowserState(), used directly to build LLM prompts.'}
</p>
<CodeEditor
language="typescript"
code={`interface BrowserState {
url: string
title: string
header: string // page info + scroll position
content: string // simplified HTML of interactive elements
footer: string // scroll hint
}`}
/>
</section>
{/* ActionResult */}
<section className="mb-10">
<Heading id="action-result">ActionResult</Heading>
<CodeEditor
language="typescript"
code={`interface ActionResult {
success: boolean
message: string
}`}
/>
</section>
{/* Custom PageController */}
<section className="mb-10">
<Heading id="custom-implementation">
{isZh ? '自定义实现' : 'Custom Implementation'}
</Heading>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '在非浏览器环境(如 Puppeteer、Playwright),你可以实现自定义 PageController。需要实现 Agent 使用的核心方法:'
: 'In non-browser environments (e.g. Puppeteer, Playwright), you can implement a custom PageController. Implement the core methods used by the agent:'}
</p>
<CodeEditor
language="typescript"
code={`import { PageAgentCore } from '@page-agent/core'
import type { PageController } from '@page-agent/page-controller'
class PuppeteerPageController implements PageController {
async getBrowserState() { /* ... */ }
async clickElement(index: number) { /* ... */ }
async inputText(index: number, text: string) { /* ... */ }
async scroll(options: { down: boolean; numPages: number }) { /* ... */ }
// ... other methods
}
const agent = new PageAgentCore({
pageController: new PuppeteerPageController(),
baseURL: 'https://api.openai.com/v1',
apiKey: 'your-api-key',
model: 'gpt-5.2',
})`}
/>
</section>
</div>
)
}
@@ -187,39 +187,7 @@ localStorage.setItem('PageAgentExtUserAuthToken', '<your-token-from-extension>')
</p> </p>
<CodeEditor <CodeEditor
code={ code={`import type {
isZh
? `import type {
AgentActivity,
AgentStatus,
ExecutionResult,
HistoricalEvent
} from '@page-agent/core'
interface ExecuteConfig {
baseURL: string // LLM API 端点
apiKey: string // API 密钥
model: string // 模型名称
includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void
onActivity?: (activity: AgentActivity) => void
onHistoryUpdate?: (history: HistoricalEvent[]) => void
}
type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
declare global {
interface Window {
PAGE_AGENT_EXT_VERSION?: string
PAGE_AGENT_EXT?: {
version: string
execute: Execute
stop: () => void
}
}
}`
: `import type {
AgentActivity, AgentActivity,
AgentStatus, AgentStatus,
ExecutionResult, ExecutionResult,
@@ -228,8 +196,8 @@ declare global {
interface ExecuteConfig { interface ExecuteConfig {
baseURL: string // LLM API endpoint baseURL: string // LLM API endpoint
apiKey: string // API key
model: string // Model name model: string // Model name
apiKey?: string // LLM AK
includeInitialTab?: boolean includeInitialTab?: boolean
onStatusChange?: (status: AgentStatus) => void onStatusChange?: (status: AgentStatus) => void
@@ -248,8 +216,7 @@ declare global {
stop: () => void stop: () => void
} }
} }
}` }`}
}
language="typescript" language="typescript"
/> />
@@ -1,4 +1,3 @@
import BetaNotice from '@/components/BetaNotice'
import CodeEditor from '@/components/CodeEditor' import CodeEditor from '@/components/CodeEditor'
import { Heading } from '@/components/Heading' import { Heading } from '@/components/Heading'
import { useLanguage } from '@/i18n/context' import { useLanguage } from '@/i18n/context'
@@ -12,115 +11,100 @@ export default function CustomTools() {
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed"> <p className="text-xl text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
{isZh {isZh
? '通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义严格的输入接口,让 AI 安全调用你的业务逻辑。' ? '通过注册自定义工具,扩展 AI Agent 的能力边界。使用 Zod 定义输入接口,让 AI 安全调用你的业务逻辑。'
: 'Extend AI Agent capabilities by registering custom tools. Use Zod to define strict input schemas for safe business logic calls.'} : 'Extend AI Agent capabilities by registering custom tools. Define input schemas with Zod for safe business logic invocation.'}
</p> </p>
<div className="space-y-8"> <div className="space-y-8">
<section> <section>
<Heading id="tool-registration" className="text-2xl font-bold mb-4"> <Heading id="zod-version" className="text-2xl font-bold mb-4">
{isZh ? '工具注册' : 'Tool Registration'} {isZh ? 'Zod 版本' : 'Zod Version'}
</Heading> </Heading>
<p className="text-gray-600 dark:text-gray-300 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh {isZh
? '每个自定义工具需要定义四个核心属性:name、description、input schema 和 execute 函数。' ? 'Page Agent 使用 Zod 定义工具的输入 schema。支持 Zod 3 (>=3.25.0) 和 Zod 4,请从 zod/v4 子路径导入。不支持 Zod Mini。'
: 'Each custom tool requires four core properties: name, description, input schema, and execute function.'} : 'Page Agent uses Zod for tool input schemas. Both Zod 3 (>=3.25.0) and Zod 4 are supported. Always import from the zod/v4 subpath. Zod Mini is not supported.'}
</p> </p>
<CodeEditor <CodeEditor
code={`import * as zod from 'zod' code={`// Zod 3 (>=3.25.0) or Zod 4
import { PageAgent, tool } from 'page-agent' import { z } from 'zod/v4'`}
// override internal tool
const customTools = {
ask_user: tool({
description:
'Ask the user or parent model a question and wait for their answer. Use this if you need more information or clarification.',
inputSchema: zod.object({
question: zod.string(),
}),
execute: async function (this: PageAgent, input) {
const answer = await do_some_thing(input.question)
return "✅ Received user answer: " + answer
},
})
}
// remove internal tool
const customTools = {
scroll: null, // never scroll
}
const pageAgent = new PageAgent({customTools})
`}
language="javascript" language="javascript"
/> />
</section> </section>
<section> <section>
<Heading id="page-filter" className="text-2xl font-bold mb-4"> <Heading id="define-tools" className="text-2xl font-bold mb-4">
{isZh ? '页面过滤器' : 'Page Filter'} {isZh ? '定义工具' : 'Define Tools'}
</Heading> </Heading>
<BetaNotice />
<p className="text-gray-600 dark:text-gray-300 mb-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
{isZh {isZh
? '通过 pageFilter 属性控制工具在哪些页面可见,提升安全性和用户体验。' ? '使用 tool() 辅助函数定义自定义工具,每个工具包含 description、inputSchema 和 execute 三个属性。'
: 'Control tool visibility on specific pages via the pageFilter property to enhance security and UX.'} : 'Use the tool() helper to define custom tools with description, inputSchema, and execute.'}
</p> </p>
<CodeEditor <CodeEditor
code={`pageAgent.registerTool({ code={`import { z } from 'zod/v4'
name: 'approveOrder', import { PageAgent, tool } from 'page-agent'
description: '审批订单',
input: z.object({ const pageAgent = new PageAgent({
orderId: z.string(), customTools: {
approved: z.boolean()
//
add_to_cart: tool({
description: 'Add a product to the shopping cart by its product ID.',
inputSchema: z.object({
productId: z.string(),
quantity: z.number().min(1).default(1),
}),
execute: async function (input) {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(input),
})
return \`Added \${input.quantity}x \${input.productId} to cart.\`
},
}),
//
search_knowledge_base: tool({
description: 'Search the internal knowledge base and return relevant articles.',
inputSchema: z.object({
query: z.string(),
limit: z.number().max(10).default(3),
}),
execute: async function (input) {
const res = await fetch(
\`/api/kb?q=\${encodeURIComponent(input.query)}&limit=\${input.limit}\`
)
const articles = await res.json()
return JSON.stringify(articles)
},
}), }),
execute: async (params) => {
// 审批逻辑
}, },
// 可选:页面过滤器
pageFilter: {
// 只在订单管理页面显示
include: ['/admin/orders', '/admin/orders/*'],
// 排除特定页面
exclude: ['/admin/orders/archived']
}
})`} })`}
language="javascript" language="javascript"
/> />
</section> </section>
<section> <section>
<Heading id="best-practices" className="text-2xl font-bold mb-4"> <Heading id="override-remove" className="text-2xl font-bold mb-4">
{isZh ? '最佳实践' : 'Best Practices'} {isZh ? '覆盖与移除内置工具' : 'Override & Remove Built-in Tools'}
</Heading> </Heading>
<div className="space-y-4"> <p className="text-gray-600 dark:text-gray-300 mb-4">
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<h3 className="text-lg font-semibold text-yellow-900 dark:text-yellow-300 mb-2">
{isZh ? '⚡ 性能优化' : '⚡ Performance Optimization'}
</h3>
<ul className="text-gray-600 dark:text-gray-300 space-y-1 text-sm">
<li>
{isZh {isZh
? '• 使用 pageFilter 减少不必要的工具加载' ? '使用相同的名称可以覆盖内置工具的行为,设置为 null 则完全移除该工具。'
: 'Use pageFilter to reduce unnecessary tool loading'} : 'Use the same name to override a built-in tool, or set it to null to remove it entirely.'}
</li> </p>
<li>
{isZh <CodeEditor
? '• 在 execute 函数中实现适当的缓存机制' code={`const pageAgent = new PageAgent({
: '• Implement appropriate caching in execute functions'} customTools: {
</li> scroll: null, // remove scroll tool
<li> execute_javascript: null, // remove script execution
{isZh },
? '• 避免在工具中执行耗时的同步操作' })`}
: '• Avoid long-running sync operations in tools'} language="javascript"
</li> />
</ul>
</div>
</div>
</section> </section>
</div> </div>
</div> </div>
@@ -10,11 +10,11 @@ const BASELINE = new Set([
'gemini-3-flash', 'gemini-3-flash',
'deepseek-3.2', 'deepseek-3.2',
'qwen3.5-plus', 'qwen3.5-plus',
'qwen3.5-flash',
]) ])
// Models grouped by brand, newest first // Models grouped by brand, newest first
const MODEL_GROUPS: Record<string, string[]> = { const MODEL_GROUPS: Record<string, string[]> = {
OpenAI: ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
Qwen: [ Qwen: [
'qwen3.5-plus', 'qwen3.5-plus',
'qwen3.5-flash', 'qwen3.5-flash',
@@ -23,6 +23,7 @@ const MODEL_GROUPS: Record<string, string[]> = {
'qwen-3-plus', 'qwen-3-plus',
'qwen3:14b (ollama)', 'qwen3:14b (ollama)',
], ],
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
DeepSeek: ['deepseek-3.2'], DeepSeek: ['deepseek-3.2'],
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'], Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
Anthropic: [ Anthropic: [
@@ -33,6 +34,7 @@ const MODEL_GROUPS: Record<string, string[]> = {
'claude-sonnet-3.5', 'claude-sonnet-3.5',
], ],
xAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'], xAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'],
MiniMax: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed'],
MoonshotAI: ['kimi-k2.5'], MoonshotAI: ['kimi-k2.5'],
'Z.AI': ['glm-5', 'glm-4.7'], 'Z.AI': ['glm-5', 'glm-4.7'],
} }
@@ -67,11 +69,6 @@ export default function Models() {
<Heading id="tested-models" className="text-2xl font-semibold mb-3"> <Heading id="tested-models" className="text-2xl font-semibold mb-3">
{isZh ? '已测试模型' : 'Tested Models'} {isZh ? '已测试模型' : 'Tested Models'}
</Heading> </Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '推荐使用 ToolCall 能力强的轻量级模型。'
: 'Recommended: Fast, lightweight models with strong ToolCall capabilities.'}
</p>
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50"> <div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start"> <div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
{Object.entries(MODEL_GROUPS).map(([brand, models]) => ( {Object.entries(MODEL_GROUPS).map(([brand, models]) => (
@@ -95,6 +92,11 @@ export default function Models() {
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2> <h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2>
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800"> <div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5"> <ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
{isZh
? '⭐ 推荐使用 ToolCall 能力强的轻量级模型'
: '⭐ Recommended: Fast, lightweight models with strong ToolCall capabilities'}
</li>
<li> <li>
{isZh {isZh
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature' ? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature'
@@ -109,39 +111,6 @@ export default function Models() {
</div> </div>
</section> </section>
{/* Security Section */}
<section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">
{isZh ? '🔐 生产环境鉴权建议' : '🔐 Production Authentication'}
</h2>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-5 rounded-r-lg mb-4">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{isZh
? '⚠️ 永远不要把真实的 LLM API Key 发布到前端代码'
: '⚠️ Never commit real LLM API Keys to your frontend code'}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
{isZh ? '后端代理转发' : 'Backend Proxy Pattern'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh
? '在后端搭建一个 LLM 流量转发接口,该接口使用与你网站上其他接口相同的鉴权方式,例如:'
: 'Set up a backend LLM proxy endpoint that uses the same authentication method as other APIs in your website, such as:'}
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>{isZh ? '• Session/Cookie 会话认证' : '• Session/Cookie-based authentication'}</li>
<li>
{isZh ? '• OIDC (OpenID Connect) 单点登录' : '• OIDC (OpenID Connect) single sign-on'}
</li>
<li>
{isZh ? '• 临时 Access Key 或 JWT Token' : '• Temporary Access Key or JWT Token'}
</li>
</ul>
</div>
</section>
{/* Configuration Section */} {/* Configuration Section */}
<section className="mb-10"> <section className="mb-10">
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading> <Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
@@ -153,10 +122,16 @@ const pageAgent = new PageAgent({
model: 'qwen3.5-plus' model: 'qwen3.5-plus'
}); });
// Self-hosted models (e.g., Ollama) // MiniMax
const pageAgent = new PageAgent({
baseURL: 'https://api.minimax.io/v1',
apiKey: 'your-minimax-api-key',
model: 'MiniMax-M2.7'
});
// Self-hosted models (e.g., Ollama) — no apiKey needed
const pageAgent = new PageAgent({ const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1', baseURL: 'http://localhost:11434/v1',
apiKey: 'NA',
model: 'qwen3:14b' model: 'qwen3:14b'
}); });
@@ -169,9 +144,24 @@ const pageAgent = new PageAgent({
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading> <Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4"> <p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh {isZh
? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估使用。有速率限制,可能随时变更。请勿用于生产环境。' ? '以下免费测试接口仅供 PageAgent.js 和 PageAgent Extension 的技术评估和测试使用。'
: 'The following free testing endpoints are provided for technical evaluation of PageAgent.js and PageAgent Extension only. Rate-limited, subject to change. Not for production use.'} : 'The following free testing endpoint is provided for testing and technical evaluation.'}
</p> </p>
<div className="my-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-xs text-gray-600 dark:text-gray-400">
{isZh
? '⚠️ 仅供技术评估和研发用途,禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'
: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{isZh ? '使用条款' : 'Terms of Use'}
</a>
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800"> <div className="bg-gray-50 dark:bg-gray-900/30 rounded-lg p-5 border border-gray-200 dark:border-gray-800">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2"> <h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-2">
Qwen (Alibaba Cloud China) Qwen (Alibaba Cloud China)
@@ -197,21 +187,6 @@ LLM_MODEL_NAME="qwen3.5-plus"
LLM_API_KEY="NA"`} LLM_API_KEY="NA"`}
/> />
</div> </div>
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<p className="text-xs text-gray-600 dark:text-gray-400">
{isZh
? '⚠️ 仅供技术评估和研发用途,禁止用于生产环境。数据通过中国大陆服务器处理。请勿输入任何个人身份信息或敏感数据。使用即表示您同意'
: '⚠️ Strictly for technical evaluation and R&D only. Data is processed via servers in Mainland China. Do not input any PII or sensitive data. By using this API you agree to the'}{' '}
<a
href="https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md#2-testing-api-and-demo-disclaimer--terms-of-use"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline"
>
{isZh ? '使用条款' : 'Terms of Use'}
</a>
</p>
</div>
</section> </section>
{/* Ollama Section */} {/* Ollama Section */}
@@ -247,16 +222,75 @@ LLM_MODEL_NAME="qwen3:14b"`}
{isZh {isZh
? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作' ? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作'
: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'} : 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
<code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 rounded">
$env:OLLAMA_CONTEXT_LENGTH=64000; ollama serve
</code>
</li>
</ul>
</li> </li>
</ul> </ul>
</div> </div>
<div className="mt-4">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
{isZh ? '建议启动参数' : 'Recommended Startup'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh
? '启动 Ollama 时建议配置以下环境变量:扩大上下文窗口、允许跨域访问、监听所有网络接口。'
: 'Start Ollama with these environment variables: larger context window, allow cross-origin access, and listen on all interfaces.'}
</p>
<div className="space-y-2">
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">macOS / Linux</p>
<CodeEditor
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
/>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
Windows (PowerShell)
</p>
<CodeEditor
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
/>
</div>
</div>
</section>
{/* Production Authentication */}
<section className="mb-10">
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{isZh ? (
<>
Web LLM 使{' '}
<code>customFetch</code> Cookie
</>
) : (
<>
If you plan to integrate it into your web app, it's better to have a backend proxy for
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
other methods:
</>
)}
</p>
<CodeEditor
code={`const agent = new PageAgent({
baseURL: '/api/llm-proxy',
model: 'gpt-5.1',
customFetch: (url, init) =>
fetch(url, { ...init, credentials: 'include' }),
});`}
/>
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
{isZh
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'
: '⚠️ NEVER commit real LLM API keys to your frontend code'}
</p>
</div>
</section> </section>
</div> </div>
) )

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