Compare commits

...

89 Commits

Author SHA1 Message Date
Simon 2ad2d06d01 chore(version): bump version to 1.8.0 2026-04-15 03:11:14 +08:00
Simon 8bf7abfe76 chore: wxt version 2026-04-15 03:09:15 +08:00
Simon 099a1251cd Merge branch 'main' of https://github.com/alibaba/page-agent 2026-04-14 15:54:25 +08:00
Simon 354397a1b4 Merge pull request #449 from alibaba/dependabot/npm_and_yarn/development-dependencies-d34cd256e0
chore(deps-dev): bump the development-dependencies group with 11 updates
2026-04-14 15:53:22 +08:00
Simon a1d9e11c05 Merge pull request #448 from alibaba/dependabot/github_actions/github-actions-903567a242
chore(deps): bump actions/upload-pages-artifact from 4 to 5 in the github-actions group
2026-04-14 15:52:48 +08:00
Simon 33538cca05 chore(dependabot): set labels 2026-04-14 15:51:55 +08:00
Simon 50f4866142 Merge pull request #447 from alibaba/fix/iife-keepnames-redeclaration
fix(vite): remove global helper functions in built assets
2026-04-14 15:45:47 +08:00
dependabot[bot] 28a2ebb817 chore(deps-dev): bump the development-dependencies group with 11 updates
Bumps the development-dependencies group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.1` | `7.58.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.2` | `25.6.0` |
| [dotenv](https://github.com/motdotla/dotenv) | `17.4.1` | `17.4.2` |
| [globals](https://github.com/sindresorhus/globals) | `17.4.0` | `17.5.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.58.1` | `8.58.2` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.39` | `0.1.40` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.7.0` | `1.8.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.4` | `19.2.5` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.4` | `19.2.5` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.15.0` | `16.16.0` |


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

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

Updates `dotenv` from 17.4.1 to 17.4.2
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.4.1...v17.4.2)

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

Updates `prettier` from 3.8.1 to 3.8.2
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.1...3.8.2)

Updates `typescript-eslint` from 8.58.1 to 8.58.2
- [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.58.2/packages/typescript-eslint)

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

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

Updates `react` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react)

Updates `react-dom` from 19.2.4 to 19.2.5
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.5/packages/react-dom)

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

---
updated-dependencies:
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: dotenv
  dependency-version: 17.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.1.40
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: react
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.16.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 20:13:25 +00:00
dependabot[bot] 7a08022c72 chore(deps): bump actions/upload-pages-artifact
Bumps the github-actions group with 1 update: [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact).


Updates `actions/upload-pages-artifact` from 4 to 5
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 20:10:33 +00:00
Simon 78c829d955 fix: global helper functions redeclaration 2026-04-14 02:57:13 +08:00
Simon 5008345ce8 Merge pull request #425 from XePope/fix/autofixer-wait-fallback-format
fix(core): correct autoFixer wait fallback action format
2026-04-13 20:33:08 +08:00
Simon a1b7684bf2 Merge pull request #418 from lgy2020/fix/simulator-mask-raf-leak
fix(mask): dispose 后 requestAnimationFrame 循环未停止导致内存泄漏
2026-04-13 20:14:32 +08:00
Simon da67f3b07e chore(controller): .disposed guard 2026-04-13 20:13:41 +08:00
Simon c72ff95670 docs: update links in troubleshooting page 2026-04-13 17:21:26 +08:00
Simon d7a38abdbd chore: update tsconfig 2026-04-13 16:07:24 +08:00
Simon 6fed95b365 chore: enable workspace TypeScript SDK prompt 2026-04-13 15:23:35 +08:00
Simon 0099c1ca8b chore: upgrade wxt 2026-04-13 15:19:05 +08:00
Simon c5c7e8af55 chore: set ts SDK path 2026-04-12 04:29:26 +08:00
Simon 83476da97f docs: update agents.md 2026-04-12 03:39:15 +08:00
Simon 4b6b591849 Merge pull request #423 from alibaba/chore/update-dev-guide-and-pr-skill
chore: update dev guide and PR skill
2026-04-12 03:29:38 +08:00
Simon b5ec9c4406 Merge pull request #422 from alibaba/refactor/typescript-6
refactor: upgrade to TypeScript 6 with source-first monorepo resolution
2026-04-12 03:28:46 +08:00
Simon ca197d14e9 fix: dom typing 2026-04-12 03:16:39 +08:00
Simon acfb336a9a fix: pre-publish and post-publish hooks 2026-04-12 03:16:13 +08:00
Simon 4d27d49752 refactor(setup): upgrade to TypeScript 6 with source-first monorepo resolution 2026-04-12 02:04:21 +08:00
XePope b46693f525 fix(core): correct autoFixer wait fallback action format 2026-04-09 16:49:16 +08:00
Simon c3720ce6b3 chore(repo): update dev guide and PR skill 2026-04-09 03:51:13 +08:00
Simon f68c73c5e9 feat: add maintainer's skills; rm npmrc 2026-04-08 22:37:03 +08:00
Simon 58d992d875 Merge pull request #420 from alibaba/refactor/node-and-eslint-upgrade
refactor: upgrade Node and ESLint
2026-04-08 22:05:06 +08:00
Simon e9784a16d0 chore: upgrade deps 2026-04-08 22:00:29 +08:00
Simon ebe3a3f7c3 chore: rm eslint-config-prettier and eslint-plugin-react-refresh 2026-04-08 21:58:03 +08:00
Simon 9ebb629aca fix: wxt vite-plugin error 2026-04-08 21:16:29 +08:00
Simon 4f80ec1459 refactor: upgrade ESLint 9→10 and simplify React lint toolchain
- Upgrade eslint and @eslint/js to v10
- Replace eslint-plugin-react-x + eslint-plugin-react-dom + eslint-plugin-react-hooks
  with unified @eslint-react/eslint-plugin
- Raise dev Node.js requirement to ^22.13.0 || >=24 (runtime packages unaffected)
- Add .npmrc with engine-strict=true
- Move all @eslint-react rule overrides to eslint.config.js,
  eliminating plugin-specific inline eslint-disable comments
- Fix real issues caught by new rules: useless assignments,
  leaked setTimeout, ref naming, useState setter naming
2026-04-08 20:31:31 +08:00
liuguiyuan 9104064e8c fix(mask): stop requestAnimationFrame loop on dispose to prevent memory leak
The #moveCursorToTarget() method recursively schedules itself via
requestAnimationFrame, creating a continuous animation loop for the
AI cursor. However, dispose() only removes the DOM wrapper element
without stopping this loop, causing:

- CPU waste: rAF callback continues executing every frame (~60fps)
  after the mask is disposed, performing unnecessary calculations
  on a detached cursor element.
- Resource leak: Each SimulatorMask instance creates an unrecoverable
  animation loop that persists for the lifetime of the page.
- Console noise: style assignments to removed DOM nodes may produce
  browser warnings.

Fix: Add a #disposed boolean flag, checked at the top of
#moveCursorToTarget() to short-circuit the recursion. Set the flag
to true in dispose() before removing DOM elements.

Changes:
- Add #disposed field (default false)
- Guard #moveCursorToTarget() with early return when #disposed
- Set #disposed = true in dispose() before cleanup
2026-04-08 18:53:42 +08:00
Simon a43e653a74 chore: simplify issue config 2026-04-08 18:52:53 +08:00
Simon 3bffd76626 chore: update github templates 2026-04-08 18:49:16 +08:00
Simon 015101d6ae Merge pull request #416 from alibaba/dependabot/npm_and_yarn/development-dependencies-ffd7d36be5
chore(deps-dev): bump typescript-eslint from 8.58.0 to 8.58.1 in the development-dependencies group
2026-04-08 18:45:14 +08:00
dependabot[bot] 8904e602dd chore(deps-dev): bump typescript-eslint
Bumps the development-dependencies group with 1 update: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `typescript-eslint` from 8.58.0 to 8.58.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.58.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.58.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-08 09:36:39 +00:00
Simon d384967e41 chore: dependabot config 2026-04-08 17:34:17 +08:00
Simon 531b59f538 chore: add git-cleanup skill 2026-04-08 17:28:11 +08:00
Simon bef26c3feb Merge pull request #415 from alibaba/fix/security-deps-hono-vite-path-to-regexp
fix(deps): update hono, path-to-regexp and vite for security patches
2026-04-08 17:21:17 +08:00
Simon c48dd538a0 fix(deps): update hono, @hono/node-server, path-to-regexp and vite for security patches
- hono 4.12.8 → 4.12.12 (cookie bypass, middleware bypass, path traversal, IP matching)
- @hono/node-server 1.19.11 → 1.19.13 (middleware bypass)
- path-to-regexp 8.3.0 → 8.4.2 (ReDoS)
- vite ^7.3.1 → ^7.3.2 (arbitrary file read, fs.deny bypass, path traversal)
2026-04-08 17:20:14 +08:00
Simon 950767c1bf Merge pull request #414 from alibaba/fix/panel-history-max-height-responsive
fix(ui): make panel history max-height responsive to viewport
2026-04-08 17:07:09 +08:00
Simon 12b6001d53 fix(ui): make panel history max-height responsive to viewport 2026-04-08 17:05:10 +08:00
Simon 2c1972ed89 Merge pull request #413 from alibaba/fix/ext-result-card-font-size
fix(ext): increase result card font size and use foreground color
2026-04-08 16:52:39 +08:00
Simon 362bf822f7 fix(ext): increase result card font size and use foreground color
Fixes conflicting text-xs/text-[11px] classes and muted color on the
ResultCard component, making the result text more readable.

Closes #360
2026-04-08 16:50:09 +08:00
dependabot[bot] 4734e2df26 chore(deps-dev): bump the development-dependencies group with 6 updates (#405) 2026-04-07 15:39:11 +08:00
Simon c2648c1749 chore(setup): update TypeScript SDK path in settings 2026-04-04 03:44:31 +08:00
Simon 7071a6c26a fix(mcp): bind HTTP+WS server to localhost only (#399) 2026-04-04 03:15:20 +08:00
Simon 303ece84fd feat: add update-changelog skill documentation 2026-04-04 03:14:37 +08:00
Simon 8a0266ce8d docs: update changelog for version 1.7.1 2026-04-04 03:14:23 +08:00
Simon cb55daf47d chore(version): bump version to 1.7.1 2026-04-04 01:37:49 +08:00
Simon 43415d3139 feat(website): sticky header 2026-04-04 01:34:54 +08:00
Simon 9cc9edf17e docs: lm studio (#398)
* docs(page): lm studio doc
* docs: update documentation and improve model section
2026-04-04 01:15:41 +08:00
Simon e290e2f67a Merge pull request #396 from alibaba/fix/interactive-aria-attrs-detection
fix(controller): detect elements with ARIA state attrs as interactive
2026-04-03 20:22:09 +08:00
Simon 4272939217 fix(controller): treat interactive with aria as distinct 2026-04-03 20:21:11 +08:00
Lubrsy 148bdb6839 fix: recognize role="listitem" as interactive element (#203)
* fix: recognize role="listitem" as interactive element
* fix: DISTINCT_INTERACTIVE_ROLES

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
2026-04-03 20:09:48 +08:00
Simon 68cee4cc64 Merge pull request #395 from alibaba/feat/controller-keep-semantic-tags
feat(controller): add experimental `keepSemanticTags` config
2026-04-03 19:07:36 +08:00
Simon 7e9027167d feat(controller): add keepSemanticTags config to keep the semantic structure of the page 2026-04-03 19:01:54 +08:00
Simon 6630774dc9 Merge pull request #390 from alibaba/fix/controller-improve-scrolling
feat(controller): improve scroll container detection and tool guidance
2026-04-03 17:29:38 +08:00
Simon 0ca1c8de0b feat(controller): consider hasScrollbarSignal when detecting scrollable 2026-04-03 16:09:35 +08:00
Simon 85a33ac1a4 feat(controller): improve scroll action 2026-04-02 22:05:47 +08:00
Simon b8fd1aaebc Merge pull request #389 from alibaba/fix/ext-postmessage-source-guard 2026-04-02 19:06:06 +08:00
Simon 13d952dc9d fix(ext): guard postMessage listeners against iframe sources
Add `e.source !== window` check to both content script and main-world
script message handlers, preventing iframes from injecting or
intercepting extension bridge messages.
2026-04-02 18:53:59 +08:00
Simon 1f2f5a44d3 Merge pull request #332 from mvanhorn/fix/scroll-direction-pixels
fix(page-controller): apply scroll direction to pixels parameter
2026-04-02 18:36:42 +08:00
Simon 8159aa58a6 Merge branch 'main' into fix/scroll-direction-pixels 2026-04-02 18:31:56 +08:00
Simon 2bdb3be81c Merge pull request #356 from lgy2020/fix/aria-attribute-detection
fix(isInteractiveCandidate): use hasAttribute with known aria list to detect aria- attributes
2026-04-02 17:41:36 +08:00
Simon bde630f55d chore(controller): add @edit mark 2026-04-02 17:38:02 +08:00
Simon 3efef0ec42 fix(controller): clean up INTERACTIVE_ARIA_ATTRS 2026-04-02 17:33:27 +08:00
Simon 1141287733 Merge pull request #386 from alibaba/feat/ext-execute-system-instruction
feat(ext): add `systemInstruction` to ExecuteConfig
2026-04-02 16:37:29 +08:00
Simon ec08b05da1 feat(ext): add systemInstruction to ExecuteConfig
Expose a serializable `systemInstruction` string field on the
page-facing ExecuteConfig, mapped to `instructions.system` when
creating MultiPageAgent. Functions cannot cross the postMessage
boundary, so this flat string field replaces the object form.

Closes #359
2026-04-02 16:34:57 +08:00
Simon 5ea2edd13c Merge pull request #384 from Anyexyz/main
fix: extract currentScript outside setTimeout to avoid null pointer
2026-04-02 16:20:17 +08:00
Anyexyz 1999bbeda9 fix: extract currentScript outside setTimeout to avoid null pointer 2026-04-02 16:17:46 +08:00
Simon b3ec571341 Merge pull request #385 from alibaba/docs/website-models-qwen3.6-plus
docs(website): add qwen3.6-plus to models page
2026-04-02 15:33:23 +08:00
Simon d0de8ad838 docs(website): add qwen3.6-plus to models page 2026-04-02 15:30:58 +08:00
Simon 0402a6be03 fix(tabs): error message typo 2026-03-31 21:04:53 +08:00
Simon 6823e04ca9 chore(version): bump version to 1.7.0 2026-03-31 20:45:12 +08:00
Simon 9ae1527201 Merge pull request #378 from alibaba/fix/controller-click-action-robust 2026-03-31 20:41:27 +08:00
Simon 2b20b48dff chore(controller): reuse pointer xy 2026-03-31 20:27:04 +08:00
Simon 296459924a feat(controller): enhance click action with elementFromPoint 2026-03-31 20:02:39 +08:00
Simon 8eee3b27e2 feat(controller): fix SimulatorMast mem leak; add passthrough events 2026-03-31 19:59:57 +08:00
Simon 32d6f0c74b fix(controller): click action robust 2026-03-31 17:41:49 +08:00
Simon 6e4094565a Merge pull request #369 from alibaba/dependabot/github_actions/github-actions-e5be0e1c7a
chore(deps): bump the github-actions group with 2 updates
2026-03-31 15:11:09 +08:00
Simon da2b7b5a8f Merge pull request #370 from alibaba/dependabot/npm_and_yarn/production-dependencies-90c90c5d9d
chore(deps): bump @modelcontextprotocol/sdk from 1.27.1 to 1.29.0 in the production-dependencies group
2026-03-31 15:09:37 +08:00
Simon 3207c3ffc1 Merge pull request #371 from alibaba/dependabot/npm_and_yarn/development-dependencies-003fb0370a 2026-03-31 12:22:35 +08:00
dependabot[bot] 784627a7c1 chore(deps-dev): bump the development-dependencies group with 3 updates
Bumps the development-dependencies group with 3 updates: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint), [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) and [simple-icons](https://github.com/simple-icons/simple-icons).


Updates `typescript-eslint` from 8.57.2 to 8.58.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.58.0/packages/typescript-eslint)

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

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

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.14.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-30 20:22:53 +00:00
dependabot[bot] 46d6c02c95 chore(deps): bump @modelcontextprotocol/sdk
Bumps the production-dependencies group with 1 update: [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk).


Updates `@modelcontextprotocol/sdk` from 1.27.1 to 1.29.0
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](https://github.com/modelcontextprotocol/typescript-sdk/compare/v1.27.1...v1.29.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 20:21:25 +00:00
dependabot[bot] 6d46719fe8 chore(deps): bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/configure-pages](https://github.com/actions/configure-pages) and [actions/deploy-pages](https://github.com/actions/deploy-pages).


Updates `actions/configure-pages` from 5 to 6
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

Updates `actions/deploy-pages` from 4 to 5
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 20:21:07 +00:00
liuguiyuan f2b6c9dfd2 fix: use hasAttribute with known aria list for interactive candidate detection
Replace broken el.hasAttribute("aria-") with a curated list of 27
aria attributes checked via hasAttribute. Each check is O(1).

WAI-ARIA 1.2 defines ~50 aria attributes total per MDN.
Of these ~27 appear on interactive elements such as buttons,
inputs, sliders, and dialogs. The remaining ~20 are structural
container attributes like aria-live, aria-colcount, and
aria-rowspan that only appear on non-interactive containers.
Checking them would not change results.
2026-03-31 00:57:44 +08:00
Matt Van Horn 005bc8ab44 fix(page-controller): apply scroll direction to pixels parameter
Two bugs in the scroll direction logic:

1. Vertical scroll with `pixels` ignores the `down` boolean because the
   `??` operator bypasses the direction multiplier when pixels is provided.
   Fix: move the direction multiplier outside the `??` so it applies to
   both the pixels and numPages paths.

2. Horizontal scroll with `pixels` applies direction twice - once in
   PageController.ts and again in actions.ts - causing a double negation
   that reverses the intended direction. Fix: remove the redundant
   direction logic from actions.ts since PageController already signs
   the scroll amount.

Also removes the now-unused `down` and `right` parameters from the
scrollVertically() and scrollHorizontally() action functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 08:38:32 -07:00
90 changed files with 3738 additions and 2192 deletions
+64
View File
@@ -0,0 +1,64 @@
---
name: git-cleanup
description: Clean up local git branches and remotes accumulated from PR reviews. Use when the user asks to clean branches, remove stale remotes, or tidy up the local git state.
---
# Git Cleanup
Clean local branches (except `main` and current branch) and non-origin remotes.
Useful after reviewing multiple PRs that leave behind tracking branches and contributor remotes.
## Workflow
### 1. Survey
Run these in parallel:
```bash
git branch
git remote
git status --short
git stash list
```
Present a summary of how many branches and remotes will be removed.
### 2. Safety Checks — Ask Before Proceeding
**Stop and ask for confirmation if any of these are true:**
- Current branch is NOT `main`
- Working tree has uncommitted changes or stashes
- Any branch is ahead of its upstream
List unmerged branches separately and let the user decide.
### 3. Delete Branches
```bash
# Safe delete first (fails on unmerged branches)
git branch | grep -v '^\*' | grep -v '^\s*main$' | xargs git branch -d 2>&1
# Force-delete only with explicit user approval
git branch -D <branch>
```
### 4. Remove Remotes
```bash
git remote | grep -v '^origin$' | xargs -I{} git remote remove {}
```
### 5. Confirm
```bash
git branch && echo "---" && git remote
```
Report what was cleaned up.
## Key Rules
- **NEVER** delete `main` or the current checked-out branch
- **NEVER** force-delete unmerged branches without user confirmation
- If in doubt, ask
+100
View File
@@ -0,0 +1,100 @@
---
name: pre-impl-discussion
description: "Conduct a thorough pre-implementation discussion before making significant changes. Use when the user wants to discuss, plan, or evaluate a change before implementing it — especially when they say words like 'discuss', 'evaluate', 'plan', or 'let's talk about'."
argument-hint: "Describe the change to evaluate"
---
# Pre-Implementation Discussion
Facilitate a structured, collaborative discussion to evaluate a proposed change **before any implementation begins**.
## Golden Rule
**Do NOT implement anything until the user explicitly confirms the final plan and says to proceed.** Not even "let me try on a branch" — that's implementation. The user will tell you when discussion is over.
## Interaction Style
- **Concise during discussion.** Each intermediate response should be short and focused on the current question. Do NOT repeat the full plan in every response.
- **Complete when finalizing.** Once the plan is mature and the user asks for it, present a single comprehensive (but terse) summary for final review.
- **Ask, don't assume.** When uncertain about project context, constraints, or preferences — ask. The user prefers collaborative discussion over receiving a pre-baked answer.
- **Challenge the premise.** Question whether the proposed change is the right one. Suggest simpler alternatives if they exist.
- **Match the user's language.** Reply in the same language the user writes in.
## Procedure
### 1. Understand the Current State
Before forming any opinion:
- **Read the relevant source files** — configs, code, docs that relate to the change
- **Understand the project structure** — what's published vs private, what environments things target, existing constraints
- **Map the impact surface** — which files, packages, or systems would be affected
- **Estimate implementation cost** — how many files to touch, how much config to rewrite, what could break
Do NOT skip this step. Do NOT rely on assumptions about what "most projects" do.
### 2. Research Thoroughly
- **Consult official sources** — docs, changelogs, migration guides, GitHub issues
- **Verify every claim with evidence.** Don't say "X supports Y" without a source. If unsure, say so.
- **Search in parallel** to save time — batch independent queries
Common pitfalls:
- Assuming compatibility without checking actual version constraints
- Confusing roadmap/aspirations with actual released state
- Missing transitive constraints (a dependency of a dependency)
### 3. Present Findings (Concise)
Share a **brief** assessment. Tables work well for comparisons. Highlight **blockers** and **unknowns** prominently — don't bury them in paragraphs.
### 4. Identify Key Decisions
Surface the decisions the user needs to make. Present them as clear choices with trade-offs, not as a recommendation monologue.
For each decision point:
- What are the options? (2-3 max)
- What does each option cost or give up?
- What's your lean and why? (one sentence)
### 5. Iterate
The user will ask follow-up questions, raise concerns, or challenge assumptions. For each round:
- **Research if needed** — don't answer from instinct when facts are available
- **Answer only what was asked** — don't re-present the entire plan
- **Update your mental model** based on user feedback
Common mistakes in this phase:
- Repeating the full plan after every small clarification
- Answering a narrow question with a broad redesign
- Treating user questions as confirmation to proceed
### 6. Finalize the Plan
When the user signals the discussion is converging, present the **complete plan once**:
- **What changes** — concrete list of actions (table or bullet points)
- **Impact surface** — what files/systems are affected, estimated effort
- **What does NOT change** — explicitly state scope boundaries
- **Risks or open items** — anything that needs testing or further verification
Keep it terse. Tables over paragraphs. No explanations the user already heard during discussion.
### 7. Wait for Confirmation
After presenting the final plan, **stop and wait**. The user will either:
- Confirm → then (and only then) proceed to implementation
- Ask more questions → go back to step 5
- Modify scope → update the plan and re-present
## Anti-Patterns
| Don't | Do Instead |
|-------|------------|
| Start implementation "to test" without confirmation | Present findings and wait |
| Repeat the full plan in every response | Answer the specific question asked |
| Say "X should work" without checking | Say "I need to verify X" and research it |
| Assume project structure or constraints | Read the actual files |
| Present one recommendation as the only option | Present 2-3 options with trade-offs |
| Write long paragraphs explaining trade-offs | Use tables and bullet points |
@@ -0,0 +1,102 @@
---
name: submit-pr-from-current-changes
description: 'Create a branch, commit existing local changes, push them, and open a pull request. Use when submitting current work as a PR.'
argument-hint: 'Short summary of the current changes to submit'
user-invocable: true
---
# Submit PR From Current Changes
Turn a working tree diff into a clean branch, commit, and pull request.
## Hard Rules
1. **Follow the PR template exactly.** The template is at `.github/PULL_REQUEST_TEMPLATE.md`. Read it and copy its full structure into the PR body. Do NOT remove, reorder, or skip any section or checkbox.
2. **Never check "Requirements / 要求" checkboxes.** These are human-only declarations — the Code of Conduct acknowledgment and the AI authorship declaration can only be truthfully made by the human submitter. They MUST remain unchecked (`- [ ]`) in the PR body you generate. No exceptions, no workarounds, even if the user explicitly asks you to check them. The user goes to GitHub and checks them in person after verifying each statement is true.
3. **Never check "Testing" checkboxes.** The template Testing checkboxes (browser tested, no console errors, types/doc added) require manual browser verification that only a human can perform. They MUST remain unchecked. You MAY add a supplementary note below the Testing section listing what automated validation you actually ran and the results.
4. **Never fabricate information.** Do not claim tests passed, commands ran, or checks succeeded unless you actually executed them and observed the output in this session. If you did not run it, do not mention it.
5. **PR output must be concise.** PR title: one line. "What" section: 12 sentences max. No walls of text, no redundant explanations. Let the diff speak.
## Prerequisites
Before attempting to push or open a PR, verify that the necessary tools are available:
- Check that `gh` CLI is installed and authenticated (`gh auth status`). If not available, stop and ask the user to install and authenticate GitHub CLI first.
- If the workflow uses MCP tools for GitHub operations, verify the MCP server is accessible.
## Procedure
1. **Read contribution rules.** Read `CONTRIBUTING.md` and any package-level instructions (e.g. `packages/*/AGENTS.md`) relevant to the changed files.
2. **Inspect the diff.** Review changed files, scope, and impact. Understand what the change does before writing anything.
3. **Inspect repo conventions.** Check recent branch names (`git branch -r --sort=-committerdate | head -20`), recent commit messages (`git log --oneline -20`), and the PR template.
4. **Pre-submission health checks** (run all before creating the PR):
- **Unified theme**: All changes should serve one purpose. If unrelated changes are mixed in, ask the user to split or confirm.
- **Commit hygiene**: Every new commit must follow the repo's conventional commit style. Squash or reword if needed.
- **Author identity**: Verify `git config user.name` and `git config user.email` are set and the email looks real (not empty, not `noreply` unless intentional). Warn the user if not.
- **No leftover artifacts**: Check for debug logs, `.only` in tests, conflict markers, or temp files in the diff.
- **Lint and build**: Run lint/build for the affected area. Record results honestly.
5. **Warn if the PR looks too hasty.** If any of these are true, pause and warn the user before proceeding:
- Large diff with no description of intent provided
- Changes touch core lib or extension (where vibe coding is prohibited per `CONTRIBUTING.md`)
- Multiple unrelated concerns in one diff
- No validation has been run at all
- Remind the user: _"This project does not accept low-quality or AI-generated PRs without meaningful human review. Please review your changes carefully."_
6. **Branch.** Check the current branch first:
- If already on a non-main feature branch with a valid name (matching `type/topic` convention), reuse it.
- If the branch name does not follow repo conventions (e.g. missing prefix, unclear topic), ask the user whether to rename or create a new one.
- If on `main` or a default branch, create a new branch from it with a short kebab-case name: prefix (`fix/`, `feat/`, `docs/`, `refactor/`, `chore/`) + concrete topic words.
7. **Stage and commit.** Stage only the intended files. Use `type(scope): subject` format. Keep the subject specific and compact.
8. **Push.** Push with `-u origin HEAD`.
9. **Open PR.** Use `gh pr create` with the full PR template structure. Fill in "What" and "Type" sections based on the actual diff. Leave all "Testing" and "Requirements" checkboxes unchecked.
10. **Report results.** Tell the user: branch name, commit hash, PR link, and what validation actually ran (with pass/fail).
## Post-Submission Reminder
After successfully opening the PR, ALWAYS give a brief reminder in the user's language. Keep it concise and natural, but make sure it clearly tells the user:
1. They need to test the changes themselves in the browser.
2. They need to go to the PR page and check the Testing and Requirements checkboxes only after verifying each item.
3. The PR will not enter review until those checkboxes are checked.
4. The project does not accept autonomously AI-generated PRs, so they should only check the AI declaration if it is truthful.
## Branch Naming
- Prefer short kebab-case names with a repo-consistent prefix such as `fix/`, `feat/`, `docs/`, `refactor/`, or `chore/`.
- Match the change type first, then the smallest useful topic.
- Prefer concrete topic words over issue text dumps.
## Commit Style
- Use the repository's prevailing commit style (conventional commits).
- Use `type(scope): subject` when scope is clear from the changed area.
- Keep the subject specific and compact.
- If multiple commits exist on the branch, each one must independently follow conventions.
## Validation Strategy
- Default to enough validation to defend the PR, not the absolute minimum.
- Prefer area-specific build, lint, or test commands when they exist.
- Escalate to broader validation when the diff crosses packages, changes shared code, or affects release behavior.
- Never claim checks that did not actually run.
- You MAY note what you ran and the results below the Testing section in the PR body, but do NOT check the template checkboxes.
## Decision Points
- No local changes → stop, say so.
- `gh` CLI not available → stop, ask user to install it.
- Unrelated files mixed → stage only the intended subset or ask whether the work should be split.
- If the repo has area-specific instructions, read them before naming the branch or writing the PR.
- Change is small but high-risk → prefer broader validation.
- Change is narrowly scoped and low-risk → run the most relevant checks, not arbitrary unrelated ones.
- Template has declarations you cannot truthfully check → leave unchecked, tell the user why.
## Completion Checks
- The branch exists remotely and tracks upstream.
- The commit message(s) match repo style.
- All commit authors have proper name and email configured.
- The PR title matches the commit intent.
- The PR body follows the template structure completely.
- All "Requirements" and "Testing" checkboxes are unchecked (`- [ ]`) in the PR body. Double-check: if any `- [x]` appears in these sections, it is a violation — fix it before submitting.
- The reported validation is accurate — nothing fabricated.
- The post-submission reminder was delivered in the user's language, concisely and accurately.
+109
View File
@@ -0,0 +1,109 @@
---
name: update-changelog
description: 'Update docs/CHANGELOG.md from git history, GitHub releases, and code diffs. Use when: writing release notes, syncing the latest changelog entry, summarizing a new tag, or keeping changelog wording concise and consistent.'
argument-hint: 'Describe the target, for example: latest version only'
---
# Update Changelog
Update `docs/CHANGELOG.md` from repository evidence instead of guesswork.
## When to Use
- Add the newest release entry to `docs/CHANGELOG.md`
- Sync changelog text with GitHub Releases
- Summarize the latest tag from git history or code diffs
## Defaults
- Keep the wording brief
- Only add the latest missing version unless the user explicitly asks to backfill older releases
- Prefer GitHub sources first, especially Releases and tag metadata
- Skip `Features` / `Improvements` / `Bug Fixes` headings when the release only has a few clear items
## Procedure
### 1. Read the local style
- Open `docs/CHANGELOG.md`
- Match the existing tone, bullet style, section ordering, and date format
### 2. Determine the target release
- Read the root `package.json` version and compare it with the top changelog entry
- Find the previous tag for the target version
- If the latest version is already documented, stop and report that no changelog update is needed
### 3. Gather evidence
Prefer these sources in order:
1. GitHub release notes
```bash
GH_PAGER=cat gh release view v<version> --repo <owner>/<repo> --json tagName,name,publishedAt,body
```
2. Tag date
```bash
git log -1 --format=%cs v<version>
```
3. Commit history between tags
```bash
git --no-pager log --format='%h %s' --no-merges v<previous>..v<version>
```
4. Diff scope when commit subjects are vague
```bash
git --no-pager diff --name-only v<previous>..v<version>
git --no-pager diff --stat v<previous>..v<version>
```
5. Read touched files directly only when the user-visible change is still unclear
### 4. Distill what belongs in the changelog
Include:
- User-visible features
- Important behavior changes
- Bug fixes that improve reliability, compatibility, or developer experience
- Small docs updates only when they materially change supported setups or onboarding
Exclude unless explicitly requested:
- Pure version bumps
- Routine dependency updates
- Internal refactors with no visible impact
- Mechanical formatting noise
### 5. Choose the structure
- If the release has 1 to 3 clear points, write flat bullets directly under the version heading
- If the release has several distinct items, use short headings such as `### Features`, `### Improvements`, and `### Bug Fixes`
- Do not force categories when they make the entry longer or noisier
### 6. Write the entry
- Add or update only the requested release entry
- Keep bullets short and concrete
- Reuse project terminology from nearby entries
- Avoid marketing language
- Do not copy noisy GitHub release text verbatim
### 7. Verify
- Check dates, version numbers, and tag boundaries
- Ensure Markdown structure matches nearby entries
- Confirm no intermediate versions were added unless requested
## Completion Checks
- The newest requested version is documented
- The wording is concise and consistent with the surrounding changelog
- The entry is backed by GitHub releases, git history, or code diff evidence
- Low-signal internal changes were left out
+24 -26
View File
@@ -1,61 +1,59 @@
name: Bug Report name: Bug Report
description: Report a bug description: Report a bug
title: '[Bug] ' title: '[Bug] '
labels: ['bug'] type: Bug
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines. Thanks for your interest in improving the project! Before submitting, please search for existing issues.
感谢您对改进项目的兴趣!提交前请阅读我们的指南 感谢参与我们的社区!请先搜索现有 Issue,避免重复提交
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md) - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md) - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: What happened? label: What happened? / 问题描述
placeholder: Describe the bug and expected behavior description: Describe what you expected vs what actually happened.
placeholder: 'A clear description of the bug.'
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: code id: reproduction
attributes: attributes:
label: Code label: How to reproduce / 如何复现
render: typescript description: Provide steps, URL, config or code snippet to reproduce the bug.
placeholder: Minimal reproduction code placeholder: '1. Open URL: https://...'
validations: validations:
required: false required: true
- type: input
id: version
attributes:
label: Version
placeholder: '0.0.0'
- type: input - type: input
id: browser id: browser
attributes: attributes:
label: Browser label: Browser
placeholder: 'Chrome 120, Firefox 119, etc.' placeholder: 'e.g. Chrome 130'
validations:
required: false
- type: input
id: version
attributes:
label: version
placeholder: '0.0.0'
validations:
required: false
- type: checkboxes - type: checkboxes
id: community id: checklist
attributes: attributes:
label: Community Communication / 社区沟通 label: Before submitting
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options: options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。 - label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the Code of Conduct. / 我已阅读行为准则。 - label: I have read the Code of Conduct. / 我已阅读行为准则。
required: true required: true
- label: I have searched existing issues and this is not a duplicate.
required: true
validations: validations:
required: true required: true
-6
View File
@@ -1,11 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Questions & Ideas / 问题与想法(Discussions
url: https://github.com/alibaba/page-agent/discussions
about: Use Discussions for Q&A and ideation. 使用 Discussions 进行问答与想法交流。
- name: Security Report / 安全问题报告
url: https://github.com/alibaba/page-agent/security/policy
about: Report security vulnerabilities responsibly. 通过安全页面报告漏洞。
- name: Contributing Guide / 贡献指南 - name: Contributing Guide / 贡献指南
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
about: How to contribute code and ideas. 如何进行贡献与提交代码。 about: How to contribute code and ideas. 如何进行贡献与提交代码。
+9 -14
View File
@@ -1,32 +1,28 @@
name: Feature Request name: Feature Request
description: Suggest a feature description: Suggest a feature
title: '[Feature] ' title: '[Feature] '
labels: ['enhancement'] type: Feature
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for your interest in improving the project! Before submitting, please read our guidelines. Thanks for your interest in improving the project! Before submitting, please search for existing issues.
感谢您对改进项目的兴趣!提交前请阅读我们的指南 感谢参与我们的社区!请先搜索现有 Issue,避免重复提交
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md) - [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md) - [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: Feature Description / 功能描述 label: Feature Description / 功能描述
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。 description: Describe the problem, solution, and any API changes.
placeholder: | placeholder: |
**Problem**: **Problem**:
What problem does this solve?
**Solution**: **Solution**:
How should this work?
**Proposed API**: **Proposed API**:
```typescript
// code here
``` ```
validations: validations:
required: true required: true
@@ -34,14 +30,13 @@ body:
- type: checkboxes - type: checkboxes
id: community id: community
attributes: attributes:
label: Community Communication / 社区沟通 label: Before submitting
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
options: options:
- label: I will be polite and respectful. / 我会保持礼貌与尊重。 - label: I will be polite and respectful. / 我会保持礼貌与尊重。
required: true required: true
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
required: true
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。 - label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
required: true required: true
- label: I have searched existing issues and this is not a duplicate.
required: true
validations: validations:
required: true required: true
+5 -7
View File
@@ -2,15 +2,15 @@
Brief description of changes. Brief description of changes.
Closes #(issue)
## Type ## Type
- [ ] Breaking change
- [ ] Bug fix - [ ] Bug fix
- [ ] Feature / Improvement - [ ] Feature / Improvement
- [ ] Refactor - [ ] Refactor / Chores
- [ ] Documentation - [ ] Documentation / Website / Demo / Testing
- [ ] Website
- [ ] Demo / Testing
- [ ] Breaking change
## Testing ## Testing
@@ -18,8 +18,6 @@ Brief description of changes.
- [ ] No console errors - [ ] No console errors
- [ ] Types/doc added - [ ] Types/doc added
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) . / 我已阅读并遵守行为准则。
+11 -4
View File
@@ -4,28 +4,35 @@ updates:
directory: '/' directory: '/'
schedule: schedule:
interval: 'weekly' interval: 'weekly'
labels:
- 'dependencies'
groups: groups:
# 生产依赖 - 小版本更新
production-dependencies: production-dependencies:
dependency-type: 'production' dependency-type: 'production'
update-types: update-types:
- 'minor' - 'minor'
- 'patch' - 'patch'
# 开发依赖 - 小版本更新
development-dependencies: development-dependencies:
dependency-type: 'development' dependency-type: 'development'
update-types: update-types:
- 'minor' - 'minor'
- 'patch' - 'patch'
# Major 更新单独处理(不分组,需要人工审查) development-major:
# 安全更新也不分组,Dependabot 会自动优先创建独立 PR dependency-type: 'development'
update-types:
- 'major'
# Production major updates are intentionally ungrouped for individual review.
# Security updates are also ungrouped — Dependabot prioritizes them automatically.
- package-ecosystem: 'github-actions' - package-ecosystem: 'github-actions'
directory: '/' directory: '/'
schedule: schedule:
interval: 'weekly' interval: 'weekly'
labels:
- 'dependencies'
groups: groups:
github-actions: github-actions:
patterns: patterns:
+3 -3
View File
@@ -28,13 +28,13 @@ jobs:
run: npm run build:website run: npm run build:website
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v6
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v4 uses: actions/upload-pages-artifact@v5
with: with:
path: './packages/website/dist' path: './packages/website/dist'
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v5
+3
View File
@@ -25,6 +25,9 @@ dist-ssr
*.sw? *.sw?
.qoder .qoder
# publish backup
package.json.bak
# env files # env files
.env .env
.env.* .env.*
+4 -1
View File
@@ -35,5 +35,8 @@
"blanks-around-lists": false, "blanks-around-lists": false,
"ol-prefix": false, "ol-prefix": false,
"no-duplicate-heading": false "no-duplicate-heading": false
} },
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
} }
+7 -6
View File
@@ -5,7 +5,7 @@
This is a **monorepo** with npm workspaces: This is a **monorepo** with npm workspaces:
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm - **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
- **Extension** (`packages/extension/`) - Browser extension (WXT + React) 🚧 WIP - **Extension** (`packages/extension/`) - Browser extension (WXT + React)
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`** - **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
Internal packages: Internal packages:
@@ -18,10 +18,11 @@ Internal packages:
## Development Commands ## Development Commands
```bash ```bash
npm start # Start website dev server npm start # Start website dev server
npm run build # Build all packages npm run build # Build all packages
npm run build:libs # Build all libraries npm run build:libs # Build all libraries
npm run lint # ESLint with TypeScript strict rules npm run typecheck # Typecheck all packages (libs + website + extension)
npm run lint # ESLint
npm run zip -w @page-agent/ext # Zip the extension package npm run zip -w @page-agent/ext # Zip the extension package
``` ```
@@ -29,7 +30,7 @@ npm run zip -w @page-agent/ext # Zip the extension package
### Monorepo Structure ### Monorepo Structure
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages. Source-first monorepo: library `package.json` exports point to `src/*.ts` during development. At publish time, `scripts/pre-publish.js` promotes `publishConfig` fields to top-level (swapping to `dist/`), and `scripts/post-publish.js` restores the originals.
``` ```
packages/ packages/
+12 -12
View File
@@ -50,8 +50,8 @@ Fastest way to try PageAgent with our free Demo LLM:
| Mirrors | URL | | Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- | | ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.3/dist/iife/page-agent.demo.js | | Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.3/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
### NPM Installation ### NPM Installation
@@ -74,6 +74,16 @@ await agent.execute('Click the login button')
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview). For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
## 🌟 Awesome Page Agent
Built something cool with PageAgent? Add it here! Open a PR to share your project.
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
| Project | Description |
| -------- | ----------------------------------------------------------- |
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
## 🤝 Contributing ## 🤝 Contributing
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows. We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
@@ -104,16 +114,6 @@ excellent work on web automation and DOM interaction patterns that helped make
this project possible. this project possible.
``` ```
## 🌟 Awesome Page Agent
Built something cool with PageAgent? Add it here! Open a PR to share your project.
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
| Project | Description |
| -------- | ----------------------------------------------------------- |
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
--- ---
**⭐ Star this repo if you find PageAgent helpful!** **⭐ Star this repo if you find PageAgent helpful!**
+50
View File
@@ -5,6 +5,56 @@ 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.7.1] - 2026-04-04
### Features
- **Optional `keepSemanticTags`** - Added an experimental `keepSemanticTags` config to preserve semantic structure in PageController output
- **Per-task extension system instructions** - Extension `ExecuteConfig` now supports `systemInstruction`
### Improvements
- **Smarter scroll handling** - Scroll container detection and scroll direction handling are more reliable
- **Better accessibility-aware element detection** - Interactive candidates with supported ARIA attributes and `role="listitem"` are recognized more accurately
### Bug Fixes
- Fixed iframe-origin filtering for extension `postMessage` listeners
- Avoided a `currentScript` null pointer during deferred initialization
## [1.7.0] - 2026-03-31
- **More reliable click actions** - Click handling now reuses pointer coordinates, verifies targets with `elementFromPoint`, and behaves better on layered layouts
- **Better mask event handling** - `SimulatorMask` now supports passthrough events when automation should not fully swallow input
- Fixed a `SimulatorMask` memory leak
## [1.6.3] - 2026-03-30
### Features
- **Experimental all-tabs control** - Extension can include and control all browser tabs via `experimentalIncludeAllTabs`
### Improvements
- **Calmer empty state motion** - Disabled the EmptyState auto-start animation in the extension UI
- **Cleaner extension docs** - Simplified setup and tab-control documentation across the README and developer guide
### Bug Fixes
- Fixed new-tab detection from content scripts
- Fixed tab deduplication and multi-window handling in the extension
## [1.6.2] - 2026-03-25
- **Longer task input** - The UI task input now accepts up to 1000 characters
- **Contributor docs refresh** - Added a maintainer note and refreshed contributor-facing documentation
- Fixed lint issues in the release pipeline
## [1.6.1] - 2026-03-22
- **Internal PageController action exports** - PageController actions are now exposed as internal methods for easier reuse across packages
- **Expanded docs** - Added MCP docs and clarified project limitations and homepage details
## [1.6.0] - 2026-03-21 ## [1.6.0] - 2026-03-21
### Features ### Features
+2 -2
View File
@@ -49,8 +49,8 @@
| Mirrors | URL | | Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- | | ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.6.3/dist/iife/page-agent.demo.js | | Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.6.3/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
### NPM 安装 ### NPM 安装
+1 -1
View File
@@ -10,7 +10,7 @@ For contribution rules and expectations, see [../CONTRIBUTING.md](../CONTRIBUTIN
1. **Prerequisites** 1. **Prerequisites**
- `macOS` / `Linux` / `WSL` - `macOS` / `Linux` / `WSL`
- `node.js >= 20` with `npm >= 10` - `node.js >= 24` with `npm >= 11`
- An editor that supports `ts/eslint/prettier` - An editor that supports `ts/eslint/prettier`
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI. - Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
+6 -29
View File
@@ -1,8 +1,5 @@
import eslintReact from '@eslint-react/eslint-plugin'
import js from '@eslint/js' import js from '@eslint/js'
import reactDom from 'eslint-plugin-react-dom'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import reactX from 'eslint-plugin-react-x'
import { defineConfig, globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals' import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
@@ -15,44 +12,24 @@ export default defineConfig([
'**/.wxt', '**/.wxt',
'**/.output', '**/.output',
]), ]),
{
plugins: {
'react-hooks': reactHooks,
},
rules: reactHooks.configs.recommended.rules,
},
{ {
files: ['**/*.{ts,tsx}'], files: ['**/*.{ts,tsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
// reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked, ...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked, ...tseslint.configs.stylisticTypeChecked,
eslintReact.configs['recommended-typescript'],
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
], ],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
// project: ['./tsconfig.json'],
// project: ['./packages/*/tsconfig.json'],
// tsconfigRootDir: import.meta.dirname,
projectService: true, projectService: true,
}, },
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
}, },
rules: { rules: {
// Add any additional rules here
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-member-access': 'off',
@@ -72,14 +49,14 @@ export default defineConfig([
'@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/restrict-plus-operands': 'off', '@typescript-eslint/restrict-plus-operands': 'off',
'react-dom/no-missing-button-type': 'off',
'react-x/no-nested-component-definitions': 'off',
'@typescript-eslint/prefer-optional-chain': 'off', '@typescript-eslint/prefer-optional-chain': 'off',
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off', '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off', '@typescript-eslint/no-unnecessary-type-parameters': 'off',
// 'require-await': 'off',
'@typescript-eslint/require-await': 'off', '@typescript-eslint/require-await': 'off',
'@eslint-react/dom-no-missing-button-type': 'off',
'@eslint-react/no-nested-component-definitions': 'off',
'@eslint-react/no-array-index-key': 'off',
'@eslint-react/dom-no-dangerously-set-innerhtml': 'off',
}, },
}, },
]) ])
+2352 -1526
View File
File diff suppressed because it is too large Load Diff
+21 -22
View File
@@ -1,7 +1,7 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
@@ -22,7 +22,7 @@
}, },
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"engines": { "engines": {
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^22.13.0 || >=24"
}, },
"scripts": { "scripts": {
"start": "npm run dev --workspace=@page-agent/website", "start": "npm run dev --workspace=@page-agent/website",
@@ -33,41 +33,40 @@
"build:website": "npm run build:website --workspace=@page-agent/website", "build:website": "npm run build:website --workspace=@page-agent/website",
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext", "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",
"postpublish": "npm run postpublish --workspaces --if-present",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
"lint": "eslint .", "lint": "eslint .",
"cleanup": "rm -rf packages/*/dist", "cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
"prepare": "husky" "prepare": "husky || true"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.5.0", "@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0", "@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^9.39.2", "@eslint-react/eslint-plugin": "^4.2.3",
"@microsoft/api-extractor": "^7.57.7", "@eslint/js": "^10.0.1",
"@microsoft/api-extractor": "^7.58.2",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.5.0", "@types/node": "^25.6.0",
"@vitejs/plugin-react-swc": "^4.3.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.4.2",
"eslint": "^9.39.2", "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8", "globals": "^17.5.0",
"eslint-plugin-react-dom": "^2.13.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"eslint-plugin-react-x": "^2.13.0",
"globals": "^17.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.4.0", "lint-staged": "^16.4.0",
"prettier": "^3.8.0", "prettier": "^3.8.2",
"typescript": "^5.9.3", "typescript": "^6.0.2",
"typescript-eslint": "^8.57.2", "typescript-eslint": "^8.58.2",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.3.1", "vite": "^7.3.2",
"vite-plugin-css-injected-by-js": "^4.0.1", "vite-bundle-analyzer": "^1.3.7",
"vite-bundle-analyzer": "^1.3.6" "vite-plugin-css-injected-by-js": "^4.0.1"
}, },
"overrides": { "overrides": {
"typescript": "^5.9.3" "typescript": "^6.0.2",
"@vitejs/plugin-react": "^5.2.0"
}, },
"lint-staged": { "lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [ "*.{js,ts,cjs,cts,mjs,mts}": [
+22 -12
View File
@@ -1,16 +1,26 @@
{ {
"name": "@page-agent/core", "name": "@page-agent/core",
"private": false, "private": false,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"main": "./dist/esm/page-agent-core.js", "main": "./src/PageAgentCore.ts",
"module": "./dist/esm/page-agent-core.js", "types": "./src/PageAgentCore.ts",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/esm/PageAgentCore.d.ts", "types": "./src/PageAgentCore.ts",
"import": "./dist/esm/page-agent-core.js", "default": "./src/PageAgentCore.ts"
"default": "./dist/esm/page-agent-core.js" }
},
"publishConfig": {
"main": "./dist/esm/page-agent-core.js",
"module": "./dist/esm/page-agent-core.js",
"types": "./dist/esm/PageAgentCore.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgentCore.d.ts",
"import": "./dist/esm/page-agent-core.js",
"default": "./dist/esm/page-agent-core.js"
}
} }
}, },
"files": [ "files": [
@@ -39,18 +49,18 @@
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"", "dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node ../../scripts/post-publish.js"
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
"@page-agent/llms": "1.6.3", "@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.6.3" "@page-agent/page-controller": "1.8.0"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"zod": "^4.3.5" "zod": "^4.3.6"
} }
} }
+3 -2
View File
@@ -131,7 +131,8 @@ tools.set(
tools.set( tools.set(
'scroll', 'scroll',
tool({ tool({
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).', description:
'Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
inputSchema: z.object({ inputSchema: z.object({
down: z.boolean().default(true), down: z.boolean().default(true),
num_pages: z.number().min(0).max(10).optional().default(0.1), num_pages: z.number().min(0).max(10).optional().default(0.1),
@@ -155,7 +156,7 @@ tools.set(
'scroll_horizontally', 'scroll_horizontally',
tool({ tool({
description: description:
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.', 'Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
inputSchema: z.object({ inputSchema: z.object({
right: z.boolean().default(true), right: z.boolean().default(true),
pixels: z.number().int().min(0), pixels: z.number().int().min(0),
+2 -2
View File
@@ -19,7 +19,7 @@ const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
* - etc. * - etc.
*/ */
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any { export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
let resolvedArguments = null as any let resolvedArguments: any
const choice = (response as { choices?: Choice[] }).choices?.[0] const choice = (response as { choices?: Choice[] }).choices?.[0]
if (!choice) throw new Error('No choices in response') if (!choice) throw new Error('No choices in response')
@@ -93,7 +93,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
// fix incomplete formats // fix incomplete formats
if (!resolvedArguments.action) { if (!resolvedArguments.action) {
log(`#5: fixing tool_call`) log(`#5: fixing tool_call`)
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } } resolvedArguments.action = { wait: { seconds: 1 } }
} }
// pack back to standard format // pack back to standard format
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+3 -17
View File
@@ -1,22 +1,8 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
}
}, },
"include": ["**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"], "exclude": ["dist", "node_modules"]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" }
]
} }
+9 -4
View File
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }), dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }), cssInjectedByJsPlugin({ relativeCSSInjection: true }),
], ],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/PageAgentCore.ts'), entry: resolve(__dirname, 'src/PageAgentCore.ts'),
+7
View File
@@ -118,6 +118,10 @@ export interface ExecuteConfig {
model: string model: string
apiKey?: string apiKey?: string
// Global system-level instructions for the agent.
// Equivalent to AgentConfig.instructions.system.
systemInstruction?: 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
@@ -212,6 +216,9 @@ interface ExecuteConfig {
baseURL: string baseURL: string
model: string model: string
apiKey?: string apiKey?: string
systemInstruction?: string
includeInitialTab?: boolean includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean experimentalIncludeAllTabs?: boolean
onStatusChange?: (status: AgentStatus) => void onStatusChange?: (status: AgentStatus) => void
+13 -12
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/ext", "name": "@page-agent/ext",
"private": true, "private": true,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
@@ -16,31 +16,32 @@
"@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",
"@types/chrome": "^0.1.38", "@tailwindcss/vite": "^4.2.2",
"@types/chrome": "^0.1.40",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.2.2", "@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": "^1.0.1", "lucide-react": "^1.8.0",
"motion": "^12.38.0", "motion": "^12.38.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.4", "react": "^19.2.5",
"react-dom": "^19.2.4", "react-dom": "^19.2.5",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.13.0", "simple-icons": "^16.16.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.2.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"wxt": "^0.20.20" "wxt": "^0.20.20"
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.6.3", "@page-agent/core": "1.8.0",
"@page-agent/llms": "1.6.3", "@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.6.3", "@page-agent/page-controller": "1.8.0",
"@page-agent/ui": "1.6.3", "@page-agent/ui": "1.8.0",
"ai-motion": "^0.4.8", "ai-motion": "^0.4.8",
"chalk": "^5.6.2" "chalk": "^5.6.2"
}, },
@@ -19,7 +19,10 @@ export function initPageController() {
function getPC(): PageController { function getPC(): PageController {
if (!pageController) { if (!pageController) {
pageController = new PageController({ enableMask: false, viewportExpansion: 400 }) pageController = new PageController({
enableMask: false,
viewportExpansion: 400,
})
} }
return pageController return pageController
} }
@@ -56,7 +56,7 @@ export class RemotePageController {
} }
async getBrowserState(): Promise<BrowserState> { async getBrowserState(): Promise<BrowserState> {
let browserState = {} as BrowserState let browserState: BrowserState
debug('getBrowserState', this.currentTabId) debug('getBrowserState', this.currentTabId)
const currentUrl = await this.getCurrentUrl() const currentUrl = await this.getCurrentUrl()
@@ -24,7 +24,7 @@ export class TabsController {
currentTabId: number | null = null currentTabId: number | null = null
private disposed = false private disposed = false
private port: chrome.runtime.Port | null = null private port?: chrome.runtime.Port
private portRetries = 0 private portRetries = 0
private windowId: number | null = null private windowId: number | null = null
@@ -44,7 +44,7 @@ export class TabsController {
this.currentTabId = null this.currentTabId = null
this.disposed = false this.disposed = false
this.port = null this.port = undefined
this.portRetries = 0 this.portRetries = 0
this.windowId = null this.windowId = null
@@ -338,10 +338,10 @@ export class TabsController {
}) })
this.port.onDisconnect.addListener(() => { this.port.onDisconnect.addListener(() => {
this.port = null this.port = undefined
if (this.disposed) return if (this.disposed) return
if (this.portRetries >= 7) { if (this.portRetries >= 7) {
console.error(PREFIX, 'tab events port failed after 3 retries, giving up') console.error(PREFIX, 'tab events port failed after 7 retries, giving up')
return return
} }
debug('port disconnected, reconnecting...') debug('port disconnected, reconnecting...')
@@ -354,7 +354,7 @@ export class TabsController {
debug('dispose') debug('dispose')
this.disposed = true this.disposed = true
this.port?.disconnect() this.port?.disconnect()
this.port = null this.port = undefined
} }
} }
@@ -49,7 +49,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [showToken, setShowToken] = useState(false) const [showToken, setShowToken] = useState(false)
const [showApiKey, setShowApiKey] = useState(false) const [showApiKey, setShowApiKey] = useState(false)
useEffect(() => { const [prevConfig, setPrevConfig] = useState(config)
if (prevConfig !== config) {
setPrevConfig(config)
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) setApiKey(config?.apiKey)
@@ -59,7 +61,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false) setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false) setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false) setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config]) }
// Poll for user auth token every second until found // Poll for user auth token every second until found
useEffect(() => { useEffect(() => {
@@ -71,7 +71,6 @@ export function HistoryDetail({
{/* Events (read-only) */} {/* Events (read-only) */}
<div className="flex-1 overflow-y-auto p-3 space-y-2"> <div className="flex-1 overflow-y-auto p-3 space-y-2">
{session.history.map((event, index) => ( {session.history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} /> <EventCard key={index} event={event} />
))} ))}
</div> </div>
@@ -34,7 +34,6 @@ export function HistoryList({
}, []) }, [])
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
load() load()
}, [load]) }, [load])
+1 -1
View File
@@ -54,7 +54,7 @@ function ResultCard({
Result: {success ? 'Success' : 'Failed'} Result: {success ? 'Success' : 'Failed'}
</span> </span>
</div> </div>
<p className="text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap">{text}</p> <p className="text-[12px] text-foreground pl-5 whitespace-pre-wrap">{text}</p>
{children} {children}
</div> </div>
) )
@@ -46,6 +46,8 @@ async function exposeAgentToPage() {
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
window.addEventListener('message', async (e) => { window.addEventListener('message', async (e) => {
if (e.source !== window) return
const data = e.data const data = e.data
if (typeof data !== 'object' || data === null) return if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
@@ -70,11 +72,15 @@ async function exposeAgentToPage() {
try { try {
const { task, config } = payload const { task, config } = payload
const { systemInstruction, ...agentConfig } = config
// Dispose old instance before creating new one // Dispose old instance before creating new one
multiPageAgent?.dispose() multiPageAgent?.dispose()
multiPageAgent = new MultiPageAgent(config) multiPageAgent = new MultiPageAgent({
...agentConfig,
instructions: systemInstruction ? { system: systemInstruction } : undefined,
})
// events // events
@@ -133,7 +133,6 @@ export default function App() {
)} )}
{history.map((event, index) => ( {history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} /> <EventCard key={index} event={event} />
))} ))}
@@ -206,9 +206,9 @@ export function useHubWs(
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected')) const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
const hubWsRef = useRef<HubWs | null>(null) const hubWsRef = useRef<HubWs | null>(null)
const latest = useRef({ execute, stop, configure, config }) const latestRef = useRef({ execute, stop, configure, config })
useEffect(() => { useEffect(() => {
latest.current = { execute, stop, configure, config } latestRef.current = { execute, stop, configure, config }
}) })
useEffect(() => { useEffect(() => {
@@ -218,14 +218,14 @@ export function useHubWs(
Number(wsPort), Number(wsPort),
{ {
onExecute: async (task, incomingConfig) => { onExecute: async (task, incomingConfig) => {
const { execute, configure, config } = latest.current const { execute, configure, config } = latestRef.current
if (incomingConfig) { if (incomingConfig) {
await configure({ ...config, ...incomingConfig } as ExtConfig) await configure({ ...config, ...incomingConfig } as ExtConfig)
} }
const result = await execute(task) const result = await execute(task)
return { success: result.success, data: result.data } return { success: result.success, data: result.data }
}, },
onStop: () => latest.current.stop(), onStop: () => latestRef.current.stop(),
}, },
setWsState setWsState
) )
@@ -7,6 +7,12 @@ export interface ExecuteConfig {
model: string model: string
apiKey?: string apiKey?: string
/**
* Global system-level instructions for the agent.
* Equivalent to `AgentConfig.instructions.system`.
*/
systemInstruction?: 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.
* @default true * @default true
@@ -39,6 +45,8 @@ export default defineUnlistedScript(() => {
const promise = new Promise<ExecutionResult>((resolve, reject) => { const promise = new Promise<ExecutionResult>((resolve, reject) => {
function handleMessage(e: MessageEvent) { function handleMessage(e: MessageEvent) {
if (e.source !== window) return
const data = e.data const data = e.data
if (typeof data !== 'object' || data === null) return if (typeof data !== 'object' || data === null) return
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
@@ -89,6 +97,7 @@ export default defineUnlistedScript(() => {
baseURL: config.baseURL, baseURL: config.baseURL,
model: config.model, model: config.model,
apiKey: config.apiKey, apiKey: config.apiKey,
systemInstruction: config.systemInstruction,
includeInitialTab: config.includeInitialTab, includeInitialTab: config.includeInitialTab,
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs, experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
}, },
@@ -178,7 +178,6 @@ export default function App() {
{showEmptyState && <EmptyState />} {showEmptyState && <EmptyState />}
{history.map((event, index) => ( {history.map((event, index) => (
// eslint-disable-next-line react-x/no-array-index-key
<EventCard key={index} event={event} /> <EventCard key={index} event={event} />
))} ))}
+3 -18
View File
@@ -3,26 +3,11 @@
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"noEmit": false,
"allowImportingTsExtensions": false,
"strictNullChecks": true, "strictNullChecks": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".", "types": ["node", "chrome"],
"paths": { "paths": {
// Self root "@/*": ["./src/*"]
"@/*": ["src/*"],
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
} }
}, }
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
} }
+20 -10
View File
@@ -1,15 +1,25 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-llms.js", "main": "./src/index.ts",
"module": "./dist/lib/page-agent-llms.js", "types": "./src/index.ts",
"types": "./dist/lib/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/lib/index.d.ts", "types": "./src/index.ts",
"import": "./dist/lib/page-agent-llms.js", "default": "./src/index.ts"
"default": "./dist/lib/page-agent-llms.js" }
},
"publishConfig": {
"main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-llms.js",
"default": "./dist/lib/page-agent-llms.js"
}
} }
}, },
"files": [ "files": [
@@ -33,8 +43,8 @@
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node ../../scripts/post-publish.js"
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2" "chalk": "^5.6.2"
@@ -43,6 +53,6 @@
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"zod": "^4.3.5" "zod": "^4.3.6"
} }
} }
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+1 -5
View File
@@ -1,11 +1,7 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
}, },
"include": ["**/*.ts"], "include": ["**/*.ts"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
+11 -4
View File
@@ -11,11 +11,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/llms`))
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })], plugins: [
dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/index.ts'), entry: resolve(__dirname, 'src/index.ts'),
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/mcp", "name": "@page-agent/mcp",
"private": false, "private": false,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"bin": { "bin": {
"page-agent-mcp": "src/index.js" "page-agent-mcp": "src/index.js"
@@ -28,8 +28,8 @@
"node": ">=20" "node": ">=20"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1", "@modelcontextprotocol/sdk": "^1.29.0",
"ws": "^8.20.0", "ws": "^8.20.0",
"zod": "^4.3.5" "zod": "^4.3.6"
} }
} }
+3 -2
View File
@@ -6,6 +6,7 @@ import { WebSocketServer } from 'ws'
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj' const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}` const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
const LOOPBACK_HOST = 'localhost'
const launcherTemplate = readFileSync( const launcherTemplate = readFileSync(
fileURLToPath(new URL('./launcher.html', import.meta.url)), fileURLToPath(new URL('./launcher.html', import.meta.url)),
@@ -60,8 +61,8 @@ export class HubBridge {
reject(err) reject(err)
} }
}) })
this.#httpServer.listen(this.port, () => { this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
console.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`) console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
resolve() resolve()
}) })
}) })
+24 -14
View File
@@ -1,16 +1,26 @@
{ {
"name": "page-agent", "name": "page-agent",
"private": false, "private": false,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"main": "./dist/esm/page-agent.js", "main": "./src/PageAgent.ts",
"module": "./dist/esm/page-agent.js", "types": "./src/PageAgent.ts",
"types": "./dist/esm/PageAgent.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/esm/PageAgent.d.ts", "types": "./src/PageAgent.ts",
"import": "./dist/esm/page-agent.js", "default": "./src/PageAgent.ts"
"default": "./dist/esm/page-agent.js" }
},
"publishConfig": {
"main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js",
"types": "./dist/esm/PageAgent.d.ts",
"exports": {
".": {
"types": "./dist/esm/PageAgent.d.ts",
"import": "./dist/esm/page-agent.js",
"default": "./dist/esm/page-agent.js"
}
} }
}, },
"files": [ "files": [
@@ -40,20 +50,20 @@
"build": "vite build && npm run build:demo", "build": "vite build && npm run build:demo",
"build:demo": "vite build --config vite.iife.config.js", "build:demo": "vite build --config vite.iife.config.js",
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"", "dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node ../../scripts/post-publish.js"
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.6.3", "@page-agent/core": "1.8.0",
"@page-agent/llms": "1.6.3", "@page-agent/llms": "1.8.0",
"@page-agent/page-controller": "1.6.3", "@page-agent/page-controller": "1.8.0",
"@page-agent/ui": "1.6.3", "@page-agent/ui": "1.8.0",
"chalk": "^5.6.2" "chalk": "^5.6.2"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"zod": "^4.3.5" "zod": "^4.3.6"
} }
} }
+2 -1
View File
@@ -17,9 +17,10 @@ const DEMO_MODEL = 'qwen3.5-plus'
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run' const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
const DEMO_API_KEY = 'NA' const DEMO_API_KEY = 'NA'
const currentScript = document.currentScript as HTMLScriptElement | null
// in case document.x is not ready yet // in case document.x is not ready yet
setTimeout(() => { setTimeout(() => {
const currentScript = document.currentScript as HTMLScriptElement | null
let config: PageAgentConfig let config: PageAgentConfig
if (currentScript) { if (currentScript) {
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+3 -21
View File
@@ -1,26 +1,8 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": {
//
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"]
}
}, },
"include": ["**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"], "exclude": ["dist", "node_modules"]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" }
]
} }
+9 -4
View File
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }), dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }), cssInjectedByJsPlugin({ relativeCSSInjection: true }),
], ],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/PageAgent.ts'), entry: resolve(__dirname, 'src/PageAgent.ts'),
-11
View File
@@ -21,17 +21,6 @@ export default defineConfig(() => ({
// analyzer() // analyzer()
], ],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
resolve: {
alias: {
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
},
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/demo.ts'), entry: resolve(__dirname, 'src/demo.ts'),
+19 -9
View File
@@ -1,15 +1,25 @@
{ {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-controller.js", "main": "./src/PageController.ts",
"module": "./dist/lib/page-controller.js", "types": "./src/PageController.ts",
"types": "./dist/lib/PageController.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/lib/PageController.d.ts", "types": "./src/PageController.ts",
"import": "./dist/lib/page-controller.js", "default": "./src/PageController.ts"
"default": "./dist/lib/page-controller.js" }
},
"publishConfig": {
"main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js",
"types": "./dist/lib/PageController.d.ts",
"exports": {
".": {
"types": "./dist/lib/PageController.d.ts",
"import": "./dist/lib/page-controller.js",
"default": "./dist/lib/page-controller.js"
}
} }
}, },
"files": [ "files": [
@@ -32,8 +42,8 @@
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node ../../scripts/post-publish.js"
}, },
"dependencies": { "dependencies": {
"ai-motion": "^0.4.8" "ai-motion": "^0.4.8"
+10 -5
View File
@@ -185,7 +185,7 @@ export class PageController extends EventTarget {
const blacklist = [ const blacklist = [
...(this.config.interactiveBlacklist || []), ...(this.config.interactiveBlacklist || []),
...document.querySelectorAll('[data-page-agent-not-interactive]').values(), ...Array.from(document.querySelectorAll('[data-page-agent-not-interactive]')),
] ]
this.flatTree = dom.getFlatTree({ this.flatTree = dom.getFlatTree({
@@ -193,7 +193,11 @@ export class PageController extends EventTarget {
interactiveBlacklist: blacklist, interactiveBlacklist: blacklist,
}) })
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes) this.simplifiedHTML = dom.flatTreeToString(
this.flatTree,
this.config.includeAttributes,
this.config.keepSemanticTags
)
this.selectorMap.clear() this.selectorMap.clear()
this.selectorMap = dom.getSelectorMap(this.flatTree) this.selectorMap = dom.getSelectorMap(this.flatTree)
@@ -218,6 +222,7 @@ export class PageController extends EventTarget {
* Clean up all element highlights * Clean up all element highlights
*/ */
async cleanUpHighlights(): Promise<void> { async cleanUpHighlights(): Promise<void> {
console.log('[PageController] cleanUpHighlights')
dom.cleanUpHighlights() dom.cleanUpHighlights()
} }
@@ -321,11 +326,11 @@ export class PageController extends EventTarget {
this.assertIndexed() this.assertIndexed()
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1)
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollVertically(down, scrollAmount, element) const message = await scrollVertically(scrollAmount, element)
return { return {
success: true, success: true,
@@ -356,7 +361,7 @@ export class PageController extends EventTarget {
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
const message = await scrollHorizontally(right, scrollAmount, element) const message = await scrollHorizontally(scrollAmount, element)
return { return {
success: true, success: true,
+100 -52
View File
@@ -4,6 +4,9 @@
*/ */
import type { InteractiveElementDomNode } from './dom/dom_tree/type' import type { InteractiveElementDomNode } from './dom/dom_tree/type'
import { import {
clickPointer,
disablePassThrough,
enablePassThrough,
getNativeValueSetter, getNativeValueSetter,
isHTMLElement, isHTMLElement,
isInputElement, isInputElement,
@@ -42,19 +45,20 @@ let lastClickedElement: HTMLElement | null = null
function blurLastClickedElement() { function blurLastClickedElement() {
if (lastClickedElement) { if (lastClickedElement) {
lastClickedElement.dispatchEvent(new PointerEvent('pointerout', { bubbles: true }))
lastClickedElement.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }))
lastClickedElement.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
lastClickedElement.blur() lastClickedElement.blur()
lastClickedElement.dispatchEvent(
new MouseEvent('mouseout', { bubbles: true, cancelable: true })
)
lastClickedElement.dispatchEvent(
new MouseEvent('mouseleave', { bubbles: false, cancelable: true })
)
lastClickedElement = null lastClickedElement = null
} }
} }
/** /**
* Simulate a click on the element * Simulate a full click following W3C Pointer Events + UI Events spec order:
* pointerover/enter → mouseover/enter → pointerdown → mousedown → [focus] →
* pointerup → mouseup → click
*
* @private Internal method, subject to change at any time. * @private Internal method, subject to change at any time.
*/ */
export async function clickElement(element: HTMLElement) { export async function clickElement(element: HTMLElement) {
@@ -63,32 +67,62 @@ export async function clickElement(element: HTMLElement) {
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 const frame = element.ownerDocument.defaultView?.frameElement
if (frame) await scrollIntoViewIfNeeded(frame) if (frame) await scrollIntoViewIfNeeded(frame)
await movePointerToElement(element) const rect = element.getBoundingClientRect()
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer')) const x = rect.left + rect.width / 2
const y = rect.top + rect.height / 2
await movePointerToElement(element, x, y)
await clickPointer()
await waitFor(0.1) await waitFor(0.1)
// hover it // Hit-test to find the deepest element at click coordinates, matching
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true })) // real browser behavior where events target the innermost element.
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true })) // @note This may hit a element in the blacklist
// TODO: This is a temporary workaround. Should have been handled during dom extraction.
const doc = element.ownerDocument
await enablePassThrough()
const hitTarget = doc.elementFromPoint(x, y)
await disablePassThrough()
const target =
hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element
// dispatch a sequence of events to ensure all listeners are triggered const pointerOpts = {
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
pointerType: 'mouse',
}
const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }
// focus it to ensure it gets the click event // Hover — pointer events first, then mouse events (spec order)
element.focus() target.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
target.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
target.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
target.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })) // Press
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) target.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
target.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
// dispatch a click event // Focus is not part of the standard pointer/mouse event sequence
// element.click() // "undefined and varies between user agents".
// We focus the original element (nearest focusable ancestor), not the hit-test target, matching browser behavior.
element.focus({ preventScroll: true })
await waitFor(0.2) // Wait to ensure click event processing completes // Release
target.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
target.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
// Click — activation behavior (navigation, form submit, etc.) triggers
// via bubbling from target up to the interactive ancestor.
target.click()
await waitFor(0.2)
} }
/** /**
@@ -240,14 +274,7 @@ export async function scrollIntoViewIfNeeded(element: Element) {
} }
} }
/** export async function scrollVertically(scroll_amount: number, element?: HTMLElement | null) {
* @private Internal method, subject to change at any time.
*/
export async function scrollVertically(
down: boolean,
scroll_amount: number,
element?: HTMLElement | null
) {
// Element-specific scrolling if element is provided // Element-specific scrolling if element is provided
if (element) { if (element) {
const targetElement = element const targetElement = element
@@ -260,7 +287,10 @@ export async function scrollVertically(
while (currentElement && attempts < 10) { while (currentElement && attempts < 10) {
const computedStyle = window.getComputedStyle(currentElement) const computedStyle = window.getComputedStyle(currentElement)
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY) const hasScrollableY =
/(auto|scroll|overlay)/.test(computedStyle.overflowY) ||
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
if (hasScrollableY && canScrollVertically) { if (hasScrollableY && canScrollVertically) {
@@ -312,9 +342,20 @@ export async function scrollVertically(
el.scrollHeight > el.clientHeight && el.scrollHeight > el.clientHeight &&
bigEnough(el) bigEnough(el)
// @deprecated Heuristic container search.
// Unreliable in multi-panel layouts. Should guide LLMs to use indexed scroll for consistency.
// TODO: remove this fallback
// try to find the nearest scrollable container
// document.activeElement is usually body.
// After a successful element.focus(), activeElement become the nearest focusable parent
let el: HTMLElement | null = document.activeElement as HTMLElement | null let el: HTMLElement | null = document.activeElement as HTMLElement | null
while (el && !canScroll(el) && el !== document.body) el = el.parentElement while (el && !canScroll(el) && el !== document.body) el = el.parentElement
// Something is wrong if it falls back to global '*' search
// TODO: Return error message instead of global '*' search
el = canScroll(el) el = canScroll(el)
? el ? el
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) || : Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
@@ -345,6 +386,10 @@ export async function scrollVertically(
return `✅ Scrolled page by ${scrolled}px.` return `✅ Scrolled page by ${scrolled}px.`
} else { } else {
// Container scroll // Container scroll
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
console.log(`[PageController] ${warningMsg}`)
const scrollBefore = el!.scrollTop const scrollBefore = el!.scrollTop
const scrollMax = el!.scrollHeight - el!.clientHeight const scrollMax = el!.scrollHeight - el!.clientHeight
@@ -356,29 +401,22 @@ export async function scrollVertically(
if (Math.abs(scrolled) < 1) { if (Math.abs(scrolled) < 1) {
return dy > 0 return dy > 0
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.` ? `⚠️ ${warningMsg} Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.` : `⚠️ ${warningMsg} Already at the top of container (${el!.tagName}), cannot scroll up further.`
} }
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1 const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
const reachedTop = dy < 0 && scrollAfter <= 1 const reachedTop = dy < 0 && scrollAfter <= 1
if (reachedBottom) if (reachedBottom)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.` return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
if (reachedTop) if (reachedTop)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.` return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.` return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px.`
} }
} }
/** export async function scrollHorizontally(scroll_amount: number, element?: HTMLElement | null) {
* @private Internal method, subject to change at any time.
*/
export async function scrollHorizontally(
right: boolean,
scroll_amount: number,
element?: HTMLElement | null
) {
// Element-specific scrolling if element is provided // Element-specific scrolling if element is provided
if (element) { if (element) {
const targetElement = element const targetElement = element
@@ -387,11 +425,14 @@ export async function scrollHorizontally(
let scrolledElement: HTMLElement | null = null let scrolledElement: HTMLElement | null = null
let scrollDelta = 0 let scrollDelta = 0
let attempts = 0 let attempts = 0
const dx = right ? scroll_amount : -scroll_amount const dx = scroll_amount
while (currentElement && attempts < 10) { while (currentElement && attempts < 10) {
const computedStyle = window.getComputedStyle(currentElement) const computedStyle = window.getComputedStyle(currentElement)
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX) const hasScrollableX =
/(auto|scroll|overlay)/.test(computedStyle.overflowX) ||
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
if (hasScrollableX && canScrollHorizontally) { if (hasScrollableX && canScrollHorizontally) {
@@ -435,7 +476,8 @@ export async function scrollHorizontally(
// Page-level scrolling (default or fallback) // Page-level scrolling (default or fallback)
const dx = right ? scroll_amount : -scroll_amount const dx = scroll_amount
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5 const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
const canScroll = (el: HTMLElement | null) => const canScroll = (el: HTMLElement | null) =>
el && el &&
@@ -443,6 +485,9 @@ export async function scrollHorizontally(
el.scrollWidth > el.clientWidth && el.scrollWidth > el.clientWidth &&
bigEnough(el) bigEnough(el)
// @deprecated Same heuristic container search as scrollVertically.
// TODO: Remove once LLMs reliably use indexed scrolling via data-scrollable.
let el: HTMLElement | null = document.activeElement as HTMLElement | null let el: HTMLElement | null = document.activeElement as HTMLElement | null
while (el && !canScroll(el) && el !== document.body) el = el.parentElement while (el && !canScroll(el) && el !== document.body) el = el.parentElement
@@ -477,6 +522,9 @@ export async function scrollHorizontally(
return `✅ Scrolled page horizontally by ${scrolled}px.` return `✅ Scrolled page horizontally by ${scrolled}px.`
} else { } else {
// Container scroll // Container scroll
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
console.log(`[PageController] ${warningMsg}`)
const scrollBefore = el!.scrollLeft const scrollBefore = el!.scrollLeft
const scrollMax = el!.scrollWidth - el!.clientWidth const scrollMax = el!.scrollWidth - el!.clientWidth
@@ -488,17 +536,17 @@ export async function scrollHorizontally(
if (Math.abs(scrolled) < 1) { if (Math.abs(scrolled) < 1) {
return dx > 0 return dx > 0
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.` ? `⚠️ ${warningMsg} Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.` : `⚠️ ${warningMsg} Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
} }
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1 const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
const reachedLeft = dx < 0 && scrollAfter <= 1 const reachedLeft = dx < 0 && scrollAfter <= 1
if (reachedRight) if (reachedRight)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.` return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
if (reachedLeft) if (reachedLeft)
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.` return ` ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.` return ` ${warningMsg} Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
} }
} }
+16
View File
@@ -0,0 +1,16 @@
import type { FlatDomTree } from './type'
interface DomTreeArgs {
doHighlightElements?: boolean
focusHighlightIndex?: number
viewportExpansion?: number
debugMode?: boolean
interactiveBlacklist?: Element[]
interactiveWhitelist?: Element[]
highlightOpacity?: number
highlightLabelOpacity?: number
}
declare const domTree: (args?: DomTreeArgs) => FlatDomTree
export default domTree
@@ -18,6 +18,7 @@
* @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. * @edit make sure attributes exist for interactive candidates.
* @edit fix "aria-*" attributes check
*/ */
export default ( export default (
@@ -502,11 +503,16 @@ export default (
const overflowX = style.overflowX const overflowX = style.overflowX
const overflowY = style.overflowY const overflowY = style.overflowY
// Check scrollable distances // scrollbar-width/scrollbar-gutter are only set on elements designed to scroll;
// their presence signals scroll intent even when overflow is hidden (e.g. overflow: auto on :hover)
const hasScrollbarSignal =
(style.scrollbarWidth && style.scrollbarWidth !== 'auto') ||
(style.scrollbarGutter && style.scrollbarGutter !== 'auto')
const scrollableX = overflowX === 'auto' || overflowX === 'scroll' const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
const scrollableY = overflowY === 'auto' || overflowY === 'scroll' const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
if (!scrollableX && !scrollableY) { if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
return null // Not scrollable in any direction return null // Not scrollable in any direction
} }
@@ -520,11 +526,11 @@ export default (
return null // Not scrollable return null // Not scrollable
} }
if (!scrollableY && scrollWidth < threshold) { if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
return null // Not scrollable horizontally return null // Not scrollable horizontally
} }
if (!scrollableX && scrollHeight < threshold) { if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
return null // Not scrollable vertically return null // Not scrollable vertically
} }
@@ -546,6 +552,8 @@ export default (
scrollData: scrollData, scrollData: scrollData,
}) })
console.log('scrollData!!!', scrollData)
return scrollData return scrollData
} }
@@ -1143,6 +1151,31 @@ export default (
* @param {HTMLElement} element - The element to check. * @param {HTMLElement} element - The element to check.
* @returns {boolean} Whether the element is an interactive candidate. * @returns {boolean} Whether the element is an interactive candidate.
*/ */
// @edit fix "aria-*" attributes check
const INTERACTIVE_ARIA_ATTRS = [
'aria-expanded',
'aria-checked',
'aria-selected',
'aria-pressed',
'aria-haspopup',
'aria-controls',
'aria-owns',
'aria-activedescendant',
'aria-valuenow',
'aria-valuetext',
'aria-valuemax',
'aria-valuemin',
'aria-autocomplete',
]
function hasInteractiveAria(el) {
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true
}
return false
}
function isInteractiveCandidate(element) { function isInteractiveCandidate(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
@@ -1167,7 +1200,7 @@ export default (
element.hasAttribute('onclick') || element.hasAttribute('onclick') ||
element.hasAttribute('role') || element.hasAttribute('role') ||
element.hasAttribute('tabindex') || element.hasAttribute('tabindex') ||
element.hasAttribute('aria-') || hasInteractiveAria(element) ||
element.hasAttribute('data-action') || element.hasAttribute('data-action') ||
element.getAttribute('contenteditable') === 'true' element.getAttribute('contenteditable') === 'true'
@@ -1185,8 +1218,9 @@ export default (
'details', 'details',
'label', 'label',
'option', 'option',
'li',
]) ])
const INTERACTIVE_ROLES = new Set([ const DISTINCT_INTERACTIVE_ROLES = new Set([
'button', 'button',
'link', 'link',
'menuitem', 'menuitem',
@@ -1202,6 +1236,9 @@ export default (
'searchbox', 'searchbox',
'textbox', 'textbox',
'listbox', 'listbox',
'listitem',
'treeitem',
'row',
'option', 'option',
'scrollbar', 'scrollbar',
]) ])
@@ -1278,7 +1315,7 @@ export default (
return true return true
} }
// Check interactive roles // Check interactive roles
if (role && INTERACTIVE_ROLES.has(role)) { if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
return true return true
} }
// Check contenteditable // Check contenteditable
@@ -1297,6 +1334,10 @@ export default (
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') { if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
return true return true
} }
// ARIA state attributes imply the element manages its own interaction state
if (hasInteractiveAria(element)) {
return true
}
// return false // return false
@@ -1352,6 +1393,12 @@ export default (
return true return true
} }
// Scrollable containers are always distinct — the LLM needs their index for targeted scrolling.
// Check extraData (already set by isScrollableElement in isInteractiveElement) to avoid redundant layout reads.
if (extraData.get(element)?.scrollable) {
return true
}
// Default to false: if it's interactive but doesn't match above, // Default to false: if it's interactive but doesn't match above,
// assume it triggers the same action as the parent. // assume it triggers the same action as the parent.
return false return false
+48 -3
View File
@@ -28,8 +28,27 @@ export interface DomConfig {
includeAttributes?: string[] includeAttributes?: string[]
highlightOpacity?: number highlightOpacity?: number
highlightLabelOpacity?: number highlightLabelOpacity?: number
/**
* Preserve semantic landmark tags in dehydrated output even if not interactive
* @note maybe confusing for LLM combining with page scrolling, use with caution
**/
keepSemanticTags?: boolean
} }
// TODO: corresponding roles
const SEMANTIC_TAGS = new Set([
'nav',
'menu',
// 'main',
'header',
'footer',
'aside',
// 'article',
// 'form',
'dialog',
])
/** /**
* 用于检测可交互元素是否是新出现的。 * 用于检测可交互元素是否是新出现的。
*/ */
@@ -171,7 +190,11 @@ interface TreeNode {
* *
* @todo 数据脱敏过滤器 * @todo 数据脱敏过滤器
*/ */
export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string { export function flatTreeToString(
flatTree: FlatDomTree,
includeAttributes: string[] = [],
keepSemanticTags = false
): string {
const DEFAULT_INCLUDE_ATTRIBUTES = [ const DEFAULT_INCLUDE_ATTRIBUTES = [
'title', 'title',
'type', 'type',
@@ -203,7 +226,7 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
'contenteditable', 'contenteditable',
] ]
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES] const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES]
// Helper function to cap text length // Helper function to cap text length
const capTextLength = (text: string, maxLength: number): string => { const capTextLength = (text: string, maxLength: number): string => {
@@ -294,6 +317,8 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
const depthStr = '\t'.repeat(depth) const depthStr = '\t'.repeat(depth)
if (node.type === 'element') { if (node.type === 'element') {
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName)
// Add element with highlight_index // Add element with highlight_index
if (node.highlightIndex !== undefined) { if (node.highlightIndex !== undefined) {
nextDepth += 1 nextDepth += 1
@@ -391,10 +416,30 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
result.push(line) result.push(line)
} }
// Process children regardless // special treatment for semantic tags
// even if they are not interactive, we can keep them for clear context
const emitSemantic = isSemantic && node.highlightIndex === undefined
// to check if this tag is empty
const mark = emitSemantic ? result.length : -1
if (emitSemantic) {
result.push(`${depthStr}<${node.tagName}>`)
nextDepth += 1
}
for (const child of node.children) { for (const child of node.children) {
processNode(child, nextDepth, result) processNode(child, nextDepth, result)
} }
if (emitSemantic) {
// empty tag should be removed
if (result.length === mark + 1) {
result.pop()
} else {
result.push(`${depthStr}</${node.tagName}>`)
}
}
} else if (node.type === 'text') { } else if (node.type === 'text') {
// Add text only if it doesn't have a highlighted parent // Add text only if it doesn't have a highlighted parent
if (hasParentWithHighlightIndex(node)) { if (hasParentWithHighlightIndex(node)) {
@@ -5,11 +5,13 @@ import { isPageDark } from './checkDarkMode'
import styles from './SimulatorMask.module.css' import styles from './SimulatorMask.module.css'
import cursorStyles from './cursor.module.css' import cursorStyles from './cursor.module.css'
export class SimulatorMask { export class SimulatorMask extends EventTarget {
shown: boolean = false shown: boolean = false
wrapper = document.createElement('div') wrapper = document.createElement('div')
motion: Motion | null = null motion: Motion | null = null
#disposed = false
#cursor = document.createElement('div') #cursor = document.createElement('div')
#currentCursorX = 0 #currentCursorX = 0
@@ -19,6 +21,8 @@ export class SimulatorMask {
#targetCursorY = 0 #targetCursorY = 0
constructor() { constructor() {
super()
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')
@@ -74,13 +78,34 @@ export class SimulatorMask {
this.#moveCursorToTarget() this.#moveCursorToTarget()
window.addEventListener('PageAgent::MovePointerTo', (event: Event) => { // global events
// @note Mask should be isolated from the rest of the code.
// Global events are easier to manage and cleanup.
const movePointerToListener = (event: Event) => {
const { x, y } = (event as CustomEvent).detail const { x, y } = (event as CustomEvent).detail
this.setCursorPosition(x, y) this.setCursorPosition(x, y)
}) }
const clickPointerListener = () => {
window.addEventListener('PageAgent::ClickPointer', (event: Event) => {
this.triggerClickAnimation() this.triggerClickAnimation()
}
const enablePassThroughListener = () => {
this.wrapper.style.pointerEvents = 'none'
}
const disablePassThroughListener = () => {
this.wrapper.style.pointerEvents = 'auto'
}
window.addEventListener('PageAgent::MovePointerTo', movePointerToListener)
window.addEventListener('PageAgent::ClickPointer', clickPointerListener)
window.addEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
window.addEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
this.addEventListener('dispose', () => {
window.removeEventListener('PageAgent::MovePointerTo', movePointerToListener)
window.removeEventListener('PageAgent::ClickPointer', clickPointerListener)
window.removeEventListener('PageAgent::EnablePassThrough', enablePassThroughListener)
window.removeEventListener('PageAgent::DisablePassThrough', disablePassThroughListener)
}) })
} }
@@ -106,6 +131,8 @@ export class SimulatorMask {
} }
#moveCursorToTarget() { #moveCursorToTarget() {
if (this.#disposed) return
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2 const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2 const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2
@@ -133,11 +160,15 @@ export class SimulatorMask {
} }
setCursorPosition(x: number, y: number) { setCursorPosition(x: number, y: number) {
if (this.#disposed) return
this.#targetCursorX = x this.#targetCursorX = x
this.#targetCursorY = y this.#targetCursorY = y
} }
triggerClickAnimation() { triggerClickAnimation() {
if (this.#disposed) return
this.#cursor.classList.remove(cursorStyles.clicking) this.#cursor.classList.remove(cursorStyles.clicking)
// Force reflow to restart animation // Force reflow to restart animation
void this.#cursor.offsetHeight void this.#cursor.offsetHeight
@@ -145,7 +176,7 @@ export class SimulatorMask {
} }
show() { show() {
if (this.shown) return if (this.shown || this.#disposed) return
this.shown = true this.shown = true
this.motion?.start() this.motion?.start()
@@ -163,7 +194,7 @@ export class SimulatorMask {
} }
hide() { hide() {
if (!this.shown) return if (!this.shown || this.#disposed) return
this.shown = false this.shown = false
this.motion?.fadeOut() this.motion?.fadeOut()
@@ -177,7 +208,10 @@ export class SimulatorMask {
} }
dispose() { dispose() {
this.#disposed = true
console.log('dispose SimulatorMask')
this.motion?.dispose() this.motion?.dispose()
this.wrapper.remove() this.wrapper.remove()
this.dispatchEvent(new Event('dispose'))
} }
} }
+24 -6
View File
@@ -48,15 +48,33 @@ export async function waitFor(seconds: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000)) await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
} }
// ======= dom utils ======= // ======= mask events =======
export async function movePointerToElement(element: HTMLElement) { /**
const rect = element.getBoundingClientRect() * Move the visual pointer to a position within an element.
* @param x - x coordinate in the element's document viewport
* @param y - y coordinate in the element's document viewport
*/
export async function movePointerToElement(element: HTMLElement, x: number, y: number) {
const offset = getIframeOffset(element) 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 } })) window.dispatchEvent(
new CustomEvent('PageAgent::MovePointerTo', {
detail: { x: x + offset.x, y: y + offset.y },
})
)
await waitFor(0.3) await waitFor(0.3)
} }
export async function clickPointer() {
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
}
export async function enablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::EnablePassThrough'))
}
export async function disablePassThrough() {
window.dispatchEvent(new CustomEvent('PageAgent::DisablePassThrough'))
}
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+1 -5
View File
@@ -1,11 +1,7 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
}, },
"include": ["**/*.ts", "**/*.js"], "include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
+9 -4
View File
@@ -13,13 +13,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/page-controller`))
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }), dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }), cssInjectedByJsPlugin({ relativeCSSInjection: true }),
], ],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/PageController.ts'), entry: resolve(__dirname, 'src/PageController.ts'),
+19 -9
View File
@@ -1,15 +1,25 @@
{ {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-ui.js", "main": "./src/index.ts",
"module": "./dist/lib/page-agent-ui.js", "types": "./src/index.ts",
"types": "./dist/lib/index.d.ts",
"exports": { "exports": {
".": { ".": {
"types": "./dist/lib/index.d.ts", "types": "./src/index.ts",
"import": "./dist/lib/page-agent-ui.js", "default": "./src/index.ts"
"default": "./dist/lib/page-agent-ui.js" }
},
"publishConfig": {
"main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js",
"types": "./dist/lib/index.d.ts",
"exports": {
".": {
"types": "./dist/lib/index.d.ts",
"import": "./dist/lib/page-agent-ui.js",
"default": "./dist/lib/page-agent-ui.js"
}
} }
}, },
"files": [ "files": [
@@ -32,7 +42,7 @@
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\"" "postpublish": "node ../../scripts/post-publish.js"
} }
} }
+1 -1
View File
@@ -291,7 +291,7 @@
transition: max-height 0.2s; transition: max-height 0.2s;
.expanded & { .expanded & {
max-height: 400px; max-height: min(500px, calc(100vh - 200px - var(--height)));
} }
.historyItem { .historyItem {
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// @workaround DTS bug
// dts do not work with monorepo path mapping
// disable path mapping for it
"paths": {}
}
}
+1 -5
View File
@@ -1,11 +1,7 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist"
}, },
"include": ["**/*.ts", "**/*.js"], "include": ["**/*.ts", "**/*.js"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
+9 -4
View File
@@ -13,13 +13,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/ui`))
export default defineConfig({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }), dts({
bundleTypes: true,
compilerOptions: {
composite: true,
noEmit: false,
emitDeclarationOnly: true,
declaration: true,
},
}),
cssInjectedByJsPlugin({ relativeCSSInjection: true }), cssInjectedByJsPlugin({ relativeCSSInjection: true }),
], ],
publicDir: false, publicDir: false,
esbuild: {
keepNames: true,
},
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/index.ts'), entry: resolve(__dirname, 'src/index.ts'),
+2 -1
View File
@@ -85,7 +85,7 @@ Uses wouter browser routing with base path for GitHub Pages deployment at `https
```tsx ```tsx
// main.tsx // main.tsx
<Router base="/page-agent"> <Router base="/page-agent">
<PagesRouter /> <PagesRouter />
</Router> </Router>
``` ```
@@ -109,6 +109,7 @@ Instead of `404.html` redirects, the build copies `index.html` into every route
2. Add route in `src/pages/docs/index.tsx` 2. Add route in `src/pages/docs/index.tsx`
3. Add navigation item in `src/pages/docs/Layout.tsx` 3. Add navigation item in `src/pages/docs/Layout.tsx`
4. Add path to `SPA_ROUTES` in `vite.config.js` 4. Add path to `SPA_ROUTES` in `vite.config.js`
5. Keep `<slug>` consistent across the folder name, import path, route path, sidebar link, and any related cross-links. If the public route changes, rename the folder too.
## Configuration Files ## Configuration Files
+8 -9
View File
@@ -1,13 +1,12 @@
{ {
"name": "@page-agent/website", "name": "@page-agent/website",
"private": true, "private": true,
"version": "1.6.3", "version": "1.8.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
"build:website": "vite build", "build:website": "vite build",
"preview": "vite preview", "preview": "vite preview"
"typecheck": "tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
@@ -16,19 +15,19 @@
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.1", "@types/react-dom": "^19.2.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.0.1", "lucide-react": "^1.8.0",
"motion": "^12.38.0", "motion": "^12.38.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.4", "react": "^19.2.5",
"react-dom": "^19.2.4", "react-dom": "^19.2.5",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.13.0", "simple-icons": "^16.16.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.2.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"wouter": "^3.9.0" "wouter": "^3.9.0"
} }
+1 -1
View File
@@ -18,7 +18,7 @@ export default function Header() {
return ( return (
<> <>
<header <header
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700" className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
role="banner" role="banner"
> >
<div className="max-w-7xl mx-auto px-6 py-4"> <div className="max-w-7xl mx-auto px-6 py-4">
@@ -169,7 +169,6 @@ function highlightSyntax(code: string): string {
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => { const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
const htmlContent = highlightSyntax(code) const htmlContent = highlightSyntax(code)
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} /> return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
} }
@@ -140,6 +140,7 @@ function JSConsole({
// 全局console拦截处理 // 全局console拦截处理
useEffect(() => { useEffect(() => {
const interceptor = ConsoleInterceptor.getInstance() const interceptor = ConsoleInterceptor.getInstance()
let scrollTimer: ReturnType<typeof setTimeout>
const handleGlobalConsole = (type: string, args: unknown[]) => { const handleGlobalConsole = (type: string, args: unknown[]) => {
const content = args.map((arg) => formatResult(arg)).join(' ') const content = args.map((arg) => formatResult(arg)).join(' ')
@@ -152,8 +153,8 @@ function JSConsole({
setOutputs((prev) => [...prev, outputItem]) setOutputs((prev) => [...prev, outputItem])
// 自动滚动到底部 clearTimeout(scrollTimer)
setTimeout(() => { scrollTimer = setTimeout(() => {
if (outputRef.current) { if (outputRef.current) {
outputRef.current.scrollTop = outputRef.current.scrollHeight outputRef.current.scrollTop = outputRef.current.scrollHeight
} }
@@ -164,6 +165,7 @@ function JSConsole({
return () => { return () => {
interceptor.unsubscribe(handleGlobalConsole) interceptor.unsubscribe(handleGlobalConsole)
clearTimeout(scrollTimer)
} }
}, []) }, [])
+2 -2
View File
@@ -1,8 +1,8 @@
// 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.6.3/dist/iife/page-agent.demo.js' 'https://cdn.jsdelivr.net/npm/page-agent@1.8.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.6.3/files/dist/iife/page-agent.demo.js' 'https://registry.npmmirror.com/page-agent/1.8.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'
+4 -5
View File
@@ -9,25 +9,24 @@ const LanguageContext = createContext<{
} | null>(null) } | null>(null)
export function LanguageProvider({ children }: { children: ReactNode }) { export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLang] = useState<Lang>(() => { const [language, setLanguage] = useState<Lang>(() => {
const stored = localStorage.getItem('language') as Lang const stored = localStorage.getItem('language') as Lang
if (stored === 'zh-CN' || stored === 'en-US') return stored if (stored === 'zh-CN' || stored === 'en-US') return stored
return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US' return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'
}) })
const setLanguage = (lang: Lang) => { const switchLanguage = (lang: Lang) => {
setLang(lang) setLanguage(lang)
localStorage.setItem('language', lang) localStorage.setItem('language', lang)
} }
return ( return (
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage }}> <LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage: switchLanguage }}>
{children} {children}
</LanguageContext> </LanguageContext>
) )
} }
// eslint-disable-next-line react-refresh/only-export-components
export function useLanguage() { export function useLanguage() {
const ctx = use(LanguageContext) const ctx = use(LanguageContext)
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider') if (!ctx) throw new Error('useLanguage must be used within LanguageProvider')
@@ -108,6 +108,14 @@ const agent = new PageAgentCore({
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。' ? '在 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.', : '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.',
}, },
{
name: 'keepSemanticTags',
type: 'boolean',
defaultValue: 'false',
description: isZh
? '在简化输出中保留语义标签(如 nav, main, header, footer, aside 等),即使它们不可交互。帮助 LLM 理解页面结构。'
: 'Preserve semantic landmark tags (e.g. nav, main, header, footer, aside) in dehydrated output even if not interactive. Helps LLM understand page structure.',
},
]} ]}
/> />
</section> </section>
@@ -199,6 +199,7 @@ interface ExecuteConfig {
model: string // Model name model: string // Model name
apiKey?: string // LLM AK apiKey?: string // LLM AK
systemInstruction?: string // Global system-level instructions
includeInitialTab?: boolean includeInitialTab?: boolean
experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window
onStatusChange?: (status: AgentStatus) => void onStatusChange?: (status: AgentStatus) => void
@@ -16,12 +16,12 @@ const BASELINE = new Set([
// Models grouped by brand, newest first // Models grouped by brand, newest first
const MODEL_GROUPS: Record<string, string[]> = { const MODEL_GROUPS: Record<string, string[]> = {
Qwen: [ Qwen: [
'qwen3.6-plus',
'qwen3.5-plus', 'qwen3.5-plus',
'qwen3.5-flash', 'qwen3.5-flash',
'qwen3-coder-next', 'qwen3-coder-next',
'qwen-3-max', 'qwen-3-max',
'qwen-3-plus', 'qwen-3-plus',
'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'], 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'],
@@ -64,7 +64,6 @@ export default function Models() {
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'} : 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
</p> </p>
{/* Models Section */}
<section className="mb-10"> <section className="mb-10">
<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'}
@@ -87,9 +86,10 @@ export default function Models() {
</div> </div>
</section> </section>
{/* Tips Section */}
<section className="mb-10"> <section className="mb-10">
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2> <Heading id="tips" level={3}>
Tips
</Heading>
<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> <li>
@@ -111,7 +111,6 @@ export default function Models() {
</div> </div>
</section> </section>
{/* Configuration Section */}
<section className="mb-10"> <section className="mb-10">
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading> <Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
<CodeEditor <CodeEditor
@@ -121,25 +120,50 @@ const pageAgent = new PageAgent({
apiKey: 'your-api-key', apiKey: 'your-api-key',
model: 'qwen3.5-plus' model: 'qwen3.5-plus'
}); });
// 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({
baseURL: 'http://localhost:11434/v1',
model: 'qwen3:14b'
});
`} `}
/> />
</section> </section>
{/* Free Testing API Section */} <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 className="mb-10"> <section className="mb-10">
<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">
@@ -181,115 +205,143 @@ const pageAgent = new PageAgent({
</a> </a>
</p> </p>
<CodeEditor <CodeEditor
code={`# qwen3.5-plus (default for demos) or qwen3.5-flash (lighter) code={`# qwen3.5-plus / qwen3.5-flash
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run" LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
LLM_MODEL_NAME="qwen3.5-plus" LLM_MODEL_NAME="qwen3.5-plus"`}
LLM_API_KEY="NA"`}
/> />
</div> </div>
</section> </section>
{/* Ollama Section */}
<section className="mb-10"> <section className="mb-10">
<Heading id="ollama">Ollama</Heading> <Heading id="local-runtimes">{isZh ? '本地运行时' : 'Local Runtimes'}</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-6">
{isZh {isZh
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。' ? '通过 Ollama、LM Studio 等本地 OpenAI-compatible 运行时接入 PageAgent,实现离线或局域网部署。'
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'} : 'Use local OpenAI-compatible runtimes such as Ollama and LM Studio with PageAgent for offline or LAN deployments.'}
</p> </p>
<CodeEditor
code={`LLM_BASE_URL="http://localhost:11434/v1"
LLM_API_KEY="NA"
LLM_MODEL_NAME="qwen3:14b"`}
/>
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
</h3>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
{isZh
? '确保 OLLAMA_ORIGINS 设置为 * 以避免 403 错误'
: 'Add * to OLLAMA_ORIGINS to avoid 403 errors'}
</li>
<li>
{isZh
? '小于 10B 参数的模型通常效果不佳'
: 'Models smaller than 10B are unlikely to be strong enough'}
</li>
<li>{isZh ? '需要支持 tool_call 的模型' : 'Requires tool_call capable models'}</li>
<li>
{isZh
? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作'
: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}
</li>
</ul>
</div>
<div className="mt-4"> <div className="space-y-10">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3"> <section>
{isZh ? '建议启动参数' : 'Recommended Startup'} <Heading id="requirements" level={3}>
</h3> Requirements
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> </Heading>
{isZh <div className="mt-4 p-5 bg-amber-50 dark:bg-amber-950/20 rounded-xl border border-amber-200 dark:border-amber-800">
? '启动 Ollama 时建议配置以下环境变量:扩大上下文窗口、允许跨域访问、监听所有网络接口。' <ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
: 'Start Ollama with these environment variables: larger context window, allow cross-origin access, and listen on all interfaces.'} <li>
</p> {isZh
? '务必打开 CORS,否则浏览器无法直接请求本地 LLM 服务。'
: 'Enable CORS, otherwise the browser cannot call your local LLM endpoint directly.'}
</li>
<li>
{isZh
? '将 context length 或 content length 至少设置为 8000。普通页面常常需要 15k token 左右,默认 4k 很容易被截断。'
: 'Set context length or content length to at least 8000. A typical page often needs around 15k tokens, so the default 4k usually truncates prompts.'}
</li>
<li>
{isZh ? '需要支持 tool_call 的模型。' : 'Use a model with tool_call support.'}
</li>
<li>
{isZh
? '小于 10B 参数的模型通常效果不佳。'
: 'Models smaller than 10B are usually not strong enough.'}
</li>
</ul>
</div>
</section>
<div className="space-y-2"> <section>
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">macOS / Linux</p> <Heading id="local-configuration" level={3}>
{isZh ? '基础配置' : 'Basic Configuration'}
</Heading>
<CodeEditor <CodeEditor
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`} code={`// Local OpenAI-compatible runtime - no apiKey needed
/> const pageAgent = new PageAgent({
baseURL: 'http://localhost:11434/v1',
model: 'qwen3:14b'
});
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2"> // Or connect to LM Studio
Windows (PowerShell) const lmStudioAgent = new PageAgent({
baseURL: 'http://127.0.0.1:1234/v1',
model: 'qwen/qwen3.5-27b'
});
`}
/>
</section>
<section>
<Heading id="ollama" level={3}>
Ollama
</Heading>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{isZh
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
</p> </p>
<CodeEditor <CodeEditor
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`} code={`LLM_BASE_URL="http://localhost:11434/v1"
LLM_MODEL_NAME="qwen3:14b"`}
/> />
</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">
</div> <h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
</section> {isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
</h3>
<p className="text-sm text-gray-700 dark:text-gray-300">
{isZh
? '如果浏览器侧请求失败,优先检查 Ollama 是否已按上面的要求开启 CORS。'
: 'If browser-side requests fail, check whether Ollama has CORS enabled as required above.'}
</p>
</div>
{/* Production Authentication */} <div className="mt-4">
<section className="mb-10"> <h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
<Heading id="production-authentication" className="text-2xl font-semibold mb-4"> {isZh ? '建议启动参数' : 'Recommended Startup'}
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'} </h3>
</Heading> <p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> {isZh
{isZh ? '启动 Ollama 时建议同时放大上下文窗口并开启跨域访问。'
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。' : 'When starting Ollama, increase the context window and enable cross-origin access.'}
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'} </p>
</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3"> <div className="space-y-2">
{isZh ? ( <p className="text-xs font-medium text-gray-500 dark:text-gray-400">
<> macOS / Linux
Web LLM 使{' '} </p>
<code>customFetch</code> Cookie <CodeEditor
</> code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
) : ( />
<>
If you plan to integrate it into your web app, it's better to have a backend proxy for <p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
the LLM and use <code>customFetch</code> to authenticate the request with cookies or Windows (PowerShell)
other methods: </p>
</> <CodeEditor
)} code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
</p> />
<CodeEditor </div>
code={`const agent = new PageAgent({ </div>
baseURL: '/api/llm-proxy', </section>
model: 'gpt-5.1',
customFetch: (url, init) => <section>
fetch(url, { ...init, credentials: 'include' }), <Heading id="lm-studio" level={3}>
});`} LM Studio
/> </Heading>
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg"> <CodeEditor
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200"> code={`LLM_BASE_URL="http://127.0.0.1:1234/v1"
{isZh LLM_MODEL_NAME="qwen/qwen3.5-27b"`}
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中' />
: '⚠️ NEVER commit real LLM API keys to your frontend code'} <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> <h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
</h3>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
<li>
{isZh
? 'Agent 必须启用 disableNamedToolChoice,否则 tool_choice 参数会报错。'
: 'Enable disableNamedToolChoice in the agent config, otherwise the tool_choice parameter may fail.'}
</li>
</ul>
</div>
</section>
</div> </div>
</section> </section>
</div> </div>
@@ -103,25 +103,25 @@ function FormatErrorsContent(isZh: boolean) {
<> <>
{' '} {' '}
<a <a
href="https://github.com/alibaba/page-agent/discussions" href="https://github.com/alibaba/page-agent/issues"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2" className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
> >
GitHub Discussions GitHub Issues
</a>{' '} </a>{' '}
</> </>
) : ( ) : (
<> <>
If the above steps don't help, join the{' '} If the above steps don't help, open a{' '}
<a <a
href="https://github.com/alibaba/page-agent/discussions" href="https://github.com/alibaba/page-agent/issues"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2" className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
> >
GitHub Discussions GitHub Issue
</a>{' '} </a>{' '}
with your model name and error details. with your model name and error details.
</> </>
@@ -274,10 +274,12 @@ function ApiErrorsContent(isZh: boolean) {
code={`const agent = new PageAgent({ code={`const agent = new PageAgent({
// ... // ...
customFetch: async (url, init) => { customFetch: async (url, init) => {
const body = JSON.parse(init.body)
// Adapt parameters for your provider // Adapt parameters for your provider
delete body.stream_options const body = JSON.parse(init.body)
return fetch(url, { ...init, body: JSON.stringify(body) }) delete body.tool_choice
const bodyStr = JSON.stringify(body)
return fetch(url, { ...init, body: bodyStr })
}, },
})`} })`}
/> />
@@ -16,12 +16,13 @@ const LLM_CLOUD: {
opacity: number opacity: number
}[] = [ }[] = [
{ name: 'OpenAI', color: '#10b981', x: 18, y: 22, size: 1.5, opacity: 1 }, { name: 'OpenAI', color: '#10b981', x: 18, y: 22, size: 1.5, opacity: 1 },
{ name: 'Claude', color: '#f97316', x: 62, y: 15, size: 1.35, opacity: 0.95 }, { name: 'Claude', color: '#f97316', x: 58, y: 15, size: 1.35, opacity: 0.95 },
{ name: 'Qwen', color: '#8b5cf6', x: 38, y: 50, size: 1.8, opacity: 0.9 }, { name: 'Qwen', color: '#8b5cf6', x: 35, y: 50, size: 1.8, opacity: 0.9 },
{ name: 'Gemini', color: '#3b82f6', x: 68, y: 48, size: 1.2, opacity: 0.85 }, { name: 'Gemini', color: '#3b82f6', x: 72, y: 48, size: 1.2, opacity: 0.85 },
{ name: 'DeepSeek', color: '#06b6d4', x: 10, y: 65, size: 1.1, opacity: 0.8 }, { name: 'DeepSeek', color: '#06b6d4', x: 13, y: 65, size: 1.1, opacity: 0.8 },
{ name: 'Grok', color: '#f43f5e', x: 52, y: 78, size: 1.0, opacity: 0.75 }, { name: 'Grok', color: '#f43f5e', x: 52, y: 78, size: 1.0, opacity: 0.75 },
{ name: 'Ollama', color: '#9ca3af', x: 82, y: 25, size: 1.1, opacity: 0.8 }, { name: 'Ollama', color: '#9ca3af', x: 84, y: 25, size: 1.1, opacity: 0.8 },
{ name: 'LM Studio', color: '#4338ca', x: 49, y: 36, size: 0.8, opacity: 0.9 },
{ name: 'Kimi', color: '#14b8a6', x: 30, y: 82, size: 0.85, opacity: 0.6 }, { name: 'Kimi', color: '#14b8a6', x: 30, y: 82, size: 0.85, opacity: 0.6 },
{ name: 'GLM', color: '#f59e0b', x: 70, y: 72, size: 0.85, opacity: 0.55 }, { name: 'GLM', color: '#f59e0b', x: 70, y: 72, size: 0.85, opacity: 0.55 },
{ name: 'LLaMA', color: '#60a5fa', x: 88, y: 70, size: 0.8, opacity: 0.45 }, { name: 'LLaMA', color: '#60a5fa', x: 88, y: 70, size: 0.8, opacity: 0.45 },
@@ -1,4 +1,3 @@
/* eslint-disable react-dom/no-dangerously-set-innerhtml */
import type { PageAgent as PageAgentType } from 'page-agent' import type { PageAgent as PageAgentType } from 'page-agent'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link, useSearchParams } from 'wouter' import { Link, useSearchParams } from 'wouter'
@@ -46,10 +45,11 @@ export default function HeroSection() {
: 'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown' : 'Goto docs in navigation bar, find Quick-Start section, and summarize in markdown'
const [task, setTask] = useState(() => defaultTask) const [task, setTask] = useState(() => defaultTask)
const [prevDefaultTask, setPrevDefaultTask] = useState(defaultTask)
useEffect(() => { if (prevDefaultTask !== defaultTask) {
setPrevDefaultTask(defaultTask)
setTask(defaultTask) setTask(defaultTask)
}, [defaultTask]) }
const [params] = useSearchParams() const [params] = useSearchParams()
const isOther = params.has('try_other') const isOther = params.has('try_other')
+6 -4
View File
@@ -18,10 +18,12 @@ function ScrollToTop() {
export default function Router() { export default function Router() {
useEffect(() => { useEffect(() => {
const schedule = globalThis.requestIdleCallback ?? ((cb: () => void) => setTimeout(cb, 1)) if ('requestIdleCallback' in globalThis) {
const cancel = globalThis.cancelIdleCallback ?? clearTimeout const id = requestIdleCallback(() => docsImport())
const id = schedule(() => docsImport()) return () => cancelIdleCallback(id)
return () => cancel(id) }
const id = setTimeout(() => docsImport(), 1)
return () => clearTimeout(id)
}, []) }, [])
return ( return (
+3 -24
View File
@@ -2,31 +2,10 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
"noEmit": false,
"allowImportingTsExtensions": false,
"baseUrl": ".",
"outDir": "dist",
"paths": { "paths": {
// Self root "@/*": ["./src/*"]
"@/*": ["src/*"],
"@page-agent/llms": ["../llms/src/index.ts"],
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
"@page-agent/ui": ["../ui/src/index.ts"],
"page-agent": ["../page-agent/src/PageAgent.ts"]
} }
}, },
"include": ["**/*.ts", "**/*.tsx"], "include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["dist", "node_modules"], "exclude": ["dist", "node_modules"]
"references": [
//
{ "path": "../llms" },
{ "path": "../page-controller" },
{ "path": "../core" },
{ "path": "../ui" },
{ "path": "../page-agent" }
]
} }
+1 -9
View File
@@ -26,6 +26,7 @@ const SPA_ROUTES = [
'docs/features/data-masking', 'docs/features/data-masking',
'docs/features/custom-instructions', 'docs/features/custom-instructions',
'docs/features/models', 'docs/features/models',
'docs/features/local-llms',
'docs/features/chrome-extension', 'docs/features/chrome-extension',
'docs/features/mcp-server', 'docs/features/mcp-server',
'docs/features/third-party-agent', 'docs/features/third-party-agent',
@@ -89,16 +90,7 @@ export default defineConfig(({ mode }) => ({
}, },
resolve: { resolve: {
alias: { alias: {
// Self root
'@': resolve(__dirname, 'src'), '@': resolve(__dirname, 'src'),
// Monorepo packages (always bundle local code instead of npm versions)
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
'page-agent': resolve(__dirname, '../page-agent/src/PageAgent.ts'),
}, },
}, },
define: { define: {
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env node
/**
* Restore package.json from the backup created by pre-publish.js,
* then clean up temporary files (backup, LICENSE, README.md).
*
* Usage: node ../../scripts/post-publish.js (from a package dir)
*/
import { existsSync, readFileSync, renameSync, rmSync } from 'fs'
import { join } from 'path'
const pkgPath = join(process.cwd(), 'package.json')
const bakPath = pkgPath + '.bak'
if (!existsSync(bakPath)) {
console.log(' No backup found, nothing to restore.')
process.exit(0)
}
const name = JSON.parse(readFileSync(pkgPath, 'utf-8')).name
renameSync(bakPath, pkgPath)
console.log(' ✓ package.json restored from backup')
rmSync(join(process.cwd(), 'LICENSE'), { force: true })
console.log(' ✓ LICENSE removed')
if (name === 'page-agent') {
rmSync(join(process.cwd(), 'README.md'), { force: true })
console.log(' ✓ README.md removed')
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env node
/**
* Backup package.json, then rewrite it for publish:
* - Promote `publishConfig` fields to top level
* - Remove `publishConfig` (npm doesn't need the wrapper)
* - Copy LICENSE (and README.md for the main package)
*
* Usage: node ../../scripts/pre-publish.js (from a package dir)
*/
import { copyFileSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
const pkgPath = join(process.cwd(), 'package.json')
const raw = readFileSync(pkgPath, 'utf-8')
const pkg = JSON.parse(raw)
const publishConfig = pkg.publishConfig
if (!publishConfig) {
console.log(' No publishConfig found, skipping manifest rewrite.')
process.exit(0)
}
// Backup the original file byte-for-byte
copyFileSync(pkgPath, pkgPath + '.bak')
console.log(' ✓ package.json backed up')
for (const [field, value] of Object.entries(publishConfig)) {
pkg[field] = value
}
delete pkg.publishConfig
writeFileSync(pkgPath, JSON.stringify(pkg, null, ' ') + '\n')
console.log(` ✓ Manifest rewritten for publish (${Object.keys(publishConfig).join(', ')})`)
const root = join(process.cwd(), '../..')
copyFileSync(join(root, 'LICENSE'), join(process.cwd(), 'LICENSE'))
console.log(' ✓ LICENSE copied')
if (pkg.name === 'page-agent') {
copyFileSync(join(root, 'README.md'), join(process.cwd(), 'README.md'))
console.log(' ✓ README.md copied')
}
+6 -8
View File
@@ -1,18 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true, "target": "es2025",
"target": "ES2024",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2024", "DOM", "DOM.Iterable"], "lib": ["ESNext", "DOM"],
"module": "ESNext", "module": "esnext",
"skipLibCheck": true, "skipLibCheck": true,
"allowJs": true, "allowJs": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
// "baseUrl": "src",
"baseUrl": ".",
"outDir": "dist", "outDir": "dist",
// "incremental": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
@@ -27,6 +23,8 @@
"noUnusedParameters": false, "noUnusedParameters": false,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"types": ["node"]
} }
} }
-14
View File
@@ -1,14 +0,0 @@
// this is only for IDE ts language server to work.
// do not use this for building or linting.
{
"extends": "./tsconfig.base.json",
"references": [
{ "path": "./packages/page-controller" },
{ "path": "./packages/ui" },
{ "path": "./packages/llms" },
{ "path": "./packages/page-agent" },
{ "path": "./packages/website" }
],
"include": ["packages/*/src/**/*.ts", "packages/*/src/**/*.tsx"],
"exclude": ["node_modules", "dist", "packages/*/dist"]
}
+25
View File
@@ -0,0 +1,25 @@
// Unified typecheck config for all lib/website packages.
// Run: tsc --noEmit -p tsconfig.typecheck.json
// Extension is checked separately (WXT has its own tsconfig base).
//
// With source-first package.json exports, TS resolves @page-agent/*
// to sibling src/ via workspace symlinks no paths mapping needed.
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": false,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.typecheck.tsbuildinfo",
"paths": {
"@/*": ["./packages/website/src/*"]
}
},
"include": [
"packages/llms/src/**/*",
"packages/page-controller/src/**/*",
"packages/ui/src/**/*",
"packages/core/src/**/*",
"packages/page-agent/src/**/*",
"packages/website/src/**/*"
]
}