Compare commits

...

59 Commits

Author SHA1 Message Date
Simon 4122735bba chore(version): bump version to 1.12.1 2026-07-10 17:09:56 +08:00
Simon 1cab66b898 feat(llms): update model list and patches (#611)
* fix(llms): gpt patch

* feat(llms): patch `hunyuan` models

* feat(llms): update model list
2026-07-10 17:08:14 +08:00
Simon d5c994708c chore(llms): gate live model tests behind test:live (#610)
* chore(llms): gate live model tests behind test:live

Keep *.live.test.ts out of npm test so local CI stays fast; run them
explicitly via npm run test:live when checking provider compatibility.

* chore: skill gh permission
2026-07-10 15:48:52 +08:00
Simon 4c76a5f1d2 fix(ci): pin release workflow to npm 11 (#609)
npm@latest now resolves to npm 12, whose install-time security defaults break the release pipeline.
2026-07-10 14:49:17 +08:00
Simon e8c8ab0d78 chore(ci): remove workflow_dispatch from main workflows (#605) 2026-07-09 16:05:42 +08:00
Simon 9705fb964e docs: readme 2026-07-09 16:00:25 +08:00
Simon 29fee0012a chore(version): bump version to 1.12.0 2026-07-09 15:50:06 +08:00
Simon 33dcf67252 chore(llms): limit live test concurrency (#604) 2026-07-09 15:47:12 +08:00
Simon ab7e9e9a89 docs: point CI badge at main-ci.yml (#603) 2026-07-09 15:46:53 +08:00
Simon d77ac0322d chore(ci): run main workflows on self-hosted runners (#602)
Split PR CI (GitHub-hosted) from main push CI/deploy (page-agent-trusted).
2026-07-09 15:35:59 +08:00
Simon 3162ac5b45 chore(ci): restrict release workflow to authorized actor 2026-07-08 17:16:39 +08:00
Simon 2c67f3498b chore: rename workflow 2026-07-08 16:30:54 +08:00
Simon 7ee27b4962 feat(chore): update PR template 2026-07-08 16:21:04 +08:00
Simon cad49d5d1f feat(extension): add tab status to context (#599) 2026-07-08 16:14:04 +08:00
Simon 35ff6d44fe fix(extension): stateless SW, pull tab state instead of ports (#596)
* feat(extension)!: make sw stateless; tabs update with pulling instead of pushing

* fix(extension): harden tab loading wait

* fix: safer `waitUntil`

* chore: comments

* chore: reduce polling logs
2026-07-07 21:08:27 +08:00
dependabot[bot] 9609592be3 chore(deps-dev): bump the development-dependencies group with 8 updates (#595) 2026-07-07 11:31:19 +08:00
Simon 08624ec245 fix(ci): format README and align markdown tooling with Prettier (#593) 2026-07-06 17:34:56 +08:00
Simon 8bc2483ab5 docs: improve README copy and quick start (#592)
* docs: improve README copy and quick start

* docs: put each badge on its own line
2026-07-06 16:53:43 +08:00
Simon 68245e8b68 docs: update CHANGELOG 2026-07-04 00:27:49 +08:00
Simon f73831036d chore(version): bump version to 1.11.0 2026-07-03 23:57:03 +08:00
Simon f8af56032a chore(ui): remove legacy motion-css code (#583)
Unused CSS motion experiment with known performance issues; no references remain.
2026-07-03 23:51:29 +08:00
Erwann Robin 60fe4c4c84 chore: set the bookmark script to English locale (#580)
* chore: set the bookmark script to English locale

* chore: adapt the booklet to the locale

* refactor(demo): improve naming to better distinguish mirror and lang

---------

Co-authored-by: Simon <10131203+gaomeng1900@users.noreply.github.com>
2026-07-03 22:49:12 +08:00
Simon 4ce6d9e5c7 Merge pull request #581 from alibaba/refactor/llms
refactor(llms): rewrite model patching and deprecate temperature
2026-07-03 20:37:43 +08:00
Simon 9a75ead6ad fix: do not enable OpenRouter reasoning by default 2026-07-03 20:30:57 +08:00
Simon 10276c6e67 fix: new URL should always be in a try block 2026-07-03 20:14:11 +08:00
Simon 5aeb67f6d6 feat(llms): update model list; update patch 2026-07-03 03:04:43 +08:00
Simon 556bc7448a chore(llms): add real-api test for all supported models 2026-07-03 02:29:06 +08:00
Simon 04b84b6e8a fix(llms): model compatibility problem 2026-07-03 02:26:01 +08:00
Simon 2163a55cc7 docs: remove models that can not be tested anymore from the list 2026-07-03 02:25:03 +08:00
Simon a47e12c0e0 fix: model patch for GPT and Claude 2026-07-02 22:11:28 +08:00
Simon d1c3dc811e docs: update model list 2026-07-02 21:51:55 +08:00
Simon 8ad719744a feat: rewrite llm config and model patch 2026-07-02 21:31:44 +08:00
Simon 83fee37fb4 fix: ci 2026-07-02 03:13:39 +08:00
Simon b41535ef5e Merge pull request #579 from alibaba/chore/cspell-vscode-config
chore: move cSpell config to .vscode/cspell.json
2026-07-02 03:09:00 +08:00
Simon 8ad1174f20 chore: move cSpell config to .vscode/cspell.json
Centralize project dictionary and ignore paths; fix minor prompt typo and GA tag spell-check noise.
2026-07-02 03:05:38 +08:00
Simon 026171715c Merge pull request #578 from alibaba/fix/wrong-active-tab-multi-window
fix: wrong active tab/window in multi-window scenarios
2026-07-02 02:43:04 +08:00
Simon 8b902e1e10 docs(ext): add limitations section 2026-07-02 02:32:33 +08:00
Simon 1f45e74378 fix(extension): resolve active tab/window per caller context 2026-07-02 02:11:32 +08:00
Simon 585e7a4900 chore: update deps 2026-07-01 17:09:57 +08:00
Simon 04a397c19e Merge pull request #577 from alibaba/dependabot/npm_and_yarn/development-dependencies-333bd1d898
chore(deps-dev): bump the development-dependencies group with 16 updates
2026-06-30 19:08:33 +08:00
Simon ae4276c624 fix: code style 2026-06-30 19:00:58 +08:00
dependabot[bot] 7579def667 chore(deps-dev): bump the development-dependencies group with 16 updates
Bumps the development-dependencies group with 16 updates:

| Package | From | To |
| --- | --- | --- |
| [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) | `21.0.2` | `21.1.0` |
| [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) | `21.0.2` | `21.1.0` |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.9.2` | `5.10.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.0.0` | `26.0.1` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [eslint](https://github.com/eslint/eslint) | `10.5.0` | `10.6.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.4` | `3.9.3` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.62.0` | `8.62.1` |
| [unplugin-dts](https://github.com/qmhc/unplugin-dts/tree/HEAD/packages/unplugin-dts) | `1.0.2` | `1.0.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.0.16` | `8.1.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.21.0` | `1.22.0` |
| [motion](https://github.com/motiondivision/motion) | `12.40.0` | `12.42.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.24.0` | `16.24.1` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` |
| [wxt](https://github.com/wxt-dev/wxt) | `0.20.26` | `0.20.27` |


Updates `@commitlint/cli` from 21.0.2 to 21.1.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.1.0/@commitlint/cli)

Updates `@commitlint/config-conventional` from 21.0.2 to 21.1.0
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.1.0/@commitlint/config-conventional)

Updates `@eslint-react/eslint-plugin` from 5.9.2 to 5.10.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.10.0/plugins/eslint-plugin)

Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite)

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

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

Updates `eslint` from 10.5.0 to 10.6.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.5.0...v10.6.0)

Updates `prettier` from 3.8.4 to 3.9.3
- [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.4...3.9.3)

Updates `typescript-eslint` from 8.62.0 to 8.62.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.62.1/packages/typescript-eslint)

Updates `unplugin-dts` from 1.0.2 to 1.0.3
- [Release notes](https://github.com/qmhc/unplugin-dts/releases)
- [Changelog](https://github.com/qmhc/unplugin-dts/blob/main/packages/unplugin-dts/CHANGELOG.md)
- [Commits](https://github.com/qmhc/unplugin-dts/commits/unplugin-dts@1.0.3/packages/unplugin-dts)

Updates `vite` from 8.0.16 to 8.1.0
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite)

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

Updates `motion` from 12.40.0 to 12.42.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.40.0...v12.42.0)

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

Updates `tailwindcss` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss)

Updates `wxt` from 0.20.26 to 0.20.27
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Commits](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.26...wxt-v0.20.27)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 21.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 21.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: unplugin-dts
  dependency-version: 1.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: vite
  dependency-version: 8.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.22.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: motion
  dependency-version: 12.42.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.24.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: wxt
  dependency-version: 0.20.27
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 00:43:38 +00:00
Simon 187d5deccb Merge pull request #568 from itxaiohanglover/fix/chat-latest-reasoning-effort
fix(llms): skip reasoning_effort patch for chat-latest models
2026-06-26 01:07:30 +08:00
Simon fb221b4535 Merge pull request #573 from alibaba/feat/assets-repo
fix(assets): migrate img.alicdn.com URLs to page-agent.github.io
2026-06-26 01:02:50 +08:00
Simon 5058c96a78 fix(assets): migrate img.alicdn.com URLs to page-agent.github.io 2026-06-26 00:56:37 +08:00
Simon 2b679d85cb fix(llms): omitting both reasoning_effort and temperature for chat-latest 2026-06-26 00:12:34 +08:00
artboy 57f64347ac fix(llms): skip reasoning_effort patch for *-chat-latest models
normalizeModelName reduces gpt-5.2-chat-latest to gpt-52-chat-latest,
which matches the gpt-52 prefix and gets reasoning_effort='none'.
But chat-latest models don't support reasoning_effort, causing
HTTP 400.

Fix: check for 'chat-latest' before applying reasoning_effort patches.

Fixes #563
2026-06-23 14:26:45 +08:00
Simon fb4b8fd067 Merge pull request #565 from alibaba/dependabot/github_actions/github-actions-640176b5ab
chore(deps): bump actions/checkout from 6 to 7 in the github-actions group
2026-06-23 14:24:52 +08:00
Simon 2bed20cd85 Merge pull request #566 from alibaba/dependabot/npm_and_yarn/development-dependencies-1075e1fe2e
chore(deps-dev): bump the development-dependencies group with 8 updates
2026-06-23 14:24:36 +08:00
Simon c48c7c524e Merge pull request #567 from alibaba/dependabot/npm_and_yarn/development-major-3bc154b33b
chore(deps-dev): bump @types/node from 25.9.3 to 26.0.0 in the development-major group
2026-06-23 14:24:20 +08:00
dependabot[bot] 32f27e73e3 chore(deps-dev): bump @types/node in the development-major group
Bumps the development-major group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: development-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:30:20 +00:00
dependabot[bot] 71738f1966 chore(deps-dev): bump the development-dependencies group with 8 updates
Bumps the development-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.9.0` | `5.9.2` |
| [globals](https://github.com/sindresorhus/globals) | `17.6.0` | `17.7.0` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.10.4` | `20.10.6` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.1` | `8.62.0` |
| [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) | `0.1.43` | `0.2.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.18.0` | `1.21.0` |
| [simple-icons](https://github.com/simple-icons/simple-icons) | `16.23.0` | `16.24.0` |


Updates `@eslint-react/eslint-plugin` from 5.9.0 to 5.9.2
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.9.2/plugins/eslint-plugin)

Updates `globals` from 17.6.0 to 17.7.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.6.0...v17.7.0)

Updates `happy-dom` from 20.10.4 to 20.10.6
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.4...v20.10.6)

Updates `lint-staged` from 17.0.7 to 17.0.8
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

Updates `typescript-eslint` from 8.61.1 to 8.62.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.62.0/packages/typescript-eslint)

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

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

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

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: globals
  dependency-version: 17.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: happy-dom
  dependency-version: 20.10.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@types/chrome"
  dependency-version: 0.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: simple-icons
  dependency-version: 16.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:29:32 +00:00
dependabot[bot] e44455ba80 chore(deps): bump actions/checkout in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 00:27:34 +00:00
Simon d2ee260d82 Merge pull request #560 from alibaba/dependabot/npm_and_yarn/development-dependencies-d854c95bb8
chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
2026-06-17 20:53:39 +08:00
dependabot[bot] 1f738bc41c chore(deps-dev): bump the development-dependencies group across 1 directory with 17 updates
Bumps the development-dependencies group with 16 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint-react/eslint-plugin](https://github.com/Rel1cx/eslint-react/tree/HEAD/plugins/eslint-plugin) | `5.8.16` | `5.9.0` |
| [@microsoft/api-extractor](https://github.com/microsoft/rushstack/tree/HEAD/apps/api-extractor) | `7.58.8` | `7.58.9` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` |
| [eslint](https://github.com/eslint/eslint) | `10.4.1` | `10.5.0` |
| [happy-dom](https://github.com/capricorn86/happy-dom) | `20.10.2` | `20.10.4` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.0` | `8.61.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.9` | `2.1.10` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.9` | `1.1.10` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.0` | `1.3.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.18.0` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.9` | `1.2.10` |



Updates `@eslint-react/eslint-plugin` from 5.8.16 to 5.9.0
- [Release notes](https://github.com/Rel1cx/eslint-react/releases)
- [Changelog](https://github.com/Rel1cx/eslint-react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Rel1cx/eslint-react/commits/v5.9.0/plugins/eslint-plugin)

Updates `@microsoft/api-extractor` from 7.58.8 to 7.58.9
- [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.9/apps/api-extractor)

Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

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

Updates `eslint` from 10.4.1 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0)

Updates `happy-dom` from 20.10.2 to 20.10.4
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.2...v20.10.4)

Updates `prettier` from 3.8.3 to 3.8.4
- [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.3...3.8.4)

Updates `typescript-eslint` from 8.61.0 to 8.61.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.61.1/packages/typescript-eslint)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/HEAD/packages/vitest)

Updates `@radix-ui/react-hover-card` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.9 to 2.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-separator` from 1.1.9 to 1.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.3.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

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

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `@radix-ui/react-tooltip` from 1.2.9 to 1.2.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@eslint-react/eslint-plugin"
  dependency-version: 5.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@microsoft/api-extractor"
  dependency-version: 7.58.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: happy-dom
  dependency-version: 20.10.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.1.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 05:00:12 +00:00
Simon 2802ab3dea Merge pull request #559 from alibaba/fix/hoist-happy-dom
fix(deps): hoist happy-dom to root devDependencies
2026-06-16 12:33:51 +08:00
Simon e6a2e07640 fix(deps): hoist happy-dom to root devDependencies 2026-06-16 12:30:55 +08:00
Simon 235fa1bc26 chore: fix lint error caused CI fail 2026-06-15 22:59:34 +08:00
Simon c04c02723d chore(version): bump version to 1.10.0 2026-06-15 17:31:54 +08:00
54 changed files with 2350 additions and 2372 deletions
@@ -13,7 +13,7 @@ Turn a working tree diff into a clean branch, commit, and pull request.
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.
3. **Only check Testing checkboxes you actually verified.** The "`npm run ci` passes" checkbox MAY be checked only if you ran `npm run ci` in this session and it passed. All other Testing checkboxes (browser tested, no console errors, types/doc added) require manual 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.
@@ -24,6 +24,13 @@ Before attempting to push or open a PR, verify that the necessary tools are avai
- 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.
## Shell Permissions
`gh` reads credentials from the OS keyring. A sandboxed shell cannot reach the keyring and will report a false auth failure.
- Run every `gh` command (`gh auth status`, `gh pr create`, `gh pr view`, …) and every `git push` / `git fetch` that talks to GitHub with full permissions on the **first** attempt (`required_permissions: ["all"]` for the Shell tool). Do not probe these in the sandbox first.
- Local read-only git inspection (`git status`, `git diff`, `git log`, `git branch`) may stay sandboxed.
## Procedure
1. **Read contribution rules.** Read `CONTRIBUTING.md` and any package-level instructions (e.g. `packages/*/AGENTS.md`) relevant to the changed files.
@@ -34,7 +41,7 @@ Before attempting to push or open a PR, verify that the necessary tools are avai
- **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.
- **CI**: Run `npm run ci` and make sure it passes. 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`)
@@ -47,7 +54,7 @@ Before attempting to push or open a PR, verify that the necessary tools are avai
- 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.
9. **Open PR.** Use `gh pr create` with the full PR template structure. Fill in "What" and "Type" sections based on the actual diff. Check "`npm run ci` passes" only if it actually passed in this session; leave all other "Testing" and all "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
@@ -75,7 +82,7 @@ After successfully opening the PR, ALWAYS give a brief reminder in the user's la
## Validation Strategy
- Default to enough validation to defend the PR, not the absolute minimum.
- Run `npm run ci` to run all build and lint checks.
- Always run `npm run ci` (build, lint, typecheck, tests) before opening the PR. It must pass.
- 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.
@@ -97,6 +104,6 @@ After successfully opening the PR, ALWAYS give a brief reminder in the user's la
- 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.
- All "Requirements" checkboxes and all human-only "Testing" checkboxes are unchecked (`- [ ]`) in the PR body. The only checkbox you may check is "`npm run ci` passes", and only if it truly passed.
- The reported validation is accurate — nothing fabricated.
- The post-submission reminder was delivered in the user's language, concisely and accurately.
+1 -1
View File
@@ -14,8 +14,8 @@ Closes #(issue)
## Testing
- [ ] `npm run ci` passes
- [ ] Tested in modern browsers
- [ ] No console errors
- [ ] Types/doc added
## Requirements / 要求
+3 -9
View File
@@ -3,27 +3,21 @@ permissions:
contents: read
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
node-version: 24
cache: 'npm'
- name: Install dependencies
@@ -1,4 +1,4 @@
name: Deploy Demo
name: Deploy Website
on:
push:
@@ -6,14 +6,14 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
runs-on: page-agent-trusted
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -24,7 +24,7 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build demo
- name: Build website
run: npm run build:website
- name: Setup Pages
+29
View File
@@ -0,0 +1,29 @@
# Runs only for code already on main, on the self-hosted runner.
# PR-triggered CI must stay on GitHub-hosted runners (see ci.yml).
name: Main CI
permissions:
contents: read
on:
push:
branches: [main]
jobs:
test:
runs-on: page-agent-trusted
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: CI checks
run: node scripts/ci.js
+5 -3
View File
@@ -11,9 +11,11 @@ permissions:
jobs:
release:
# Only the repo owner may trigger a release, even if tag protection is bypassed
if: github.actor == 'gaomeng1900'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -21,9 +23,9 @@ jobs:
node-version: 24
registry-url: 'https://registry.npmjs.org'
# Ensure npm 11.5.1 or later is installed
# Stay on the latest npm 11 release
- name: Update npm
run: npm install -g npm@latest
run: npm install -g npm@11
- name: Install dependencies
run: npm ci
+82
View File
@@ -0,0 +1,82 @@
{
"version": "0.2",
"language": "en",
"globRoot": "..",
"ignorePaths": [
"**/node_modules/**",
"**/dist/**",
"**/dist-ssr/**",
"**/.output/**",
"**/.wxt/**",
"**/*.svg"
],
"words": [
"activedescendant",
"adex",
"agentactivity",
"agentic",
"akldabonmimlicnjlflnapfeklbfemhj",
"aliyun",
"Bailian",
"Bento",
"BYOK",
"Chromewebstore",
"contenteditable",
"deepseek",
"executionresult",
"fieldsets",
"gaomeng",
"Googlechrome",
"Grabbable",
"Gregor",
"haspopup",
"highspeed",
"historicalevent",
"historychange",
"HITL",
"hrefs",
"IDBP",
"innerhtml",
"instructionsconfig",
"kimi",
"languagedetector",
"Lian",
"llms",
"magicui",
"maxlength",
"menuitemcheckbox",
"menuitemradio",
"modelcontextprotocol",
"multipageagent",
"nesw",
"npmmirror",
"Ollama",
"onwarn",
"opensource",
"openrouter",
"pageagent",
"pageagentcore",
"pageagentext",
"postpublish",
"qwen",
"reactid",
"reactroot",
"retryable",
"shadcn",
"sidepanel",
"spinbutton",
"staticity",
"statuschange",
"Supabase",
"toggleable",
"trivago",
"Uncapitalize",
"unparseable",
"valuenow",
"valuetext",
"WCAG",
"webgl",
"wouter",
"Zunic"
]
}
-35
View File
@@ -1,42 +1,7 @@
{
"cSpell.words": [
"agentic",
"contenteditable",
"deepseek",
"historychange",
"HITL",
"innerhtml",
"languagedetector",
"llms",
"magicui",
"npmmirror",
"onwarn",
"opensource",
"qwen",
"retryable",
"shadcn",
"sidepanel",
"statuschange",
"wouter"
],
"files.exclude": {
"packages/*/node_modules": true
},
"markdownlint.config": {
// Relaxed rules
"default": true,
"whitespace": false,
"line_length": false,
"ul-indent": false,
"no-inline-html": false,
"no-bare-urls": false,
"fenced-code-language": false,
"first-line-h1": false,
"block-spacing": false,
"blanks-around-lists": false,
"ol-prefix": false,
"no-duplicate-heading": false
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
+1
View File
@@ -132,6 +132,7 @@ const pageInfo = await this.pageController.getPageInfo()
- **Location**: co-located, `src/foo.test.ts` next to `src/foo.ts`
- **Coverage today**: `packages/llms` only — other packages will follow incrementally
- **Adding tests to a new package**: create `vitest.config.ts` in the package and add a `"test": "vitest run"` script. Root `npm test` and `node scripts/ci.js` pick it up through npm workspaces.
- **Live tests** (hit real external APIs, slow/costly): name them `*.live.test.ts`, exclude them from the package's `test` script, and expose them via a `test:live` script. Root `npm run test:live` runs all of them; they never run in `npm test` or CI. Template: `packages/llms`.
- **Template**: See @page-agent/llms
```bash
+27 -25
View File
@@ -1,13 +1,22 @@
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
<source media="(prefers-color-scheme: dark)" srcset="https://page-agent.github.io/assets/readme/banner-dark.png">
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![CI](https://img.shields.io/github/actions/workflow/status/alibaba/page-agent/main-ci.yml?branch=main&style=flat-square&label=ci)](https://github.com/alibaba/page-agent/actions/workflows/main-ci.yml)
[![npm](https://img.shields.io/npm/v/page-agent?style=flat-square&label=npm)](https://www.npmjs.com/package/page-agent)
[![downloads](https://img.shields.io/npm/dt/page-agent?style=flat-square)](https://www.npmjs.com/package/page-agent)
[![size](https://img.shields.io/bundlephobia/minzip/page-agent?style=flat-square&label=size)](https://bundlephobia.com/package/page-agent)
[![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://opensource.org/licenses/MIT)
[![typescript](https://img.shields.io/badge/%3C%2F%3E-typescript-blue?style=flat-square)](http://www.typescriptlang.org/)
[![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/rating/akldabonmimlicnjlflnapfeklbfemhj?style=flat-square&label=chrome%20rating)](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj)
[![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
The GUI Agent Living in Your Webpage. Control web interfaces with natural language.
The GUI Agent Living in Your Webpage. One script gives any web page its own AI agent.
<a href="https://trendshift.io/repositories/22551?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-22551" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/22551" alt="alibaba%2Fpage-agent | Trendshift" width="180"/></a>
🌐 **English** | [中文](./docs/README-zh.md)
@@ -15,7 +24,7 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
<!-- demo video -->
https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
[![Watch the demo](https://page-agent.github.io/assets/readme/poster.jpg)](https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2)
---
@@ -27,6 +36,7 @@ https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
- **📖 Text-based DOM manipulation**
- No screenshots. No multi-modal LLMs or special permissions needed.
- **🧠 Bring your own LLMs**
- Works with most mainstream models, including locally deployed ones. See [supported models](https://alibaba.github.io/page-agent/docs/features/models).
- **🐙 Optional [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension) for multi-page tasks.**
- And an [MCP Server (Beta)](https://alibaba.github.io/page-agent/docs/features/mcp-server) to control it from outside
@@ -35,7 +45,7 @@ https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
- **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
- **Multi-page Agent** — Extend your own web agent's reach across browser tabs [chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
- **Multi-page Agent** — Extend your own web agent's reach across browser tabs via the [Chrome extension](https://alibaba.github.io/page-agent/docs/features/chrome-extension).
- **MCP** - Allow your agent clients to control your browser.
## 🚀 Quick Start
@@ -45,17 +55,19 @@ https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
Fastest way to try PageAgent with our free Demo LLM:
```html
<script src="{URL}" crossorigin="true"></script>
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.12.1/dist/iife/page-agent.demo.js"
crossorigin="anonymous"
></script>
```
> **⚠️ For technical evaluation only.** This demo CDN uses our free [testing LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api). By using it, you agree to its [terms](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md).
> Add `?autoInit=false` to load the script without creating the demo agent automatically. You can then instantiate it with `new window.PageAgent(...)`.
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.9.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.9.1/files/dist/iife/page-agent.demo.js |
Add `?autoInit=false` to load the script without creating the demo agent automatically. You can then instantiate it with `new window.PageAgent(...)`.
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.12.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.12.1/files/dist/iife/page-agent.demo.js |
### NPM Installation
@@ -78,20 +90,12 @@ await agent.execute('Click the login button')
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
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.
Built something cool with PageAgent? Share it in [Show and Tell](https://github.com/alibaba/page-agent/discussions/categories/show-and-tell). 🙌
Please read the [maintainer's note](https://github.com/alibaba/page-agent/issues/349) on principles and current state.
Contributions generated entirely by **bots or AI** without substantial human involvement will **not be accepted**.
@@ -118,6 +122,4 @@ excellent work on web automation and DOM interaction patterns that helped make
this project possible.
```
---
**⭐ Star this repo if you find PageAgent helpful!**
+46
View File
@@ -5,6 +5,52 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.12.0] - 2026-07-09
- **Stateless extension tab sync** - TabsController pulls tab state on demand instead of long-lived ports, so the MV3 service worker stays stateless and survives idle kills without stalling the side panel.
- **Tab status in agent context** - Tab summaries now include each tab's loading status for the LLM.
## [1.11.0] - 2026-07-03
### Features
- **LLM model patching rework** - Rewrote per-model request patching for GPT, Claude, Qwen, Gemini, and DeepSeek, and refreshed the recommended model list.
### Improvements
- **Deprecate `LLMConfig.temperature`** - Warn when set; use `transformRequestBody` for models that still accept it.
- **Extension limitations docs** - Added a limitations section to extension documentation.
### Bug Fixes
- **Multi-window extension tabs** - Resolved the active tab/window from the caller's window context instead of the global active tab.
- **chat-latest model compatibility** - Skip `reasoning_effort` and `temperature` patches for `*-chat-latest` models.
- **OpenRouter defaults** - Do not enable reasoning by default on OpenRouter.
- **Asset URLs** - Migrated `img.alicdn.com` links to `page-agent.github.io`.
## [1.10.0] - 2026-06-15
### Breaking Changes
- **Agent run lifecycle rework** - `stop()` is now async and resolves only after the run fully settles. Run status is decoupled from task outcome: a new `stopped` state was added, and LLM self-reported failures now end as `completed`. Lifecycle hooks re-throw instead of folding errors into the result, agent errors are recorded in history, and `agent.lastResult` was added.
### Features
- **Abortable JavaScript execution** - `execute_javascript` now honors the `AbortSignal`.
- **Leaner agent prompts** - Simplified the waiting-response flow and removed navigation-back instructions to reduce LLM cognitive load.
- **MultiPageAgent safety** - Disabled `ScriptExecutionTool` for `MultiPageAgent`.
### Improvements
- **Dark mode detection** - Refined detection heuristics and made `isMainContentDark` less aggressive by checking `html` and `body` data attributes independently.
- **Extension lifecycle robustness** - Drove heartbeat and running state from status changes, cleared stale activity on any non-running status, handled the stopped lifecycle state, and cleared `currentTabId` on `TabsController.init`.
### Bug Fixes
- **Accurate wait reporting** - Wait steps now report the actual wait duration.
- **Scroll predicates** - Scroll predicates now return booleans.
- **Docs** - Fixed the broken demo video on GitHub.
## [1.9.0] - 2026-06-08
### Features
+25 -13
View File
@@ -1,21 +1,30 @@
# Page Agent
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://img.alicdn.com/imgextra/i4/O1CN01qKig1P1FnhpFKNdi6_!!6000000000532-2-tps-1280-256.png">
<img alt="Page Agent Banner" src="https://img.alicdn.com/imgextra/i1/O1CN01NCMKXj1Gn4tkFTsxf_!!6000000000666-2-tps-1280-256.png">
<source media="(prefers-color-scheme: dark)" srcset="https://page-agent.github.io/assets/readme/banner-dark.png">
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture>
[![License: MIT](https://img.shields.io/badge/License-MIT-auto.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/page-agent)](https://bundlephobia.com/package/page-agent) [![Downloads](https://img.shields.io/npm/dt/page-agent.svg)](https://www.npmjs.com/package/page-agent) [![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
[![CI](https://img.shields.io/github/actions/workflow/status/alibaba/page-agent/main-ci.yml?branch=main&style=flat-square&label=ci)](https://github.com/alibaba/page-agent/actions/workflows/main-ci.yml)
[![npm](https://img.shields.io/npm/v/page-agent?style=flat-square&label=npm)](https://www.npmjs.com/package/page-agent)
[![downloads](https://img.shields.io/npm/dt/page-agent?style=flat-square)](https://www.npmjs.com/package/page-agent)
[![size](https://img.shields.io/bundlephobia/minzip/page-agent?style=flat-square&label=size)](https://bundlephobia.com/package/page-agent)
[![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](https://opensource.org/licenses/MIT)
[![typescript](https://img.shields.io/badge/%3C%2F%3E-typescript-blue?style=flat-square)](http://www.typescriptlang.org/)
[![Chrome Web Store Rating](https://img.shields.io/chrome-web-store/rating/akldabonmimlicnjlflnapfeklbfemhj?style=flat-square&label=chrome%20rating)](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj)
[![GitHub stars](https://img.shields.io/github/stars/alibaba/page-agent.svg)](https://github.com/alibaba/page-agent)
纯 JS 实现的 GUI agent。使用自然语言操作你的 Web 应用。无须后端、客户端、浏览器插件。
<a href="https://trendshift.io/repositories/22551?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-22551" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/22551" alt="alibaba%2Fpage-agent | Trendshift" width="180"/></a>
🌐 [English](../README.md) | **中文**
<a href="https://alibaba.github.io/page-agent/" target="_blank"><b>🚀 Demo</b></a> | <a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank"><b>📖 Docs</b></a> | <a href="https://news.ycombinator.com/item?id=47264138" target="_blank"><b>📢 HN Discussion</b></a> | <a href="https://x.com/simonluvramen" target="_blank"><b>𝕏 Follow on X</b></a>
<!-- demo video -->
https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
[![Watch the demo](https://page-agent.github.io/assets/readme/poster.jpg)](https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2)
---
@@ -26,6 +35,7 @@ https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
- **📖 基于文本的 DOM 操作**
- 无需截图,无需多模态模型或特殊权限
- **🧠 自备 LLM**
- 支持多数主流模型,包括本地部署模型。参见[支持的模型](https://alibaba.github.io/page-agent/docs/features/models)。
- 🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务
- [MCP Server (Beta)](https://alibaba.github.io/page-agent/docs/features/mcp-server)
@@ -44,17 +54,19 @@ https://github.com/user-attachments/assets/a1f2eae2-13fb-4aae-98cf-a3fc1620a6c2
通过我们免费的 Demo LLM 快速体验 PageAgent
```html
<script src="{URL}" crossorigin="true"></script>
<script
src="https://cdn.jsdelivr.net/npm/page-agent@1.12.1/dist/iife/page-agent.demo.js"
crossorigin="anonymous"
></script>
```
> **⚠️ 仅用于技术评估。** 该 Demo CDN 使用了免费的[测试 LLM API](https://alibaba.github.io/page-agent/docs/features/models#free-testing-api),使用即表示您同意其[条款](https://github.com/alibaba/page-agent/blob/main/docs/terms-and-privacy.md)。
> 在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
| Mirrors | URL |
| ------- | ---------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.9.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.9.1/files/dist/iife/page-agent.demo.js |
在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
| Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.12.1/dist/iife/page-agent.demo.js |
| China | https://registry.npmmirror.com/page-agent/1.12.1/files/dist/iife/page-agent.demo.js |
### NPM 安装
@@ -85,6 +97,8 @@ await agent.execute('点击登录按钮')
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码。
用 PageAgent 做了有趣的东西?欢迎到 [Show and Tell](https://github.com/alibaba/page-agent/discussions/categories/show-and-tell) 分享。🙌
## 👏 声明与致谢
本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
@@ -107,6 +121,4 @@ this project possible.
[MIT License](../LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
+1 -1
View File
@@ -91,7 +91,7 @@ npm run build:ext
- Add a new bookmark
```javascript
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.demo.js?t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
javascript:(function(){var s=document.createElement('script');s.src=`http://localhost:5174/page-agent.demo.js?lang=en-US&t=${Math.random()}`;s.onload=()=>console.log(%27PageAgent ready!%27);document.head.appendChild(s);})();
```
- Click the bookmark on any page to load Page-Agent
+1
View File
@@ -53,6 +53,7 @@ export default defineConfig([
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-deprecated': 'off',
'@eslint-react/dom-no-missing-button-type': 'off',
'@eslint-react/no-nested-component-definitions': 'off',
'@eslint-react/no-array-index-key': 'off',
+1338 -1272
View File
File diff suppressed because it is too large Load Diff
+22 -23
View File
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"workspaces": [
"packages/page-controller",
@@ -37,44 +37,40 @@
"postpublish": "npm run postpublish --workspaces --if-present",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
"test": "npm test --workspaces --if-present",
"test:live": "npm run test:live --workspaces --if-present",
"lint": "eslint .",
"ci": "node scripts/ci.js",
"cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
"prepare": "husky || true"
},
"devDependencies": {
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint-react/eslint-plugin": "^5.8.16",
"@commitlint/cli": "^21.2.0",
"@commitlint/config-conventional": "^21.2.0",
"@eslint-react/eslint-plugin": "^5.11.2",
"@eslint/js": "^10.0.1",
"@microsoft/api-extractor": "^7.58.8",
"@tailwindcss/vite": "^4.3.0",
"@microsoft/api-extractor": "^7.58.9",
"@tailwindcss/vite": "^4.3.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.9.2",
"@vitejs/plugin-react": "^6.0.2",
"@types/node": "^26.1.0",
"@vitejs/plugin-react": "^6.0.3",
"chalk": "^5.6.2",
"concurrently": "^10.0.3",
"dotenv": "^17.4.2",
"eslint": "^10.4.0",
"globals": "^17.6.0",
"eslint": "^10.6.0",
"globals": "^17.7.0",
"happy-dom": "^20.10.6",
"husky": "^9.1.7",
"lint-staged": "^17.0.5",
"prettier": "^3.8.3",
"lint-staged": "^17.0.8",
"prettier": "^3.9.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.0",
"unplugin-dts": "^1.0.1",
"vite": "^8.0.14",
"typescript-eslint": "^8.63.0",
"unplugin-dts": "^1.0.3",
"vite": "^8.1.3",
"vite-plugin-css-injected-by-js": "^5.0.1",
"vitest": "^4.1.8"
"vitest": "^4.1.10"
},
"overrides": {
"typescript": "^6.0.3",
"node-notifier": {
"uuid": "11.1.1"
},
"web-ext-run": {
"tmp": "0.2.7"
}
"typescript": "^6.0.3"
},
"lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [
@@ -87,6 +83,9 @@
],
"*.css": [
"npx prettier --write --ignore-unknown"
],
"*.md": [
"npx prettier --write --ignore-unknown"
]
},
"commitlint": {
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/core",
"private": false,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"main": "./src/PageAgentCore.ts",
"types": "./src/PageAgentCore.ts",
@@ -55,8 +55,8 @@
},
"dependencies": {
"chalk": "^5.6.2",
"@page-agent/llms": "1.9.1",
"@page-agent/page-controller": "1.9.1"
"@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.12.1"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
+1 -1
View File
@@ -37,7 +37,7 @@ and system messages wrapped in <sys> tag.
<user_request>
USER REQUEST: This is your ultimate objective and always remains visible.
- This has the highest priority. Make the user happy.
- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.
- If the user request is very specific - then carefully follow each step and don't skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
+1 -5
View File
@@ -255,11 +255,7 @@ export interface AgentErrorEvent {
* Union type for all history events
*/
export type HistoricalEvent =
| AgentStepEvent
| ObservationEvent
| UserTakeoverEvent
| RetryEvent
| AgentErrorEvent
AgentStepEvent | ObservationEvent | UserTakeoverEvent | RetryEvent | AgentErrorEvent
/**
* Agent lifecycle status.
+4
View File
@@ -96,6 +96,10 @@ Returns: `Promise<ExecutionResult>`
Stop the current task.
## Limitations
- **Normal browser windows only.** This extension relies on tab group API which does not work for pop-up window or PWA App window.
## Types
Install `@page-agent/core` for complete types:
+19 -19
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/ext",
"private": true,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"scripts": {
"dev": "wxt",
@@ -10,38 +10,38 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@radix-ui/react-hover-card": "^1.1.16",
"@radix-ui/react-hover-card": "^1.1.18",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.9",
"@radix-ui/react-separator": "^1.1.9",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.3.0",
"@tailwindcss/vite": "^4.3.0",
"@types/chrome": "^0.1.43",
"@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.2",
"@tailwindcss/vite": "^4.3.2",
"@types/chrome": "^0.2.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"idb": "^8.0.3",
"lucide-react": "^1.16.0",
"motion": "^12.40.0",
"lucide-react": "^1.23.0",
"motion": "^12.42.2",
"next-themes": "^0.4.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"rough-notation": "^0.5.1",
"simple-icons": "^16.23.0",
"simple-icons": "^16.25.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
"wxt": "^0.20.26"
"wxt": "^0.20.27"
},
"dependencies": {
"@page-agent/core": "1.9.1",
"@page-agent/llms": "1.9.1",
"@page-agent/page-controller": "1.9.1",
"@page-agent/ui": "1.9.1",
"@page-agent/core": "1.12.1",
"@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.12.1",
"@page-agent/ui": "1.12.1",
"ai-motion": "^0.4.8",
"chalk": "^5.6.2"
},
@@ -64,9 +64,11 @@ export class MultiPageAgent extends PageAgentCore {
},
onBeforeStep: async (agent) => {
// pull latest tab state so that tabs changes can be observed
await tabsController.syncTabs()
if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!)
await tabsController.waitUntilTabLoaded(tabsController.currentTabId)
},
onDispose: () => {
@@ -1,5 +1,9 @@
/**
* background logics for TabsController
*
* Keep this stateless: pure request/response handlers only, no in-memory
* state, no ports, no event pushing. MV3 SW should be killed and restarted at
* any time (idle timeout, extension update) without special handling.
*/
import type { TabAction } from './TabsController'
@@ -7,6 +11,39 @@ const PREFIX = '[TabsController.background]'
const debug = console.debug.bind(console, `\x1b[90m${PREFIX}\x1b[0m`)
/**
* Resolve active tab.
*
* - `tabs.query({ active: true })` does not work in multi-window scenarios.
* - Extension pages (side panel, hub tab) can resolve their own windowId.
* We just find the active tab within that window.
* - Content scripts (PAGE_AGENT_EXT) can't self-report a windowId.
* Chrome populates `sender.tab` for every content-script message,
* which is the tab hosting the script.
*/
async function resolveActiveTab(
payload: { windowId?: number } | undefined,
sender: chrome.runtime.MessageSender
): Promise<chrome.tabs.Tab> {
const windowId = payload?.windowId
if (windowId != null) {
debug('get_active_tab: resolving via caller-reported windowId', windowId)
const [tab] = await chrome.tabs.query({ active: true, windowId })
if (!tab) throw new Error(`No active tab found in window ${windowId}.`)
return tab
}
if (sender.tab) {
debug('get_active_tab: resolving via sender.tab (content script)', sender.tab.id)
return sender.tab
}
throw new Error(
'Cannot resolve active tab: caller reported no windowId and is not a content script (no sender.tab).'
)
}
export function handleTabControlMessage(
message: { type: 'TAB_CONTROL'; action: TabAction; payload: any },
sender: chrome.runtime.MessageSender,
@@ -16,12 +53,11 @@ export function handleTabControlMessage(
switch (action as TabAction) {
case 'get_active_tab': {
debug('get_active_tab')
chrome.tabs
.query({ active: true })
.then((tabs) => {
debug('get_active_tab: success', tabs)
sendResponse({ success: true, tab: tabs[0] })
debug('get_active_tab', payload)
resolveActiveTab(payload, sender)
.then((tab) => {
debug('get_active_tab: success', tab)
sendResponse({ success: true, tab })
})
.catch((error) => {
sendResponse({ error: error instanceof Error ? error.message : String(error) })
@@ -46,7 +82,7 @@ export function handleTabControlMessage(
case 'open_new_tab': {
debug('open_new_tab', payload)
chrome.tabs
.create({ url: payload.url, active: false })
.create({ url: payload.url, windowId: payload.windowId, active: false })
.then((newTab) => {
debug('open_new_tab: success', newTab)
sendResponse({ success: true, tabId: newTab.id })
@@ -112,7 +148,6 @@ export function handleTabControlMessage(
}
case 'get_window_tabs': {
debug('get_window_tabs', payload)
chrome.tabs
.query({ windowId: payload.windowId })
.then((tabs) => {
@@ -129,41 +164,3 @@ export function handleTabControlMessage(
return
}
}
const tabEventPorts = new Set<chrome.runtime.Port>()
function broadcastTabEvent(message: object) {
for (const port of tabEventPorts) {
port.postMessage(message)
}
}
/**
* Port-based tab events: agents connect via `chrome.runtime.connect({ name: 'tab-events' })`
* and receive tab change events through the port. Works for both extension pages and content scripts.
*/
export function setupTabEventsPort() {
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'tab-events') return
debug('port connected', port.sender?.tab?.id ?? port.sender?.url)
tabEventPorts.add(port)
port.onDisconnect.addListener(() => {
debug('port disconnected')
tabEventPorts.delete(port)
})
})
chrome.tabs.onCreated.addListener((tab) => {
broadcastTabEvent({ action: 'created', payload: { tab } })
})
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
broadcastTabEvent({ action: 'removed', payload: { tabId, removeInfo } })
})
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
broadcastTabEvent({ action: 'updated', payload: { tabId, changeInfo, tab } })
})
}
+114 -79
View File
@@ -15,19 +15,34 @@ function sendMessage(message: {
})
}
/**
* Resolve the window hosting this script's own context, when knowable.
*
* Extension pages (side panel, hub tab) have `chrome.windows` access and can
* identify their own window directly via `getCurrent()`.
* Content scripts have no `chrome.windows` access; they resolve `undefined`
* here and the background script falls back to `sender.tab` instead.
*/
async function getOwnWindowId(): Promise<number | undefined> {
if (typeof chrome.windows === 'undefined') return undefined
const win = await chrome.windows.getCurrent()
return win.id
}
/**
* Controller for managing browser tabs.
* - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations
* - store tabs states, pull tabs info and detect changes
*/
export class TabsController {
currentTabId: number | null = null
private disposed = false
private port?: chrome.runtime.Port
private portRetries = 0
/* tracked window */
private windowId: number | null = null
/* tracked tabs */
private tabs: TabMeta[] = []
private initialTabId: number | null = null
private tabGroupId: number | null = null
@@ -43,9 +58,6 @@ export class TabsController {
}
await this.updateCurrentTabId(null)
this.disposed = false
this.port = undefined
this.portRetries = 0
this.windowId = null
this.tabs = []
@@ -57,6 +69,7 @@ export class TabsController {
const activeTabResult = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_active_tab',
payload: { windowId: await getOwnWindowId() },
})
this.initialTabId = activeTabResult.tab?.id
@@ -70,8 +83,6 @@ export class TabsController {
}
}
this.connectTabEvents()
if (experimentalIncludeAllTabs) {
const allTabs = await sendMessage({
type: 'TAB_CONTROL',
@@ -124,7 +135,7 @@ export class TabsController {
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'open_new_tab',
payload: { url },
payload: { url, windowId: this.windowId },
})
if (!result.success) {
@@ -265,11 +276,14 @@ export class TabsController {
}
async summarizeTabs(): Promise<string> {
const summaries = [`| Tab ID | URL | Title | Current |`, `|-----|-----|-----|-----|`]
const summaries = [
`| Tab ID | URL | Title | Status | Current |`,
`|-----|-----|-----|-----|-----|`,
]
for (const tab of this.tabs) {
const { title, url } = await this.getTabInfo(tab.id)
summaries.push(
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |`
`| ${tab.id} | ${url} | ${title} | ${tab.status ?? '-'} | ${this.currentTabId === tab.id ? '✅' : ''} |`
)
}
if (!this.tabs.length) {
@@ -282,79 +296,96 @@ export class TabsController {
async waitUntilTabLoaded(tabId: number): Promise<void> {
const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`)
if (tab.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
if (tab.status === 'complete') return
// When a tracked tab is closed or untracked.
// The tab object will be removed from the tab list.
// Finding the latest tab object is the only way to know if it's closed.
debug('waitUntilTabLoaded', tabId)
await waitUntil(() => tab.status === 'complete', 4_000)
await waitUntil(async () => {
await this.syncTabs()
const latest = this.tabs.find((t) => t.id === tabId)
return !latest || latest.status !== 'loading'
}, 4_000)
const latest = this.tabs.find((t) => t.id === tabId)
if (latest?.status === 'unloaded') throw new Error(`Tab ID ${tabId} is unloaded.`)
}
/**
* Connect to background SW via port to receive tab change events.
*
* @note Port is 1:1 (runtime.connect → background SW has no frames),
* so onDisconnect fires exactly once and we can safely reconnect.
* Reconnection may miss events during the gap.
* TODO: refresh this.tabs from background after reconnect to stay consistent.
* Pull the window's tabs from the background.
* Pulling is better than pushing. Long-lived ports are stateful troublemakers.
*/
private connectTabEvents() {
this.port = chrome.runtime.connect({ name: 'tab-events' })
async syncTabs(): Promise<void> {
if (this.disposed || this.windowId == null) return
this.port.onMessage.addListener((message: any) => {
if (this.disposed) return
this.portRetries = 0
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
const shouldTrack = this.experimentalIncludeAllTabs || tab.groupId === this.tabGroupId
if (shouldTrack && tab.id != null) {
this.addTab({ id: tab.id, isInitial: false })
this.switchToTab(tab.id)
}
} else if (message.action === 'removed') {
const { tabId } = message.payload as { tabId: number }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
this.tabs = this.tabs.filter((t) => t.id !== tabId)
if (this.currentTabId === tabId) {
const newCurrentTab = this.tabs[this.tabs.length - 1] || null
if (newCurrentTab) {
this.switchToTab(newCurrentTab.id)
} else {
this.updateCurrentTabId(null)
}
}
}
} else if (message.action === 'updated') {
const { tabId, tab } = message.payload as { tabId: number; tab: chrome.tabs.Tab }
const targetTab = this.tabs.find((t) => t.id === tabId)
if (targetTab) {
targetTab.url = tab.url
targetTab.title = tab.title
targetTab.status = tab.status
}
}
const result = await sendMessage({
type: 'TAB_CONTROL',
action: 'get_window_tabs',
payload: { windowId: this.windowId },
})
// sendMessage already logged the failure; keep the stale mirror
if (!result?.success) return
this.port.onDisconnect.addListener(() => {
this.port = undefined
if (this.disposed) return
if (this.portRetries >= 7) {
console.error(PREFIX, 'tab events port failed after 7 retries, giving up')
return
const liveTabs = (result.tabs as chrome.tabs.Tab[]).filter((t) => t.id != null)
const liveIds = new Set(liveTabs.map((t) => t.id!))
const closedIds = this.tabs.filter((t) => !liveIds.has(t.id)).map((t) => t.id)
if (closedIds.length) {
debug('syncTabs: tabs closed', closedIds)
this.tabs = this.tabs.filter((t) => liveIds.has(t.id))
}
const newTabs: TabMeta[] = []
for (const live of liveTabs) {
const tracked = this.tabs.find((t) => t.id === live.id)
if (tracked) {
tracked.url = live.url
tracked.title = live.title
tracked.status = live.status as TabMeta['status']
} else if (this.shouldTrack(live)) {
debug('syncTabs: new tab', live.id, live.url)
const meta: TabMeta = {
id: live.id!,
isInitial: false,
url: live.url,
title: live.title,
status: live.status,
}
this.addTab(meta)
newTabs.push(meta)
}
debug('port disconnected, reconnecting...')
this.portRetries++
this.connectTabEvents()
})
}
// Follow the page like a user would: focus the newest tab it opened.
// If the current tab is gone, fall back to the last tracked one.
if (newTabs.length) {
await this.switchToTab(newTabs[newTabs.length - 1].id)
} else if (this.currentTabId != null && !this.tabs.find((t) => t.id === this.currentTabId)) {
const fallback = this.tabs[this.tabs.length - 1]
if (fallback) {
await this.switchToTab(fallback.id)
} else {
debug('syncTabs: no fallback tab found, updating current tab to null')
await this.updateCurrentTabId(null)
}
}
}
private shouldTrack(tab: chrome.tabs.Tab): boolean {
if (this.tabGroupId != null && tab.groupId === this.tabGroupId) return true
return (
this.experimentalIncludeAllTabs &&
tab.windowId === this.windowId &&
!tab.pinned &&
isContentScriptAllowed(tab.url)
)
}
dispose() {
debug('dispose')
this.disposed = true
this.port?.disconnect()
this.port = undefined
}
}
@@ -392,29 +423,33 @@ function randomColor(): TabGroupColor {
/**
* Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout
* @param error Error object to reject on timeout. If not provided, will resolve with false
* @returns Returns when condition becomes true, false if timeout
* @param timeoutMS Timeout in milliseconds, default 1 minutes
* @param throwIfTimeout Reject on timeout instead of resolving with `false`
*/
export async function waitUntil(
async function waitUntil(
check: () => boolean | Promise<boolean>,
timeoutMS = 60_000,
error?: string
throwIfTimeout = false
): Promise<boolean> {
if (await check()) return true
return new Promise((resolve, reject) => {
const start = Date.now()
const poll = async () => {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (error) {
return reject(new Error(error))
} else {
return resolve(false)
try {
if (await check()) return resolve(true)
if (Date.now() - start > timeoutMS) {
if (throwIfTimeout) {
return reject(new Error(`waitUntil timed out after ${timeoutMS}ms`))
} else {
return resolve(false)
}
}
setTimeout(poll, 100)
} catch (err) {
reject(err instanceof Error ? err : new Error(String(err)))
}
setTimeout(poll, 100)
}
setTimeout(poll, 100)
})
@@ -37,7 +37,7 @@ and system messages wrapped in <sys> tag.
<user_request>
USER REQUEST: This is your ultimate objective and always remains visible.
- This has the highest priority. Make the user happy.
- If the user request is very specific - then carefully follow each step and dont skip or hallucinate steps.
- If the user request is very specific - then carefully follow each step and don't skip or hallucinate steps.
- If the task is open ended you can plan yourself how to get it done.
</user_request>
@@ -1,13 +1,9 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage, setupTabEventsPort } from '@/agent/TabsController.background'
import { handleTabControlMessage } from '@/agent/TabsController.background'
export default defineBackground(() => {
console.log('[Background] Service Worker started')
// tab change events
setupTabEventsPort()
// generate user auth token
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/llms",
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
@@ -43,7 +43,8 @@
"homepage": "https://alibaba.github.io/page-agent/",
"scripts": {
"build": "vite build",
"test": "vitest run",
"test": "vitest run --project llms",
"test:live": "vitest run --project llms:live",
"prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js"
},
+16 -5
View File
@@ -4,17 +4,24 @@
import * as z from 'zod/v4'
import { InvokeError, InvokeErrorTypes } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import type {
InvokeOptions,
InvokeResult,
LLMClient,
Message,
ResolvedLLMConfig,
Tool,
} from './types'
import { modelPatch, zodToOpenAITool } from './utils'
/**
* Client for OpenAI compatible APIs
*/
export class OpenAIClient implements LLMClient {
config: Required<LLMConfig>
config: ResolvedLLMConfig
private fetch: typeof globalThis.fetch
constructor(config: Required<LLMConfig>) {
constructor(config: ResolvedLLMConfig) {
this.config = config
this.fetch = config.customFetch
}
@@ -39,14 +46,18 @@ export class OpenAIClient implements LLMClient {
const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
tool_choice: toolChoice,
}
// Only sent if the caller explicitly set it. Most new models throw if this is set.
if (this.config.temperature !== undefined) {
requestBody.temperature = this.config.temperature
}
modelPatch(requestBody, this.config.baseURL)
modelPatch(requestBody)
let transformedBody: Record<string, unknown> | undefined
try {
transformedBody = this.config.transformRequestBody(requestBody)
-3
View File
@@ -1,3 +0,0 @@
// Internal constants
export const LLM_MAX_RETRIES = 2
export const DEFAULT_TEMPERATURE = 0.7 // higher randomness helps auto-recovery
+41 -24
View File
@@ -1,34 +1,23 @@
import { OpenAIClient } from './OpenAIClient'
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
import { InvokeError, InvokeErrorTypes } from './errors'
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
import type {
InvokeOptions,
InvokeResult,
LLMClient,
LLMConfig,
Message,
ResolvedLLMConfig,
Tool,
} from './types'
export { InvokeError, InvokeErrorTypes }
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models'
)
}
return {
baseURL: config.baseURL,
model: config.model,
apiKey: config.apiKey || '',
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
/**
* LLM module
*/
export class LLM extends EventTarget {
config: Required<LLMConfig>
config: ResolvedLLMConfig
client: LLMClient
constructor(config: LLMConfig) {
@@ -90,3 +79,31 @@ async function withRetry<T>(
}
}
}
export function parseLLMConfig(config: LLMConfig): ResolvedLLMConfig {
// Runtime validation as defensive programming (types already guarantee these)
if (!config.baseURL || !config.model) {
throw new Error(
'[PageAgent] LLM configuration required. Please provide: baseURL, model. ' +
'See: https://alibaba.github.io/page-agent/docs/features/models'
)
}
if (config.temperature !== undefined) {
console.warn(
'[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. ' +
'Use transformRequestBody to set it only for models you have verified accept it.'
)
}
return {
baseURL: config.baseURL,
model: config.model,
apiKey: config.apiKey || '',
temperature: config.temperature,
maxRetries: config.maxRetries ?? 2,
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
+230
View File
@@ -0,0 +1,230 @@
/**
* Live compatibility test hits real provider APIs.
*
* Purpose: verify request formatting and `modelPatch` (see `utils.ts`) work
* for every model below, across the providers that serve them. Not a
* correctness/quality eval the tool call is trivial and forced via
* `toolChoiceName`, so a failure here means the request/response shape is
* wrong for that model, not that the model is "dumb".
*
* Tests `OpenAIClient` directly (not the `LLM` retry wrapper), so a failure
* always reflects the very first request/response no retry can mask it.
*
* Per the `*.live.test.ts` convention this suite is excluded from `npm test`
* (slow, costs tokens) run it manually with `npm run test:live`. Each
* provider's tests skip (not fail) when its `TESTING_*_KEY` env var is
* absent. Keys live in the repo-root `.env`:
*
* TESTING_OPENROUTER_KEY=...
* TESTING_DEEPSEEK_KEY=...
* TESTING_ALIYUN_KEY=...
*/
import { config as dotenvConfig } from 'dotenv'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import { describe, expect, it } from 'vitest'
import * as z from 'zod/v4'
import { OpenAIClient } from './OpenAIClient'
import { parseLLMConfig } from './index'
import type { Message, Tool } from './types'
const __dirname = dirname(fileURLToPath(import.meta.url))
dotenvConfig({ path: resolve(__dirname, '../../../.env'), quiet: true })
const TEST_TIMEOUT = 30_000
/**
* Mirrors `packages/website/src/pages/docs/features/models/page.tsx`.
* This package cannot depend on the website, so the list is duplicated here.
* Keep both lists in sync manually when models are added or renamed.
*/
const MODEL_GROUPS: Record<string, string[]> = {
Qwen: [
'qwen3.7-max',
'qwen3.7-plus',
'qwen3.6-max',
'qwen3.6-plus',
'qwen3.6-flash',
'qwen3.5-plus',
'qwen3.5-flash',
'qwen3-max',
],
OpenAI: [
'gpt-5.6-sol',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5.5',
'gpt-5.4',
'gpt-5.4-mini',
'gpt-5.4-nano',
'gpt-5.2',
'gpt-5.1',
'gpt-5',
'gpt-5-mini',
'gpt-4.1',
'gpt-4.1-mini',
],
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
Google: [
'gemini-3.5-flash',
'gemini-3.1-pro',
'gemini-3.1-flash-lite',
'gemini-2.5-pro',
'gemini-2.5-flash',
],
Anthropic: [
'claude-sonnet-5',
'claude-opus-4-8',
'claude-opus-4-7',
'claude-opus-4-6',
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
],
MiniMax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'],
xAI: ['grok-4.5', 'grok-4.3', 'grok-build-0.1'],
Tencent: ['hy3'],
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
}
/**
* OpenRouter lists every model as `<vendor-slug>/<model-id>`, lowercase.
* See the commented-out entries in the repo-root `.env` for real examples,
* e.g. `x-ai/grok-4.1-fast`, `qwen/qwen3-coder-next`, `deepseek/deepseek-v3.2-exp`.
*/
const OPENROUTER_VENDOR_SLUG: Record<string, string> = {
Qwen: 'qwen',
OpenAI: 'openai',
DeepSeek: 'deepseek',
Google: 'google',
Anthropic: 'anthropic',
MiniMax: 'minimax',
xAI: 'x-ai',
Tencent: 'tencent',
MoonshotAI: 'moonshotai',
'Z.AI': 'z-ai',
}
/**
* Overrides for models whose OpenRouter id doesn't match the
* `<vendor-slug>/<lowercased-name>` heuristic dated snapshots, "-preview"
* suffixes, "v"-prefixed versions, or dots instead of hyphens in the
* version number. Verified against `GET https://openrouter.ai/api/v1/models`
* on 2026-07-10; re-check when models are added to `MODEL_GROUPS`.
*/
const OPENROUTER_ID_OVERRIDES: Record<string, string> = {
'qwen3.6-max': 'qwen/qwen3.6-max-preview',
'qwen3.5-plus': 'qwen/qwen3.5-plus-20260420',
'qwen3.5-flash': 'qwen/qwen3.5-flash-02-23',
'deepseek-3.2': 'deepseek/deepseek-v3.2',
'gemini-3.1-pro': 'google/gemini-3.1-pro-preview',
'claude-opus-4-8': 'anthropic/claude-opus-4.8',
'claude-opus-4-7': 'anthropic/claude-opus-4.7',
'claude-opus-4-6': 'anthropic/claude-opus-4.6',
'claude-opus-4-5': 'anthropic/claude-opus-4.5',
'claude-sonnet-4-5': 'anthropic/claude-sonnet-4.5',
'claude-haiku-4-5': 'anthropic/claude-haiku-4.5',
}
/**
* Derive a model's OpenRouter id from its brand (a `MODEL_GROUPS` key) and
* native model name.
*/
function toOpenRouterModelId(brand: string, model: string): string {
if (model in OPENROUTER_ID_OVERRIDES) return OPENROUTER_ID_OVERRIDES[model]
const slug = OPENROUTER_VENDOR_SLUG[brand]
if (!slug) throw new Error(`No OpenRouter vendor slug mapped for brand "${brand}"`)
return `${slug}/${model.toLowerCase()}`
}
const PROVIDERS = {
openrouter: {
baseURL: 'https://openrouter.ai/api/v1',
apiKey: process.env.TESTING_OPENROUTER_KEY,
},
aliyun: {
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: process.env.TESTING_ALIYUN_KEY,
},
deepseek: {
baseURL: 'https://api.deepseek.com',
apiKey: process.env.TESTING_DEEPSEEK_KEY,
},
} as const
const ECHO_TOOL: Tool<{ message: string }, string> = {
description: 'Echo back the given message in uppercase.',
inputSchema: z.object({ message: z.string() }),
execute: async ({ message }) => message.toUpperCase(),
}
const PROMPT: Message[] = [
{
role: 'user',
content: 'Call the "echo" tool with message set to "ping". You must call the tool.',
},
]
async function expectEchoToolCall(baseURL: string, apiKey: string, model: string) {
const client = new OpenAIClient(parseLLMConfig({ baseURL, apiKey, model }))
const result = await client.invoke(PROMPT, { echo: ECHO_TOOL }, new AbortController().signal, {
toolChoiceName: 'echo',
})
expect(result.toolResult).toBe('PING')
}
describe.concurrent('OpenRouter — all listed models', () => {
const { baseURL, apiKey } = PROVIDERS.openrouter
for (const [brand, models] of Object.entries(MODEL_GROUPS)) {
for (const model of models) {
const id = toOpenRouterModelId(brand, model)
it.skipIf(!apiKey)(
id,
async () => {
await expectEchoToolCall(baseURL, apiKey!, id)
},
TEST_TIMEOUT
)
}
}
})
// Aliyun native ids that don't match the display name in MODEL_GROUPS.
const ALIYUN_ID_OVERRIDES: Record<string, string> = {
'qwen3.6-max': 'qwen3.6-max-preview',
}
describe.concurrent('Aliyun DashScope — Qwen native', () => {
const { baseURL, apiKey } = PROVIDERS.aliyun
for (const model of MODEL_GROUPS.Qwen) {
const id = ALIYUN_ID_OVERRIDES[model] ?? model
it.skipIf(!apiKey)(
id,
async () => {
await expectEchoToolCall(baseURL, apiKey!, id)
},
TEST_TIMEOUT
)
}
})
describe.concurrent('DeepSeek — native', () => {
const { baseURL, apiKey } = PROVIDERS.deepseek
// deepseek-3.2 isn't served on DeepSeek's own API (only via OpenRouter
// resellers) — its official API only accepts deepseek-v4-pro/-flash.
const nativeModels = MODEL_GROUPS.DeepSeek.filter((model) => model !== 'deepseek-3.2')
for (const model of nativeModels) {
it.skipIf(!apiKey)(
model,
async () => {
await expectEchoToolCall(baseURL, apiKey!, model)
},
TEST_TIMEOUT
)
}
})
+7
View File
@@ -92,7 +92,12 @@ export interface LLMConfig {
model: string
apiKey?: string
/**
* @deprecated No longer a standard parameter; many models reject it outright.
* Use `transformRequestBody` to set it only for models you've verified.
*/
temperature?: number
maxRetries?: number
/**
@@ -118,3 +123,5 @@ export interface LLMConfig {
*/
customFetch?: typeof globalThis.fetch
}
export type ResolvedLLMConfig = Required<Omit<LLMConfig, 'temperature'>> & { temperature?: number }
+10 -157
View File
@@ -1,162 +1,15 @@
import { describe, expect, it } from 'vitest'
import { modelPatch } from './utils'
import { modelPatch, normalizeModelName } from './utils'
/**
* Baseline request body used as starting point for each provider test.
* Mirrors what OpenAIClient builds before calling modelPatch.
*/
function baseBody(model: string) {
return {
model,
temperature: 0.7,
messages: [],
tools: [],
parallel_tool_calls: false,
tool_choice: 'required' as unknown,
}
}
describe('modelPatch', () => {
it('returns body unchanged when model is missing', () => {
const body = { temperature: 0.7 }
expect(modelPatch(body)).toBe(body)
expect(body).toEqual({ temperature: 0.7 })
})
it('qwen: bumps temperature and disables thinking', () => {
const body = baseBody('qwen-max')
modelPatch(body)
expect(body.temperature).toBe(1.0)
expect(body).toMatchObject({ enable_thinking: false })
})
it('qwen: keeps higher caller-provided temperature', () => {
const body = baseBody('qwen-max')
body.temperature = 1.5
modelPatch(body)
expect(body.temperature).toBe(1.5)
})
it('claude: disables thinking and converts tool_choice "required" -> { type: "any" }', () => {
const body = baseBody('claude-3-5-sonnet')
modelPatch(body)
expect(body).toMatchObject({
thinking: { type: 'disabled' },
tool_choice: { type: 'any' },
})
})
it('claude: converts named tool_choice to { type: "tool", name }', () => {
const body = baseBody('claude-3-5-sonnet')
body.tool_choice = { type: 'function', function: { name: 'doStuff' } }
modelPatch(body)
expect(body.tool_choice).toEqual({ type: 'tool', name: 'doStuff' })
})
it('claude-opus-4-7: drops temperature', () => {
const body = baseBody('claude-opus-4-7')
modelPatch(body)
expect(body).not.toHaveProperty('temperature')
})
it('claude-opus-47 (alt id form): drops temperature', () => {
// Provider sometimes ships ids with the dot stripped; modelPatch normalizes.
const body = baseBody('claude-opus-47-20251029')
modelPatch(body)
expect(body).not.toHaveProperty('temperature')
})
it('claude-opus-4-8: drops temperature', () => {
const body = baseBody('claude-opus-4-8')
modelPatch(body)
expect(body).not.toHaveProperty('temperature')
})
it('claude-opus-48 (alt id form): drops temperature', () => {
const body = baseBody('claude-opus-48-20251210')
modelPatch(body)
expect(body).not.toHaveProperty('temperature')
})
it('grok: removes tool_choice and disables reasoning/thinking', () => {
const body = baseBody('grok-4')
modelPatch(body)
expect(body).not.toHaveProperty('tool_choice')
expect(body).toMatchObject({
thinking: { type: 'disabled', effort: 'minimal' },
reasoning: { enabled: false, effort: 'low' },
})
})
it('gpt-5: sets verbosity=low and reasoning_effort=low', () => {
const body = baseBody('gpt-5')
modelPatch(body)
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'low' })
})
it('gpt-5-mini: low effort, temperature=1', () => {
const body = baseBody('gpt-5-mini')
modelPatch(body)
expect(body).toMatchObject({
verbosity: 'low',
reasoning_effort: 'low',
temperature: 1,
})
})
it('gpt-5.1 (gpt-51): disables reasoning', () => {
const body = baseBody('gpt-5.1')
modelPatch(body)
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'none' })
})
it('gpt-5.4 (gpt-54): drops reasoning_effort', () => {
const body = baseBody('gpt-5.4')
modelPatch(body)
expect(body).toMatchObject({ verbosity: 'low' })
expect(body).not.toHaveProperty('reasoning_effort')
})
it('gpt-5.5 (gpt-55): drops reasoning_effort and temperature', () => {
const body = baseBody('gpt-5.5')
modelPatch(body)
expect(body).toMatchObject({ verbosity: 'low' })
expect(body).not.toHaveProperty('reasoning_effort')
expect(body).not.toHaveProperty('temperature')
})
it('gemini: sets reasoning_effort=minimal', () => {
const body = baseBody('gemini-2.5-pro')
modelPatch(body)
expect(body).toMatchObject({ reasoning_effort: 'minimal' })
})
it('deepseek: removes tool_choice', () => {
const body = baseBody('deepseek-chat')
modelPatch(body)
expect(body).not.toHaveProperty('tool_choice')
})
it('minimax: clamps temperature into (0, 1] and removes parallel_tool_calls', () => {
const body = baseBody('minimax-m2')
body.temperature = 0
modelPatch(body)
expect(body.temperature).toBeGreaterThan(0)
expect(body.temperature).toBeLessThanOrEqual(1)
expect(body).not.toHaveProperty('parallel_tool_calls')
})
it('minimax: caps temperature at 1', () => {
const body = baseBody('minimax-m2')
body.temperature = 2
modelPatch(body)
expect(body.temperature).toBe(1)
})
it('normalizes provider-prefixed model id (openai/gpt-5)', () => {
const body = baseBody('openai/gpt-5')
modelPatch(body)
expect(body).toMatchObject({ verbosity: 'low', reasoning_effort: 'low' })
describe('normalizeModelName', () => {
it.each([
['gpt-5.2', 'gpt-52'],
['gpt_5_2', 'gpt52'],
['GPT-52-2026-01-01', 'gpt-52-2026-01-01'],
['openai/gpt-5.2-chat', 'gpt-52-chat'],
['claude_sonnet4_5', 'claudesonnet45'],
])('%s -> %s', (input, expected) => {
expect(normalizeModelName(input)).toBe(expected)
})
})
+145 -67
View File
@@ -24,101 +24,167 @@ export function zodToOpenAITool(name: string, tool: Tool) {
}
/**
* Patch model specific parameters
* @note in-place modification
* Patch model specific parameters. Only patches known models.
*
* @purpose
* - Reconcile the differences in the parameter schema each model accepts.
* - Disable thinking/reasoning, or lower it to the minimum where a full disable is impossible.
* - Minimize returned tokens.
* - Raise temperature for known smaller models to improve auto-recovery odds.
* @note Honor temperature if explicitly set by the user
*
* @todo Need vendor-specific patches.
* Local and 3rd-party hosted models may have different schema.
*/
export function modelPatch(body: Record<string, any>) {
export function modelPatch(body: Record<string, any>, baseURL?: string) {
const model: string = body.model || ''
if (!model) return body
const provider = getProvider(baseURL)
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
debug('Patch Qwen: disable thinking')
body.enable_thinking = false
if (body.temperature === undefined && !/max|plus/.test(modelName)) {
debug('Patch Qwen: raise temperature to 1.0')
body.temperature = 1.0
}
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: disable thinking')
if (modelName.startsWith('deepseek')) {
debug('Patch DeepSeek: disable thinking, remove tool_choice')
body.thinking = { type: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
// TODO: Claude naming pattern has changed
// needs proper handling
if (
modelName.startsWith('claude-opus-4-7') ||
modelName.startsWith('claude-opus-47') ||
modelName.startsWith('claude-opus-4-8') ||
modelName.startsWith('claude-opus-48')
) {
debug('Applying Claude-4.7/4.8 patch: remove temperature')
delete body.temperature
}
}
if (modelName.startsWith('grok')) {
debug('Applying Grok patch: removing tool_choice')
delete body.tool_choice
debug('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
if (modelName.startsWith('gpt')) {
debug('Applying GPT patch: set verbosity to low')
body.verbosity = 'low'
if (modelName.startsWith('gpt-5')) {
body.verbosity = 'low'
}
if (modelName.startsWith('gpt-52')) {
debug('Applying GPT-52 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-54')) {
debug('Applying GPT-5.4 patch: remove reasoning_effort')
delete body.reasoning_effort
} else if (modelName.startsWith('gpt-55')) {
debug('Applying GPT-5.4 patch: remove reasoning_effort and temperature')
// Since gpt-5.4, /chat/completions rejects any explicit reasoning_effort
// when function tools are present. Newer models are expected to follow.
// - gpt-5.1 / gpt-5.2 can fully disable reasoning
// - gpt-5 / -mini / -nano bottom out at "minimal"
// - everything else (gpt-4.x, chat-latest, gpt-5.4+) must not receive it
if (modelName.includes('chat-latest')) {
debug('Patch chat-latest: omit reasoning_effort and temperature')
delete body.reasoning_effort
delete body.temperature
} else if (modelName.startsWith('gpt-5-mini')) {
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
body.reasoning_effort = 'low'
body.temperature = 1
} else if (modelName.startsWith('gpt-5')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
} else if (/^gpt-5[12](-|$)/.test(modelName)) {
debug('Patch GPT-5.1/5.2: reasoning_effort=none')
body.reasoning_effort = 'none'
} else if (/^gpt-5(-|$)/.test(modelName)) {
debug('Patch GPT-5: reasoning_effort=minimal')
body.reasoning_effort = 'minimal'
} else {
debug('Patch GPT: omit reasoning_effort')
delete body.reasoning_effort
}
}
if (modelName.startsWith('claude')) {
if (/opus|sonnet|haiku/.test(modelName)) {
debug('Patch Claude: disable thinking')
body.thinking = { type: 'disabled' }
if (provider !== 'openrouter') {
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
}
} else {
debug('Patch Claude: reasoning_effort=low')
body.reasoning_effort = 'low'
// Fable and mythos can not disable adaptive thinking.
// Claude does not support tool_choice with extended thinking.
// These 2 concepts are blurred. Basically no tool_choice with thinking.
delete body.tool_choice
}
}
if (modelName.startsWith('gemini')) {
debug('Applying Gemini patch: set reasoning effort to minimal')
body.reasoning_effort = 'minimal'
debug('Patch Gemini: reasoning_effort=low')
body.reasoning_effort = 'low'
if (/^gemini-25(?!.*pro)/.test(modelName)) {
debug('Patch Gemini 2.5 non-Pro: reasoning_effort=none')
body.reasoning_effort = 'none'
} else if (
modelName.startsWith('gemini-35-flash') ||
modelName.startsWith('gemini-31-flash-lite') ||
modelName.startsWith('gemini-3-flash')
) {
debug('Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal')
body.reasoning_effort = 'minimal'
}
}
if (modelName.startsWith('deepseek')) {
debug('Applying DeepSeek patch: remove tool_choice')
delete body.tool_choice
if (modelName.startsWith('glm')) {
debug('Patch GLM: disable thinking')
body.thinking = { type: 'disabled' }
}
if (modelName.startsWith('hy')) {
debug('Patch Hunyuan: disable thinking, reasoning_effort=low')
body.thinking = { type: 'disabled' }
body.reasoning_effort = 'low'
}
if (modelName.startsWith('grok')) {
if (/^grok-4-?3/.test(modelName)) {
debug('Patch Grok 4.3: reasoning_effort=none')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('grok-3-mini') || modelName.startsWith('grok-code-fast')) {
debug('Patch Grok mini/code: reasoning_effort=low')
body.reasoning_effort = 'low'
}
}
if (modelName.startsWith('kimi')) {
if (!modelName.includes('code')) {
// kimi-k2.7-code cannot disable thinking
debug('Patch Kimi: disable thinking')
body.thinking = { type: 'disabled' }
}
}
if (modelName.startsWith('minimax')) {
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
// MiniMax API rejects temperature = 0; clamp to a small positive value
body.temperature = Math.max(body.temperature || 0, 0.01)
if (body.temperature > 1) body.temperature = 1
// MiniMax does not support parallel_tool_calls
debug('Patch MiniMax: remove parallel_tool_calls')
delete body.parallel_tool_calls
if (modelName.includes('m3')) {
// Only M3 can disable thinking
debug('Patch MiniMax: disable thinking')
body.thinking = { type: 'disabled' }
}
}
// provider patches
if (provider === 'openrouter') {
// openrouter use reasoning object instead of reasoning_effort
const reasoningEffort = body.reasoning_effort
const reasoningDisabled =
body.thinking?.type === 'disabled' ||
body.enable_thinking === false ||
reasoningEffort === 'none'
if (reasoningDisabled) {
body.reasoning = { enabled: false }
} else if (reasoningEffort) {
body.reasoning = { enabled: true, effort: reasoningEffort }
}
}
return body
@@ -139,7 +205,7 @@ export function modelPatch(body: Record<string, any>) {
* They should be treated as the same model.
* Normalize them to `gpt-52`
*/
function normalizeModelName(modelName: string): string {
export function normalizeModelName(modelName: string): string {
let normalizedName = modelName.toLowerCase()
// remove prefix before '/'
@@ -155,3 +221,15 @@ function normalizeModelName(modelName: string): string {
return normalizedName
}
export function getProvider(baseURL?: string): 'openrouter' | undefined {
if (!baseURL) return undefined
try {
const url = new URL(baseURL)
const hostname = url.hostname
if (hostname === 'openrouter.ai') return 'openrouter'
return undefined
} catch (e) {
return undefined
}
}
+20 -3
View File
@@ -1,10 +1,27 @@
import { defineConfig } from 'vitest/config'
import { configDefaults, defineConfig } from 'vitest/config'
// Convention: `*.live.test.ts` hits real provider APIs (slow, costs tokens)
// and only runs via `npm run test:live`. Everything else runs on `npm test`.
export default defineConfig({
test: {
name: 'llms',
include: ['src/**/*.test.ts'],
// Suppress console output from passing tests; failed tests still get their logs.
silent: 'passed-only',
projects: [
{
test: {
name: 'llms',
include: ['src/**/*.test.ts'],
exclude: [...configDefaults.exclude, 'src/**/*.live.test.ts'],
},
},
{
test: {
name: 'llms:live',
include: ['src/**/*.live.test.ts'],
// Keep live provider suites under OpenRouter's ~20 req/min free-route cap.
maxConcurrency: 2,
},
},
],
},
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/mcp",
"private": false,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"bin": {
"page-agent-mcp": "src/index.js"
+3 -6
View File
@@ -3,10 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="icon"
href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png"
/>
<link rel="icon" href="https://page-agent.github.io/assets/brand/page-agent-color-64.png" />
<title>Page Agent MCP Launcher</title>
<style>
* {
@@ -145,7 +142,7 @@
<div class="card">
<img
class="logo"
src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
src="https://page-agent.github.io/assets/brand/page-agent-color-512.webp"
alt="Page Agent"
/>
<div class="badge">Page Agent MCP Launcher</div>
@@ -164,7 +161,7 @@
<a class="store-btn" href="__STORE_URL__" target="_blank">
<img
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
src="https://page-agent.github.io/assets/third-party/chrome-web-store-192.svg"
alt=""
/>
<span data-i18n="install_btn">Install from Chrome Web Store</span>
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "page-agent",
"private": false,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"main": "./src/PageAgent.ts",
"types": "./src/PageAgent.ts",
@@ -54,10 +54,10 @@
"postpublish": "node ../../scripts/post-publish.js"
},
"dependencies": {
"@page-agent/core": "1.9.1",
"@page-agent/llms": "1.9.1",
"@page-agent/page-controller": "1.9.1",
"@page-agent/ui": "1.9.1",
"@page-agent/core": "1.12.1",
"@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.12.1",
"@page-agent/ui": "1.12.1",
"chalk": "^5.6.2"
},
"peerDependencies": {
+1 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/page-controller",
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"main": "./src/PageController.ts",
"types": "./src/PageController.ts",
@@ -48,8 +48,5 @@
},
"dependencies": {
"ai-motion": "^0.4.8"
},
"devDependencies": {
"happy-dom": "^20.10.2"
}
}
-2
View File
@@ -208,9 +208,7 @@ export async function inputTextElement(element: HTMLElement, text: string) {
selection?.removeAllRanges()
selection?.addRange(range)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('delete', false)
// eslint-disable-next-line @typescript-eslint/no-deprecated
doc.execCommand('insertText', false, text)
}
@@ -598,14 +598,12 @@ export default (
isAnyRectVisible = true
// Viewport check for this rect
if (
!(
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)
) {
if (!(
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)) {
isAnyRectInViewport = true
break // Found a visible rect in viewport, no need to check others
}
@@ -985,12 +983,10 @@ export default (
rect.height > 0 &&
!(
// Only check non-empty rects
(
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)
) {
isAnyRectInViewport = true
@@ -1103,14 +1099,12 @@ export default (
for (const rect of rects) {
if (rect.width === 0 || rect.height === 0) continue // Skip empty rects
if (
!(
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)
) {
if (!(
rect.bottom < -viewportExpansion ||
rect.top > window.innerHeight + viewportExpansion ||
rect.right < -viewportExpansion ||
rect.left > window.innerWidth + viewportExpansion
)) {
return true // Found at least one rect in the viewport
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@page-agent/ui",
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
@@ -1,64 +0,0 @@
import styles from './motion.module.css'
export function createMotion() {
const wrapper = document.createElement('div')
wrapper.className = styles.wrapper
{
const colorWrapper = document.createElement('div')
colorWrapper.className = styles.colorWrapper
wrapper.appendChild(colorWrapper)
const layerA = document.createElement('div')
layerA.className = styles.colorLayer + ' ' + styles.layerA
colorWrapper.appendChild(layerA)
const layerB = document.createElement('div')
layerB.className = styles.colorLayer + ' ' + styles.layerB
colorWrapper.appendChild(layerB)
const layerC = document.createElement('div')
layerC.className = styles.colorLayer + ' ' + styles.layerC
colorWrapper.appendChild(layerC)
}
{
const borderWrapper = document.createElement('div')
borderWrapper.className = styles.borderWrapper
wrapper.appendChild(borderWrapper)
const layerA = document.createElement('div')
layerA.className = styles.borderLayer + ' ' + styles.layerA
borderWrapper.appendChild(layerA)
const layerB = document.createElement('div')
layerB.className = styles.borderLayer + ' ' + styles.layerB
borderWrapper.appendChild(layerB)
const layerC = document.createElement('div')
layerC.className = styles.borderLayer + ' ' + styles.layerC
borderWrapper.appendChild(layerC)
}
function show() {
wrapper.classList.remove(styles.exit)
wrapper.classList.remove(styles.entry)
// Force reflow to restart animation
void wrapper.offsetHeight
wrapper.classList.add(styles.entry)
}
function hide() {
wrapper.classList.remove(styles.entry)
wrapper.classList.remove(styles.exit)
// Force reflow to restart animation
void wrapper.offsetHeight
wrapper.classList.add(styles.exit)
}
return {
element: wrapper,
show,
hide,
}
}
@@ -1,397 +0,0 @@
.wrapper {
position: absolute;
inset: 0;
pointer-events: none;
transform-origin: center;
--color-1: rgb(57, 182, 255);
--color-2: rgb(189, 69, 251);
--color-3: rgb(255, 87, 51);
--color-4: rgb(255, 214, 0);
--blend-mode: screen;
}
.colorLayer {
position: absolute;
inset: 0;
/* 变亮混合模式 */
/* mix-blend-mode: screen; */
/* mix-blend-mode: overlay; */
/* mix-blend-mode: multiply; */
mix-blend-mode: add;
/* 边框遮罩 - 中间透明,边缘不透明 */
mask-image: url(https://img.alicdn.com/imgextra/i2/O1CN01iW1wfX1C0ICvoPbTq_!!6000000000018-2-tps-512-512.png);
mask-repeat: no-repeat;
mask-size: calc(100% + 10px) calc(100% + 10px);
}
.borderWrapper {
position: absolute;
inset: 0;
/* filter: blur(10px); */
}
.borderLayer {
position: absolute;
inset: 0;
/* 变亮混合模式 */
/* mix-blend-mode: overlay; */
mix-blend-mode: add;
mask-image:
linear-gradient(
to right,
black 0px,
black 2px,
transparent 2px,
transparent calc(100% - 2px),
black calc(100% - 2px),
black 100%
),
linear-gradient(
to top,
black 0px,
black 2px,
transparent 2px,
transparent calc(100% - 2px),
black calc(100% - 2px),
black 100%
);
mask-composite: add;
mask-repeat: no-repeat;
mask-size: 100% 100%;
/* filter: blur(100px); */
}
.blueLayer {
&.colorLayer {
mask-position: left -5px top -5px;
}
&::after {
content: '';
position: absolute;
/* inset: 0; */
width: calc(max(100vw, 100vh) * 1.5);
height: 600px;
top: calc(50% - 300px);
left: 50%;
filter: blur(100px);
background: rgb(57, 182, 255);
animation: rotate-clockwise 4s linear infinite;
animation-delay: -3s;
}
}
.purpleLayer {
&.colorLayer {
mask-position: left -3px top -7px;
}
&::after {
content: '';
position: absolute;
/* inset: 0; */
width: calc(max(100vw, 100vh) * 1.5);
height: 600px;
top: calc(50% - 300px);
left: 50%;
filter: blur(100px);
background: rgb(189, 69, 251);
animation: rotate-clockwise 4s linear infinite;
animation-delay: -2s;
}
}
.orangeLayer {
/* opacity: 0.5; */
&.colorLayer {
mask-position: left -7px top -2px;
}
&::after {
content: '';
position: absolute;
/* inset: 0; */
width: calc(max(100vw, 100vh) * 1.5);
height: 600px;
top: calc(50% - 300px);
left: 50%;
filter: blur(100px);
background: rgb(255, 87, 51);
animation: rotate-counter-clockwise 3s linear infinite;
animation-delay: -2s;
}
}
.yellowLayer {
/* opacity: 0.5; */
&.colorLayer {
mask-position: left -6px top -4px;
}
&::after {
content: '';
position: absolute;
/* inset: 0; */
width: calc(max(100vw, 100vh) * 1.5);
height: 600px;
top: calc(50% - 300px);
left: 50%;
filter: blur(100px);
background: rgb(255, 214, 0);
animation: rotate-counter-clockwise 4s linear infinite;
animation-delay: -1s;
}
}
/* 旋转动画 */
@keyframes rotate-clockwise {
0% {
transform: translateX(-50%) rotate(0deg);
}
100% {
transform: translateX(-50%) rotate(360deg);
}
}
@keyframes rotate-counter-clockwise {
0% {
transform: translateX(-50%) rotate(0deg);
}
100% {
transform: translateX(-50%) rotate(-360deg);
}
}
@keyframes wrapper-entry {
from {
transform: scale(1.1);
}
to {
transform: scale(1);
}
}
/*
rgb(57, 182, 255)
rgb(189, 69, 251)
rgb(255, 87, 51)
rgb(255, 214, 0)
*/
@keyframes mask-running {
from {
transform: translateX(0%);
}
to {
transform: translateX(100%);
}
}
@keyframes mask-running-reverse {
from {
transform: translateX(100%);
}
to {
transform: translateX(0%);
}
}
.colorWrapper {
position: absolute;
inset: 0;
.colorLayer {
position: absolute;
inset: 0;
mix-blend-mode: var(--blend-mode);
/* 边框遮罩 - 中间透明,边缘不透明 */
mask-image: url(https://img.alicdn.com/imgextra/i2/O1CN01iW1wfX1C0ICvoPbTq_!!6000000000018-2-tps-512-512.png);
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
}
.borderWrapper {
position: absolute;
inset: 0;
--blend-mode: lighten;
.borderLayer {
position: absolute;
inset: 0;
mix-blend-mode: var(--blend-mode);
mask-border: url(https://img.alicdn.com/imgextra/i3/O1CN01bFjRug1yssyWEUbKL_!!6000000006635-2-tps-256-256.png)
25;
-webkit-mask-box-image: url(https://img.alicdn.com/imgextra/i3/O1CN01bFjRug1yssyWEUbKL_!!6000000006635-2-tps-256-256.png)
25;
mask-repeat: no-repeat;
mask-size: 100% 100%;
background-color: var(--color-2);
}
}
.entry .colorWrapper,
.entry .borderWrapper {
animation: wrapper-entry 0.8s ease-in-out forwards;
}
.exit .colorWrapper,
.exit .borderWrapper {
animation: wrapper-entry 0.8s ease-in-out reverse forwards;
}
.layerA {
position: absolute;
inset: 0;
&::before {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: -100%;
top: 0;
background-image: linear-gradient(
to right bottom,
transparent,
var(--color-1),
transparent,
var(--color-1),
transparent
);
animation: mask-running 2s linear infinite;
}
&::after {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-image: linear-gradient(
to right bottom,
transparent,
var(--color-1),
transparent,
var(--color-1),
transparent
);
animation: mask-running 2s linear infinite;
}
}
.layerB {
position: absolute;
inset: 0;
&::before {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: -100%;
top: 0;
background: linear-gradient(
to right top,
transparent,
var(--color-2),
transparent,
var(--color-2),
transparent
);
animation: mask-running-reverse 3s linear infinite;
}
&::after {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: linear-gradient(
to right top,
transparent,
var(--color-2),
transparent,
var(--color-2),
transparent
);
animation: mask-running-reverse 3s linear infinite;
}
}
.layerC {
position: absolute;
inset: 0;
opacity: 0.5;
&::before {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: -100%;
top: 0;
background: linear-gradient(
to right top,
transparent,
var(--color-3),
transparent,
var(--color-3),
transparent
);
animation: mask-running 1s linear infinite;
}
&::after {
mix-blend-mode: var(--blend-mode);
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
background: linear-gradient(
to right top,
transparent,
var(--color-3),
transparent,
var(--color-3),
transparent
);
animation: mask-running 1s linear infinite;
}
}
-5
View File
@@ -1,5 +0,0 @@
This is the CSS implementation of ai-motion.
Easy to use but Terrible performance. Causing full screen glitching in some browsers.
Use it only in a small area.
+5 -3
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<link
rel="icon"
type="image/svg+xml"
href="https://img.alicdn.com/imgextra/i2/O1CN012eGDRI1X6nnMt9clU_!!6000000002875-49-tps-64-64.webp"
type="image/webp"
href="https://page-agent.github.io/assets/brand/page-agent-color-64.webp"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PageAgent - The GUI Agent Living in Your Webpage</title>
@@ -19,7 +19,7 @@
/>
<meta
property="og:image"
content="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
content="https://page-agent.github.io/assets/brand/page-agent-color-512.webp"
/>
<meta property="og:url" content="https://alibaba.github.io/page-agent" />
<meta property="og:type" content="website" />
@@ -33,6 +33,7 @@
<meta property="og:locale:alternate" content="zh_CN" />
<!-- Google tag (gtag.js) -->
<!-- cspell:disable -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HCGRJTN3HM"></script>
<script>
window.dataLayer = window.dataLayer || []
@@ -43,6 +44,7 @@
gtag('config', 'G-HCGRJTN3HM')
</script>
<!-- cspell:enable -->
</head>
<body>
<div id="root">
+11 -11
View File
@@ -1,7 +1,7 @@
{
"name": "@page-agent/website",
"private": true,
"version": "1.9.1",
"version": "1.12.1",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
@@ -10,24 +10,24 @@
},
"devDependencies": {
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.9",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.3.0",
"@radix-ui/react-tooltip": "^1.2.9",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.2",
"@radix-ui/react-tooltip": "^1.2.11",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.16.0",
"motion": "^12.40.0",
"lucide-react": "^1.23.0",
"motion": "^12.42.2",
"next-themes": "^0.4.6",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"rough-notation": "^0.5.1",
"simple-icons": "^16.23.0",
"simple-icons": "^16.25.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
"wouter": "^3.10.0"
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default function Header() {
onClick={() => setMobileMenuOpen(false)}
>
<img
src="https://img.alicdn.com/imgextra/i2/O1CN01HB8ylu1uozANEMZw2_!!6000000006085-49-tps-128-128.webp"
src="https://page-agent.github.io/assets/brand/page-agent-color-128.webp"
alt="PageAgent Logo"
className="w-10 h-10 rounded-xl group-hover:scale-110 transition-transform duration-200"
/>
+2 -2
View File
@@ -1,8 +1,8 @@
// Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.9.1/dist/iife/page-agent.demo.js'
'https://cdn.jsdelivr.net/npm/page-agent@1.12.1/dist/iife/page-agent.demo.js'
export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.9.1/files/dist/iife/page-agent.demo.js'
'https://registry.npmmirror.com/page-agent/1.12.1/files/dist/iife/page-agent.demo.js'
// Demo LLM for website testing (homepage quick trial uses flash)
export const DEMO_MODEL = 'qwen3.5-flash'
+3 -1
View File
@@ -9,13 +9,15 @@ export function useGitHubStars() {
useEffect(() => {
if (cached !== null) return
fetch(STATS_URL)
const controller = new AbortController()
fetch(STATS_URL, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
cached = data.stargazers_count ?? null
setStars(cached)
})
.catch(() => {})
return () => controller.abort()
}, [])
return stars
@@ -143,13 +143,6 @@ const result = await agent.execute('Fill in the form with test data')`}
required: false,
description: 'LLM AK',
},
{
name: 'temperature',
type: 'number',
description: isZh
? '模型温度参数,控制输出随机性'
: 'Model temperature, controls output randomness',
},
{
name: 'maxRetries',
type: 'number',
@@ -6,9 +6,9 @@ import { Heading } from '@/components/Heading'
import { useLanguage } from '@/i18n/context'
const BASELINE = new Set([
'gpt-5.1',
'gpt-5.4-mini',
'claude-haiku-4.5',
'gpt-5.4-nano',
'claude-haiku-4-5',
'gemini-3.5-flash',
'deepseek-v4-flash',
'qwen3.5-plus',
@@ -25,11 +25,13 @@ const MODEL_GROUPS: Record<string, string[]> = {
'qwen3.6-flash',
'qwen3.5-plus',
'qwen3.5-flash',
'qwen3-coder-next',
'qwen-3-max',
'qwen-3-plus',
'qwen3-max',
// 'qwen3-coder-next', // low success rate
],
OpenAI: [
'gpt-5.6-sol',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5.5',
'gpt-5.4',
'gpt-5.4-mini',
@@ -44,24 +46,30 @@ const MODEL_GROUPS: Record<string, string[]> = {
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
Google: [
'gemini-3.5-flash',
'gemini-3.1-pro',
'gemini-3.1-flash-lite',
'gemini-3-pro',
'gemini-3-flash',
'gemini-2.5',
'gemini-2.5-pro',
'gemini-2.5-flash',
],
Anthropic: [
'claude-opus-4.8',
'claude-opus-4.7',
'claude-opus-4.6',
'claude-opus-4.5',
'claude-sonnet-4.5',
'claude-haiku-4.5',
'claude-sonnet-3.5',
'claude-sonnet-5',
'claude-fable-5',
'claude-opus-4-8',
'claude-opus-4-7',
'claude-opus-4-6',
'claude-opus-4-5',
'claude-sonnet-4-5',
'claude-haiku-4-5',
],
MiniMax: ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed'],
xAI: ['grok-4.1-fast', 'grok-4', 'grok-code-fast'],
MoonshotAI: ['kimi-k2.5'],
'Z.AI': ['glm-5', 'glm-4.7'],
MiniMax: [
// 'MiniMax-M3', low success rate
'MiniMax-M2.7',
'MiniMax-M2.5',
],
xAI: ['grok-4.5', 'grok-4.3', 'grok-build-0.1'],
Tencent: ['hy3'],
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
}
const ModelBadge = ({ model, baseline }: { model: string; baseline?: boolean }) => (
@@ -91,7 +99,7 @@ export default function Models() {
<section className="mb-10">
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
{isZh ? '已测试模型' : 'Tested Models'}
{isZh ? '模型列表' : 'Model List'}
</Heading>
<div className="bg-linear-to-br from-emerald-50 to-cyan-50 dark:from-emerald-950/30 dark:to-cyan-950/30 rounded-xl p-6 border border-emerald-200/50 dark:border-emerald-800/50">
<div className="grid grid-cols-[5rem_1fr] gap-x-3 gap-y-3 items-start">
@@ -124,8 +132,8 @@ export default function Models() {
</li>
<li>
{isZh
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复,建议设置较高的 temperature'
: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover. Higher temperature recommended'}
? 'ToolCall 能力较弱的模型可能返回错误的格式,常见错误能够自动恢复'
: 'Models with weaker ToolCall capabilities may return incorrect formats. Common errors usually auto-recover'}
</li>
<li>
{isZh
@@ -281,7 +289,7 @@ LLM_MODEL_NAME="qwen3.5-plus"`}
code={`const pageAgent = new PageAgent({
baseURL: 'https://your-claude-proxy.example/v1',
apiKey: 'your-api-key',
model: 'claude-sonnet-4.5',
model: 'claude-sonnet-5',
transformRequestBody: (requestBody) => ({
...requestBody,
cache_control: { type: 'ephemeral' },
@@ -17,11 +17,17 @@ import { useLanguage } from '../../i18n/context'
let pageAgentModule: Promise<typeof import('page-agent')> | null = null
function getInjection(useCN?: boolean) {
const cdn = useCN ? CDN_DEMO_CN_URL : CDN_DEMO_URL
/**
* Get the bookmarklet injection script
* @param cdnSource Which CDN mirror to use
* @param isZh Whether to use Chinese language
*/
function getInjection(cdnSource: 'china' | 'international', isZh?: boolean) {
const cdn = cdnSource === 'china' ? CDN_DEMO_CN_URL : CDN_DEMO_URL
const locale = isZh ? 'zh-CN' : 'en-US'
const injection = encodeURI(
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
`javascript:(function(){var s=document.createElement('script');s.src=\`${cdn}?lang=${locale}&t=\${Math.random()}\`;s.setAttribute('crossorigin', true);s.type="text/javascript";s.onload=()=>console.log('PageAgent script loaded!');document.body.appendChild(s);})();`
)
return `
@@ -298,7 +304,7 @@ export default function HeroSection() {
</select>
<div
dangerouslySetInnerHTML={{
__html: getInjection(cdnSource === 'china'),
__html: getInjection(cdnSource, isZh),
}}
></div>
</div>
@@ -40,7 +40,7 @@ export default function OneMoreThingSection() {
className="group inline-flex items-center gap-3 px-8 py-4 bg-linear-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-medium rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105"
>
<img
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
src="https://page-agent.github.io/assets/third-party/chrome-web-store-192.svg"
alt="Chrome Web Store"
className="w-7 h-7"
/>