Compare commits

...

20 Commits

Author SHA1 Message Date
Simon fa4664dfa5 chore(deps): bump dependencies (#612) 2026-07-10 17:41:53 +08:00
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
30 changed files with 787 additions and 744 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. 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. 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. 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. 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. - 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. - 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 ## Procedure
1. **Read contribution rules.** Read `CONTRIBUTING.md` and any package-level instructions (e.g. `packages/*/AGENTS.md`) relevant to the changed files. 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. - **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. - **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. - **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: 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 - Large diff with no description of intent provided
- Changes touch core lib or extension (where vibe coding is prohibited per `CONTRIBUTING.md`) - 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. - 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. 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`. 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). 10. **Report results.** Tell the user: branch name, commit hash, PR link, and what validation actually ran (with pass/fail).
## Post-Submission Reminder ## Post-Submission Reminder
@@ -75,7 +82,7 @@ After successfully opening the PR, ALWAYS give a brief reminder in the user's la
## Validation Strategy ## Validation Strategy
- Default to enough validation to defend the PR, not the absolute minimum. - 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. - Escalate to broader validation when the diff crosses packages, changes shared code, or affects release behavior.
- Never claim checks that did not actually run. - 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. - 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. - All commit authors have proper name and email configured.
- The PR title matches the commit intent. - The PR title matches the commit intent.
- The PR body follows the template structure completely. - 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 reported validation is accurate — nothing fabricated.
- The post-submission reminder was delivered in the user's language, concisely and accurately. - 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 ## Testing
- [ ] `npm run ci` passes
- [ ] Tested in modern browsers - [ ] Tested in modern browsers
- [ ] No console errors
- [ ] Types/doc added - [ ] Types/doc added
## Requirements / 要求 ## Requirements / 要求
+2 -8
View File
@@ -3,27 +3,21 @@ permissions:
contents: read contents: read
on: on:
push:
branches: [main]
pull_request: pull_request:
branches: [main] branches: [main]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24]
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: ${{ matrix.node-version }} node-version: 24
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
@@ -1,4 +1,4 @@
name: Deploy Demo name: Deploy Website
on: on:
push: push:
@@ -6,7 +6,7 @@ on:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: page-agent-trusted
permissions: permissions:
contents: read contents: read
pages: write pages: write
@@ -24,7 +24,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build demo - name: Build website
run: npm run build:website run: npm run build:website
- name: Setup Pages - 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
+4 -2
View File
@@ -11,6 +11,8 @@ permissions:
jobs: jobs:
release: release:
# Only the repo owner may trigger a release, even if tag protection is bypassed
if: github.actor == 'gaomeng1900'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
@@ -21,9 +23,9 @@ jobs:
node-version: 24 node-version: 24
registry-url: 'https://registry.npmjs.org' 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 - name: Update npm
run: npm install -g npm@latest run: npm install -g npm@11
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
-15
View File
@@ -2,21 +2,6 @@
"files.exclude": { "files.exclude": {
"packages/*/node_modules": true "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", "editor.defaultFormatter": "esbenp.prettier-vscode",
"js/ts.tsdk.path": "node_modules/typescript/lib", "js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "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` - **Location**: co-located, `src/foo.test.ts` next to `src/foo.ts`
- **Coverage today**: `packages/llms` only — other packages will follow incrementally - **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. - **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 - **Template**: See @page-agent/llms
```bash ```bash
+23 -21
View File
@@ -5,9 +5,18 @@
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png"> <img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture> </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) 🌐 **English** | [中文](./docs/README-zh.md)
@@ -15,7 +24,7 @@ The GUI Agent Living in Your Webpage. Control web interfaces with natural langua
<!-- demo video --> <!-- 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** - **📖 Text-based DOM manipulation**
- No screenshots. No multi-modal LLMs or special permissions needed. - No screenshots. No multi-modal LLMs or special permissions needed.
- **🧠 Bring your own LLMs** - **🧠 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.** - **🐙 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 - 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. - **SaaS AI Copilot** — Ship an AI copilot in your product in lines of code. No backend rewrite.
- **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems. - **Smart Form Filling** — Turn 20-click workflows into one sentence. Perfect for ERP, CRM, and admin systems.
- **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier. - **Accessibility** — Make any web app accessible through natural language. Voice commands, screen readers, zero barrier.
- **Multi-page Agent** — Extend your 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. - **MCP** - Allow your agent clients to control your browser.
## 🚀 Quick Start ## 🚀 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: Fastest way to try PageAgent with our free Demo LLM:
```html ```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). > **⚠️ 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 | | Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- | | ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.11.0/dist/iife/page-agent.demo.js | | 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.11.0/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.12.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(...)`.
### NPM Installation ### 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). For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
## 🌟 Awesome Page Agent
Built something cool with PageAgent? Add it here! Open a PR to share your project.
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
| Project | Description |
| -------- | ----------------------------------------------------------- |
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
## 🤝 Contributing ## 🤝 Contributing
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows. We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
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. 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**. 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. this project possible.
``` ```
---
**⭐ Star this repo if you find PageAgent helpful!** **⭐ Star this repo if you find PageAgent helpful!**
+23
View File
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.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 ## [1.10.0] - 2026-06-15
### Breaking Changes ### Breaking Changes
+21 -9
View File
@@ -5,17 +5,26 @@
<img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png"> <img alt="Page Agent Banner" src="https://page-agent.github.io/assets/readme/banner-light.png">
</picture> </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 应用。无须后端、客户端、浏览器插件。 纯 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) | **中文** 🌐 [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> <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 --> <!-- 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 操作** - **📖 基于文本的 DOM 操作**
- 无需截图,无需多模态模型或特殊权限 - 无需截图,无需多模态模型或特殊权限
- **🧠 自备 LLM** - **🧠 自备 LLM**
- 支持多数主流模型,包括本地部署模型。参见[支持的模型](https://alibaba.github.io/page-agent/docs/features/models)。
- 🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务 - 🐙 可选的 [Chrome 扩展](https://alibaba.github.io/page-agent/docs/features/chrome-extension),支持跨页面任务
- [MCP Server (Beta)](https://alibaba.github.io/page-agent/docs/features/mcp-server) - [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 通过我们免费的 Demo LLM 快速体验 PageAgent
```html ```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)。 > **⚠️ 仅用于技术评估。** 该 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 | | Mirrors | URL |
| ------- | ----------------------------------------------------------------------------------- | | ------- | ----------------------------------------------------------------------------------- |
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.11.0/dist/iife/page-agent.demo.js | | 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.11.0/files/dist/iife/page-agent.demo.js | | China | https://registry.npmmirror.com/page-agent/1.12.1/files/dist/iife/page-agent.demo.js |
在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
### NPM 安装 ### NPM 安装
@@ -85,6 +97,8 @@ await agent.execute('点击登录按钮')
我们不接受未经实质性人类参与、完全由 Bot 或 Agent 自动生成的代码。 我们不接受未经实质性人类参与、完全由 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)** 的优秀工作构建。 本项目基于 **[`browser-use`](https://github.com/browser-use/browser-use)** 的优秀工作构建。
@@ -107,6 +121,4 @@ this project possible.
[MIT License](../LICENSE) [MIT License](../LICENSE)
---
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!** **⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
+462 -501
View File
File diff suppressed because it is too large Load Diff
+12 -8
View File
@@ -1,7 +1,7 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
@@ -37,20 +37,21 @@
"postpublish": "npm run postpublish --workspaces --if-present", "postpublish": "npm run postpublish --workspaces --if-present",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json", "typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
"test": "npm test --workspaces --if-present", "test": "npm test --workspaces --if-present",
"test:live": "npm run test:live --workspaces --if-present",
"lint": "eslint .", "lint": "eslint .",
"ci": "node scripts/ci.js", "ci": "node scripts/ci.js",
"cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output", "cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
"prepare": "husky || true" "prepare": "husky || true"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^21.2.0", "@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.2.0", "@commitlint/config-conventional": "^21.2.0",
"@eslint-react/eslint-plugin": "^5.10.0", "@eslint-react/eslint-plugin": "^5.13.2",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@microsoft/api-extractor": "^7.58.9", "@microsoft/api-extractor": "^7.58.9",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^26.0.1", "@types/node": "^26.1.1",
"@vitejs/plugin-react": "^6.0.3", "@vitejs/plugin-react": "^6.0.3",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"concurrently": "^10.0.3", "concurrently": "^10.0.3",
@@ -60,13 +61,13 @@
"happy-dom": "^20.10.6", "happy-dom": "^20.10.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^17.0.8", "lint-staged": "^17.0.8",
"prettier": "^3.9.4", "prettier": "^3.9.5",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.62.1", "typescript-eslint": "^8.63.0",
"unplugin-dts": "^1.0.3", "unplugin-dts": "^1.0.3",
"vite": "^8.1.2", "vite": "^8.1.4",
"vite-plugin-css-injected-by-js": "^5.0.1", "vite-plugin-css-injected-by-js": "^5.0.1",
"vitest": "^4.1.9" "vitest": "^4.1.10"
}, },
"overrides": { "overrides": {
"typescript": "^6.0.3" "typescript": "^6.0.3"
@@ -82,6 +83,9 @@
], ],
"*.css": [ "*.css": [
"npx prettier --write --ignore-unknown" "npx prettier --write --ignore-unknown"
],
"*.md": [
"npx prettier --write --ignore-unknown"
] ]
}, },
"commitlint": { "commitlint": {
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/core", "name": "@page-agent/core",
"private": false, "private": false,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"main": "./src/PageAgentCore.ts", "main": "./src/PageAgentCore.ts",
"types": "./src/PageAgentCore.ts", "types": "./src/PageAgentCore.ts",
@@ -55,8 +55,8 @@
}, },
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
"@page-agent/llms": "1.11.0", "@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.11.0" "@page-agent/page-controller": "1.12.1"
}, },
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
+10 -10
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/ext", "name": "@page-agent/ext",
"private": true, "private": true,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
@@ -10,27 +10,27 @@
"postinstall": "wxt prepare" "postinstall": "wxt prepare"
}, },
"devDependencies": { "devDependencies": {
"@radix-ui/react-hover-card": "^1.1.18", "@radix-ui/react-hover-card": "^1.1.19",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.11", "@radix-ui/react-label": "^2.1.11",
"@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.2", "@radix-ui/react-switch": "^1.3.3",
"@tailwindcss/vite": "^4.3.2", "@tailwindcss/vite": "^4.3.2",
"@types/chrome": "^0.2.1", "@types/chrome": "^0.2.2",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.2.2", "@wxt-dev/module-react": "^1.2.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"idb": "^8.0.3", "idb": "^8.0.3",
"lucide-react": "^1.22.0", "lucide-react": "^1.24.0",
"motion": "^12.42.2", "motion": "^12.42.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.24.1", "simple-icons": "^16.25.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.2", "tailwindcss": "^4.3.2",
@@ -38,10 +38,10 @@
"wxt": "^0.20.27" "wxt": "^0.20.27"
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.11.0", "@page-agent/core": "1.12.1",
"@page-agent/llms": "1.11.0", "@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.11.0", "@page-agent/page-controller": "1.12.1",
"@page-agent/ui": "1.11.0", "@page-agent/ui": "1.12.1",
"ai-motion": "^0.4.8", "ai-motion": "^0.4.8",
"chalk": "^5.6.2" "chalk": "^5.6.2"
}, },
@@ -64,9 +64,11 @@ export class MultiPageAgent extends PageAgentCore {
}, },
onBeforeStep: async (agent) => { onBeforeStep: async (agent) => {
// pull latest tab state so that tabs changes can be observed
await tabsController.syncTabs()
if (!tabsController.currentTabId) return if (!tabsController.currentTabId) return
// make sure the current tab is loaded before the step starts // make sure the current tab is loaded before the step starts
await tabsController.waitUntilTabLoaded(tabsController.currentTabId!) await tabsController.waitUntilTabLoaded(tabsController.currentTabId)
}, },
onDispose: () => { onDispose: () => {
@@ -1,5 +1,9 @@
/** /**
* background logics for TabsController * 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' import type { TabAction } from './TabsController'
@@ -144,7 +148,6 @@ export function handleTabControlMessage(
} }
case 'get_window_tabs': { case 'get_window_tabs': {
debug('get_window_tabs', payload)
chrome.tabs chrome.tabs
.query({ windowId: payload.windowId }) .query({ windowId: payload.windowId })
.then((tabs) => { .then((tabs) => {
@@ -161,41 +164,3 @@ export function handleTabControlMessage(
return 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 } })
})
}
+98 -81
View File
@@ -33,15 +33,16 @@ async function getOwnWindowId(): Promise<number | undefined> {
* Controller for managing browser tabs. * Controller for managing browser tabs.
* - live in the agent env (extension page or content script) * - live in the agent env (extension page or content script)
* - no chrome apis. call sw for tab operations * - no chrome apis. call sw for tab operations
* - store tabs states, pull tabs info and detect changes
*/ */
export class TabsController { export class TabsController {
currentTabId: number | null = null currentTabId: number | null = null
private disposed = false private disposed = false
private port?: chrome.runtime.Port
private portRetries = 0
/* tracked window */
private windowId: number | null = null private windowId: number | null = null
/* tracked tabs */
private tabs: TabMeta[] = [] private tabs: TabMeta[] = []
private initialTabId: number | null = null private initialTabId: number | null = null
private tabGroupId: number | null = null private tabGroupId: number | null = null
@@ -57,9 +58,6 @@ export class TabsController {
} }
await this.updateCurrentTabId(null) await this.updateCurrentTabId(null)
this.disposed = false
this.port = undefined
this.portRetries = 0
this.windowId = null this.windowId = null
this.tabs = [] this.tabs = []
@@ -85,8 +83,6 @@ export class TabsController {
} }
} }
this.connectTabEvents()
if (experimentalIncludeAllTabs) { if (experimentalIncludeAllTabs) {
const allTabs = await sendMessage({ const allTabs = await sendMessage({
type: 'TAB_CONTROL', type: 'TAB_CONTROL',
@@ -280,11 +276,14 @@ export class TabsController {
} }
async summarizeTabs(): Promise<string> { 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) { for (const tab of this.tabs) {
const { title, url } = await this.getTabInfo(tab.id) const { title, url } = await this.getTabInfo(tab.id)
summaries.push( summaries.push(
`| ${tab.id} | ${url} | ${title} | ${this.currentTabId === tab.id ? '✅' : ''} |` `| ${tab.id} | ${url} | ${title} | ${tab.status ?? '-'} | ${this.currentTabId === tab.id ? '✅' : ''} |`
) )
} }
if (!this.tabs.length) { if (!this.tabs.length) {
@@ -297,82 +296,96 @@ export class TabsController {
async waitUntilTabLoaded(tabId: number): Promise<void> { async waitUntilTabLoaded(tabId: number): Promise<void> {
const tab = this.tabs.find((t) => t.id === tabId) const tab = this.tabs.find((t) => t.id === tabId)
if (!tab) throw new Error(`Tab ID ${tabId} not found in tab list.`) 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 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) 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. * Pull the window's tabs from the background.
* * Pulling is better than pushing. Long-lived ports are stateful troublemakers.
* @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.
*/ */
private connectTabEvents() { async syncTabs(): Promise<void> {
this.port = chrome.runtime.connect({ name: 'tab-events' }) if (this.disposed || this.windowId == null) return
this.port.onMessage.addListener((message: any) => { const result = await sendMessage({
if (this.disposed) return type: 'TAB_CONTROL',
this.portRetries = 0 action: 'get_window_tabs',
payload: { windowId: this.windowId },
if (message.action === 'created') {
const tab = message.payload.tab as chrome.tabs.Tab
const shouldTrack =
tab.groupId === this.tabGroupId ||
// @note Never track tabs from other windows.
(this.experimentalIncludeAllTabs && tab.windowId === this.windowId)
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
}
}
}) })
// sendMessage already logged the failure; keep the stale mirror
if (!result?.success) return
this.port.onDisconnect.addListener(() => { const liveTabs = (result.tabs as chrome.tabs.Tab[]).filter((t) => t.id != null)
this.port = undefined const liveIds = new Set(liveTabs.map((t) => t.id!))
if (this.disposed) return
if (this.portRetries >= 7) { const closedIds = this.tabs.filter((t) => !liveIds.has(t.id)).map((t) => t.id)
console.error(PREFIX, 'tab events port failed after 7 retries, giving up') if (closedIds.length) {
return 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() { dispose() {
debug('dispose') debug('dispose')
this.disposed = true this.disposed = true
this.port?.disconnect()
this.port = undefined
} }
} }
@@ -410,29 +423,33 @@ function randomColor(): TabGroupColor {
/** /**
* Wait until condition becomes true * Wait until condition becomes true
* @returns Returns when condition becomes true, throws otherwise * @returns Returns when condition becomes true, false if timeout
* @param timeoutMS Timeout in milliseconds, default 1 minutes, throws error on timeout * @param timeoutMS Timeout in milliseconds, default 1 minutes
* @param error Error object to reject on timeout. If not provided, will resolve with false * @param throwIfTimeout Reject on timeout instead of resolving with `false`
*/ */
export async function waitUntil( async function waitUntil(
check: () => boolean | Promise<boolean>, check: () => boolean | Promise<boolean>,
timeoutMS = 60_000, timeoutMS = 60_000,
error?: string throwIfTimeout = false
): Promise<boolean> { ): Promise<boolean> {
if (await check()) return true if (await check()) return true
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const start = Date.now() const start = Date.now()
const poll = async () => { const poll = async () => {
if (await check()) return resolve(true) try {
if (Date.now() - start > timeoutMS) { if (await check()) return resolve(true)
if (error) { if (Date.now() - start > timeoutMS) {
return reject(new Error(error)) if (throwIfTimeout) {
} else { return reject(new Error(`waitUntil timed out after ${timeoutMS}ms`))
return resolve(false) } else {
return resolve(false)
}
} }
setTimeout(poll, 100)
} catch (err) {
reject(err instanceof Error ? err : new Error(String(err)))
} }
setTimeout(poll, 100)
} }
setTimeout(poll, 100) setTimeout(poll, 100)
}) })
@@ -1,13 +1,9 @@
import { handlePageControlMessage } from '@/agent/RemotePageController.background' import { handlePageControlMessage } from '@/agent/RemotePageController.background'
import { handleTabControlMessage, setupTabEventsPort } from '@/agent/TabsController.background' import { handleTabControlMessage } from '@/agent/TabsController.background'
export default defineBackground(() => { export default defineBackground(() => {
console.log('[Background] Service Worker started') console.log('[Background] Service Worker started')
// tab change events
setupTabEventsPort()
// generate user auth token // generate user auth token
chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => { chrome.storage.local.get('PageAgentExtUserAuthToken').then((result) => {
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
"types": "./src/index.ts", "types": "./src/index.ts",
@@ -43,7 +43,8 @@
"homepage": "https://alibaba.github.io/page-agent/", "homepage": "https://alibaba.github.io/page-agent/",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"test": "vitest run", "test": "vitest run --project llms",
"test:live": "vitest run --project llms:live",
"prepublishOnly": "node ../../scripts/pre-publish.js", "prepublishOnly": "node ../../scripts/pre-publish.js",
"postpublish": "node ../../scripts/post-publish.js" "postpublish": "node ../../scripts/post-publish.js"
}, },
@@ -10,9 +10,10 @@
* Tests `OpenAIClient` directly (not the `LLM` retry wrapper), so a failure * Tests `OpenAIClient` directly (not the `LLM` retry wrapper), so a failure
* always reflects the very first request/response no retry can mask it. * always reflects the very first request/response no retry can mask it.
* *
* Each provider's tests skip (not fail) when its `TESTING_*_KEY` env var is * Per the `*.live.test.ts` convention this suite is excluded from `npm test`
* absent, so this stays CI-safe. To actually run these, put keys in the * (slow, costs tokens) run it manually with `npm run test:live`. Each
* repo-root `.env`: * 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_OPENROUTER_KEY=...
* TESTING_DEEPSEEK_KEY=... * TESTING_DEEPSEEK_KEY=...
@@ -50,6 +51,9 @@ const MODEL_GROUPS: Record<string, string[]> = {
'qwen3-max', 'qwen3-max',
], ],
OpenAI: [ OpenAI: [
'gpt-5.6-sol',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5.5', 'gpt-5.5',
'gpt-5.4', 'gpt-5.4',
'gpt-5.4-mini', 'gpt-5.4-mini',
@@ -79,7 +83,8 @@ const MODEL_GROUPS: Record<string, string[]> = {
'claude-haiku-4-5', 'claude-haiku-4-5',
], ],
MiniMax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'], MiniMax: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.5'],
xAI: ['grok-4.3', 'grok-build-0.1'], xAI: ['grok-4.5', 'grok-4.3', 'grok-build-0.1'],
Tencent: ['hy3'],
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'], MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'], 'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
} }
@@ -97,6 +102,7 @@ const OPENROUTER_VENDOR_SLUG: Record<string, string> = {
Anthropic: 'anthropic', Anthropic: 'anthropic',
MiniMax: 'minimax', MiniMax: 'minimax',
xAI: 'x-ai', xAI: 'x-ai',
Tencent: 'tencent',
MoonshotAI: 'moonshotai', MoonshotAI: 'moonshotai',
'Z.AI': 'z-ai', 'Z.AI': 'z-ai',
} }
@@ -106,7 +112,7 @@ const OPENROUTER_VENDOR_SLUG: Record<string, string> = {
* `<vendor-slug>/<lowercased-name>` heuristic dated snapshots, "-preview" * `<vendor-slug>/<lowercased-name>` heuristic dated snapshots, "-preview"
* suffixes, "v"-prefixed versions, or dots instead of hyphens in the * suffixes, "v"-prefixed versions, or dots instead of hyphens in the
* version number. Verified against `GET https://openrouter.ai/api/v1/models` * version number. Verified against `GET https://openrouter.ai/api/v1/models`
* on 2026-07-03; re-check when models are added to `MODEL_GROUPS`. * on 2026-07-10; re-check when models are added to `MODEL_GROUPS`.
*/ */
const OPENROUTER_ID_OVERRIDES: Record<string, string> = { const OPENROUTER_ID_OVERRIDES: Record<string, string> = {
'qwen3.6-max': 'qwen/qwen3.6-max-preview', 'qwen3.6-max': 'qwen/qwen3.6-max-preview',
+21 -6
View File
@@ -62,17 +62,26 @@ export function modelPatch(body: Record<string, any>, baseURL?: string) {
if (modelName.startsWith('gpt')) { if (modelName.startsWith('gpt')) {
if (modelName.startsWith('gpt-5')) { if (modelName.startsWith('gpt-5')) {
body.verbosity = 'low' body.verbosity = 'low'
// gpt-5 gpt-5-mini gpt-5-nano only supports "minimal";
// 5.1+ supports "none".
body.reasoning_effort = /^gpt-5(-|$)/.test(modelName) ? 'minimal' : 'none'
debug(`Patch GPT-5: verbosity=low, reasoning_effort=${body.reasoning_effort}`)
} }
// 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')) { if (modelName.includes('chat-latest')) {
debug('Omitting reasoning_effort and temperature for chat-latest') debug('Patch chat-latest: omit reasoning_effort and temperature')
delete body.reasoning_effort delete body.reasoning_effort
delete body.temperature delete body.temperature
} 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
} }
} }
@@ -125,6 +134,12 @@ export function modelPatch(body: Record<string, any>, baseURL?: string) {
body.thinking = { type: 'disabled' } 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 (modelName.startsWith('grok')) {
if (/^grok-4-?3/.test(modelName)) { if (/^grok-4-?3/.test(modelName)) {
debug('Patch Grok 4.3: reasoning_effort=none') debug('Patch Grok 4.3: reasoning_effort=none')
+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({ export default defineConfig({
test: { test: {
name: 'llms',
include: ['src/**/*.test.ts'],
// Suppress console output from passing tests; failed tests still get their logs. // Suppress console output from passing tests; failed tests still get their logs.
silent: 'passed-only', 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", "name": "@page-agent/mcp",
"private": false, "private": false,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"bin": { "bin": {
"page-agent-mcp": "src/index.js" "page-agent-mcp": "src/index.js"
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "page-agent", "name": "page-agent",
"private": false, "private": false,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"main": "./src/PageAgent.ts", "main": "./src/PageAgent.ts",
"types": "./src/PageAgent.ts", "types": "./src/PageAgent.ts",
@@ -54,10 +54,10 @@
"postpublish": "node ../../scripts/post-publish.js" "postpublish": "node ../../scripts/post-publish.js"
}, },
"dependencies": { "dependencies": {
"@page-agent/core": "1.11.0", "@page-agent/core": "1.12.1",
"@page-agent/llms": "1.11.0", "@page-agent/llms": "1.12.1",
"@page-agent/page-controller": "1.11.0", "@page-agent/page-controller": "1.12.1",
"@page-agent/ui": "1.11.0", "@page-agent/ui": "1.12.1",
"chalk": "^5.6.2" "chalk": "^5.6.2"
}, },
"peerDependencies": { "peerDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"main": "./src/PageController.ts", "main": "./src/PageController.ts",
"types": "./src/PageController.ts", "types": "./src/PageController.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
"types": "./src/index.ts", "types": "./src/index.ts",
+5 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/website", "name": "@page-agent/website",
"private": true, "private": true,
"version": "1.11.0", "version": "1.12.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",
@@ -12,19 +12,19 @@
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0", "@radix-ui/react-slot": "^1.3.0",
"@radix-ui/react-switch": "^1.3.2", "@radix-ui/react-switch": "^1.3.3",
"@radix-ui/react-tooltip": "^1.2.11", "@radix-ui/react-tooltip": "^1.2.12",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.22.0", "lucide-react": "^1.24.0",
"motion": "^12.42.2", "motion": "^12.42.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"rough-notation": "^0.5.1", "rough-notation": "^0.5.1",
"simple-icons": "^16.24.1", "simple-icons": "^16.25.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.2", "tailwindcss": "^4.3.2",
+2 -2
View File
@@ -1,8 +1,8 @@
// Demo build (auto-init with demo LLM, for quick testing) // Demo build (auto-init with demo LLM, for quick testing)
export const CDN_DEMO_URL = export const CDN_DEMO_URL =
'https://cdn.jsdelivr.net/npm/page-agent@1.11.0/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 = export const CDN_DEMO_CN_URL =
'https://registry.npmmirror.com/page-agent/1.11.0/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) // Demo LLM for website testing (homepage quick trial uses flash)
export const DEMO_MODEL = 'qwen3.5-flash' export const DEMO_MODEL = 'qwen3.5-flash'
@@ -29,6 +29,9 @@ const MODEL_GROUPS: Record<string, string[]> = {
// 'qwen3-coder-next', // low success rate // 'qwen3-coder-next', // low success rate
], ],
OpenAI: [ OpenAI: [
'gpt-5.6-sol',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5.5', 'gpt-5.5',
'gpt-5.4', 'gpt-5.4',
'gpt-5.4-mini', 'gpt-5.4-mini',
@@ -63,7 +66,8 @@ const MODEL_GROUPS: Record<string, string[]> = {
'MiniMax-M2.7', 'MiniMax-M2.7',
'MiniMax-M2.5', 'MiniMax-M2.5',
], ],
xAI: ['grok-4.3', 'grok-build-0.1'], xAI: ['grok-4.5', 'grok-4.3', 'grok-build-0.1'],
Tencent: ['hy3'],
MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'], MoonshotAI: ['kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5'],
'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'], 'Z.AI': ['glm-5.2', 'glm-5.1', 'glm-5', 'glm-4.7'],
} }