Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebc785d1fe | |||
| ba932283f2 | |||
| f05bf68f61 | |||
| 35ab12a20a | |||
| fd7c54a695 | |||
| eafd834daf | |||
| f575ae0fa4 | |||
| 797c9f8575 | |||
| fb3ac4d454 | |||
| c629d706ec | |||
| 7896786311 | |||
| 820a2654ff | |||
| 8d45ab8436 | |||
| 855020636b | |||
| ff82603839 | |||
| 90270fb76c | |||
| c9edeaea44 | |||
| 757fe07b4d | |||
| f04fa706f7 | |||
| 9daf914a89 | |||
| c8e5ff3010 | |||
| f5591dd3ea | |||
| 62a0d988ce | |||
| dc41969503 | |||
| 9d9a4b9698 | |||
| b171bf210f | |||
| c39678dece | |||
| 6c1f552606 | |||
| dcad67d4b4 | |||
| 15f3af406c | |||
| b50abcc4ec | |||
| 20f1b1f51e | |||
| cd035cd30f | |||
| a7cc935453 | |||
| 349609614b | |||
| ad680cee99 | |||
| e95b2d6a28 | |||
| f55c1b984b | |||
| 5ab34a2f50 | |||
| de24d97a22 | |||
| 317b2a1f10 | |||
| 7bc2fa4d59 | |||
| 9d5c5a8969 | |||
| 052d302f16 | |||
| bb4da7fd86 | |||
| 359e6eadc8 | |||
| e41a2609b3 | |||
| b23c909953 | |||
| 79bf9e2aee | |||
| d9d8d558a9 | |||
| 6afd3dcbdf | |||
| 5809fe3249 | |||
| e626e0b8ce | |||
| 284098ff0d | |||
| 90e1c297f4 | |||
| 23adbea744 | |||
| 9af3a3f73e | |||
| cc27ff9305 | |||
| 8a75c15557 | |||
| e9eaf44bdd |
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: pre-impl-discussion
|
name: pre-impl-discussion
|
||||||
description: "Conduct a thorough pre-implementation discussion before making significant changes. Use when the user wants to discuss, plan, or evaluate a change before implementing it — especially when they say words like 'discuss', 'evaluate', 'plan', or 'let's talk about'."
|
description: "Conduct a thorough pre-implementation discussion before making significant changes. Use when the user wants to discuss, plan, or evaluate a change before implementing it — especially when they say words like 'discuss', 'evaluate', 'plan', or 'let's talk about'."
|
||||||
argument-hint: "Describe the change to evaluate"
|
argument-hint: 'Describe the change to evaluate'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Pre-Implementation Discussion
|
# Pre-Implementation Discussion
|
||||||
@@ -40,6 +40,7 @@ Do NOT skip this step. Do NOT rely on assumptions about what "most projects" do.
|
|||||||
- **Search in parallel** to save time — batch independent queries
|
- **Search in parallel** to save time — batch independent queries
|
||||||
|
|
||||||
Common pitfalls:
|
Common pitfalls:
|
||||||
|
|
||||||
- Assuming compatibility without checking actual version constraints
|
- Assuming compatibility without checking actual version constraints
|
||||||
- Confusing roadmap/aspirations with actual released state
|
- Confusing roadmap/aspirations with actual released state
|
||||||
- Missing transitive constraints (a dependency of a dependency)
|
- Missing transitive constraints (a dependency of a dependency)
|
||||||
@@ -53,6 +54,7 @@ Share a **brief** assessment. Tables work well for comparisons. Highlight **bloc
|
|||||||
Surface the decisions the user needs to make. Present them as clear choices with trade-offs, not as a recommendation monologue.
|
Surface the decisions the user needs to make. Present them as clear choices with trade-offs, not as a recommendation monologue.
|
||||||
|
|
||||||
For each decision point:
|
For each decision point:
|
||||||
|
|
||||||
- What are the options? (2-3 max)
|
- What are the options? (2-3 max)
|
||||||
- What does each option cost or give up?
|
- What does each option cost or give up?
|
||||||
- What's your lean and why? (one sentence)
|
- What's your lean and why? (one sentence)
|
||||||
@@ -66,6 +68,7 @@ The user will ask follow-up questions, raise concerns, or challenge assumptions.
|
|||||||
- **Update your mental model** based on user feedback
|
- **Update your mental model** based on user feedback
|
||||||
|
|
||||||
Common mistakes in this phase:
|
Common mistakes in this phase:
|
||||||
|
|
||||||
- Repeating the full plan after every small clarification
|
- Repeating the full plan after every small clarification
|
||||||
- Answering a narrow question with a broad redesign
|
- Answering a narrow question with a broad redesign
|
||||||
- Treating user questions as confirmation to proceed
|
- Treating user questions as confirmation to proceed
|
||||||
@@ -84,17 +87,18 @@ Keep it terse. Tables over paragraphs. No explanations the user already heard du
|
|||||||
### 7. Wait for Confirmation
|
### 7. Wait for Confirmation
|
||||||
|
|
||||||
After presenting the final plan, **stop and wait**. The user will either:
|
After presenting the final plan, **stop and wait**. The user will either:
|
||||||
|
|
||||||
- Confirm → then (and only then) proceed to implementation
|
- Confirm → then (and only then) proceed to implementation
|
||||||
- Ask more questions → go back to step 5
|
- Ask more questions → go back to step 5
|
||||||
- Modify scope → update the plan and re-present
|
- Modify scope → update the plan and re-present
|
||||||
|
|
||||||
## Anti-Patterns
|
## Anti-Patterns
|
||||||
|
|
||||||
| Don't | Do Instead |
|
| Don't | Do Instead |
|
||||||
|-------|------------|
|
| --------------------------------------------------- | ---------------------------------------- |
|
||||||
| Start implementation "to test" without confirmation | Present findings and wait |
|
| Start implementation "to test" without confirmation | Present findings and wait |
|
||||||
| Repeat the full plan in every response | Answer the specific question asked |
|
| Repeat the full plan in every response | Answer the specific question asked |
|
||||||
| Say "X should work" without checking | Say "I need to verify X" and research it |
|
| Say "X should work" without checking | Say "I need to verify X" and research it |
|
||||||
| Assume project structure or constraints | Read the actual files |
|
| Assume project structure or constraints | Read the actual files |
|
||||||
| Present one recommendation as the only option | Present 2-3 options with trade-offs |
|
| Present one recommendation as the only option | Present 2-3 options with trade-offs |
|
||||||
| Write long paragraphs explaining trade-offs | Use tables and bullet points |
|
| Write long paragraphs explaining trade-offs | Use tables and bullet points |
|
||||||
|
|||||||
@@ -75,7 +75,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.
|
||||||
- Prefer area-specific build, lint, or test commands when they exist.
|
- Run `npm run ci` to run all build and lint checks.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
name: CI
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,6 +17,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -24,18 +26,8 @@ jobs:
|
|||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
# test on default version of npm
|
|
||||||
# - 9.6~10.8 on node@20
|
|
||||||
# - 11.3~11.6 on node@24
|
|
||||||
|
|
||||||
- name: Node and NPM version
|
|
||||||
run: node --version && npm --version
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Lint
|
- name: CI checks
|
||||||
run: npx eslint . && npx prettier --check **/*.ts
|
run: node scripts/ci.js
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|||||||
+1
-3
@@ -37,8 +37,6 @@ package.json.bak
|
|||||||
.wxt
|
.wxt
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
.agent
|
|
||||||
.claude
|
.claude
|
||||||
.cursor
|
.cursor
|
||||||
.gemini
|
.gemini
|
||||||
CLAUDE.md
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Prompt templates (formatted manually for LLM readability)
|
||||||
|
*prompt.md
|
||||||
|
|
||||||
|
# Generated
|
||||||
|
packages/extension/.wxt
|
||||||
|
|
||||||
|
# Vendored
|
||||||
|
**/components/ui
|
||||||
Vendored
+2
-1
@@ -22,7 +22,7 @@
|
|||||||
"packages/*/node_modules": true
|
"packages/*/node_modules": true
|
||||||
},
|
},
|
||||||
"markdownlint.config": {
|
"markdownlint.config": {
|
||||||
// "comment": "Relaxed rules",
|
// Relaxed rules
|
||||||
"default": true,
|
"default": true,
|
||||||
"whitespace": false,
|
"whitespace": false,
|
||||||
"line_length": false,
|
"line_length": false,
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"ol-prefix": false,
|
"ol-prefix": false,
|
||||||
"no-duplicate-heading": false
|
"no-duplicate-heading": false
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Internal packages:
|
|||||||
npm start # Start website dev server
|
npm start # Start website dev server
|
||||||
npm run build # Build all packages
|
npm run build # Build all packages
|
||||||
npm run build:libs # Build all libraries
|
npm run build:libs # Build all libraries
|
||||||
npm run typecheck # Typecheck all packages (libs + website + extension)
|
npm run build:ext # Build and zip the extension package
|
||||||
|
npm run typecheck # Typecheck all packages
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint
|
||||||
npm run zip -w @page-agent/ext # Zip the extension package
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -38,7 +38,7 @@ packages/
|
|||||||
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
||||||
├── website/ # @page-agent/website (private)
|
├── website/ # @page-agent/website (private)
|
||||||
├── llms/ # @page-agent/llms
|
├── llms/ # @page-agent/llms
|
||||||
├── extension/ # Browser extension (WXT + React)
|
├── extension/ # Browser extension
|
||||||
├── page-controller/ # @page-agent/page-controller
|
├── page-controller/ # @page-agent/page-controller
|
||||||
└── ui/ # @page-agent/ui
|
└── ui/ # @page-agent/ui
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ Fastest way to try PageAgent with our free Demo LLM:
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.8.2/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
|
||||||
|
|
||||||
|
|||||||
+23
-23
@@ -16,22 +16,22 @@ appearance, race, religion, or sexual identity and orientation.
|
|||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to creating a positive environment
|
||||||
include:
|
include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
- Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
advances
|
advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
- Publishing others' private information, such as a physical or electronic
|
||||||
address, without explicit permission
|
address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
@@ -85,19 +85,19 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
|
|||||||
|
|
||||||
有助于创造正面环境的行为包括但不限于:
|
有助于创造正面环境的行为包括但不限于:
|
||||||
|
|
||||||
* 使用友好和包容性语言
|
- 使用友好和包容性语言
|
||||||
* 尊重不同的观点和经历
|
- 尊重不同的观点和经历
|
||||||
* 耐心地接受建设性批评
|
- 耐心地接受建设性批评
|
||||||
* 关注对社区最有利的事情
|
- 关注对社区最有利的事情
|
||||||
* 友善对待其他社区成员
|
- 友善对待其他社区成员
|
||||||
|
|
||||||
身为参与者不能接受的行为包括但不限于:
|
身为参与者不能接受的行为包括但不限于:
|
||||||
|
|
||||||
* 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
|
- 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
|
||||||
* 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
|
- 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
|
||||||
* 公开或私下的骚扰
|
- 公开或私下的骚扰
|
||||||
* 未经许可地发布他人的个人资料,例如住址或是电子地址
|
- 未经许可地发布他人的个人资料,例如住址或是电子地址
|
||||||
* 其他可以被合理地认定为不恰当或者违反职业操守的行为
|
- 其他可以被合理地认定为不恰当或者违反职业操守的行为
|
||||||
|
|
||||||
## 我们的责任
|
## 我们的责任
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -49,8 +49,10 @@
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.8.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.8.2/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
|
||||||
|
|
||||||
### NPM 安装
|
### NPM 安装
|
||||||
|
|
||||||
@@ -106,4 +108,3 @@ this project possible.
|
|||||||
---
|
---
|
||||||
|
|
||||||
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
||||||
|
|
||||||
|
|||||||
+21
-33
@@ -10,52 +10,41 @@ For contribution rules and expectations, see [../CONTRIBUTING.md](../CONTRIBUTIN
|
|||||||
|
|
||||||
1. **Prerequisites**
|
1. **Prerequisites**
|
||||||
- `macOS` / `Linux` / `WSL`
|
- `macOS` / `Linux` / `WSL`
|
||||||
- `node.js >= 24` with `npm >= 11`
|
- `node.js ^22.13 || >=24` with `npm >= 11`
|
||||||
- An editor that supports `ts/eslint/prettier`
|
- An editor that supports `ts/eslint/prettier`
|
||||||
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
|
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
|
||||||
|
|
||||||
2. **Setup**
|
2. **Setup**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i
|
npm i # Or `npm ci` if you don't want to change the lockfile
|
||||||
npm start # Start demo and documentation site
|
npm start # Start website dev server
|
||||||
npm run build # Build libs and website
|
npm run build # Build everything
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦 Project Structure
|
## 📦 Project Structure
|
||||||
|
|
||||||
This is a **monorepo** with npm workspaces containing **4 main packages**:
|
This is a **monorepo** with npm workspaces.
|
||||||
|
|
||||||
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
Published packages:
|
||||||
|
|
||||||
|
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel (npm: `page-agent`)
|
||||||
|
- **MCP** (`packages/mcp/`) - MCP server for browser control via Page Agent extension (npm: `@page-agent/mcp`)
|
||||||
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
||||||
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
|
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
|
||||||
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback, independent of LLM
|
||||||
|
- **UI** (`packages/ui/`) - Panel and i18n, decoupled from PageAgent
|
||||||
|
|
||||||
> We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
Applications:
|
||||||
>
|
|
||||||
> - When developing. Use alias so that we don't have to pre-build.
|
- **Extension** (`packages/extension/`) - Browser extension (WXT + React)
|
||||||
> - When bundling. Use external and disable ts `paths` alias.
|
- **Website** (`packages/website/`) - React docs, landing page, and dev playground (private)
|
||||||
> - When bundling `IIFE` and `Website`. Bundle everything together.
|
|
||||||
|
> Source-first monorepo with `npm workspaces + ts references + vite alias`. Library `package.json` exports point to `src/*.ts` during development, and point to `dist/*.js` when published. `workspaces` in root `package.json` must be in topological order.
|
||||||
|
|
||||||
## 🤖 AGENTS.md Alias
|
## 🤖 AGENTS.md Alias
|
||||||
|
|
||||||
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
|
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it.
|
||||||
|
|
||||||
- claude-code (`CLAUDE.md`)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
@AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
- antigravity (`.agent/rules/alias.md`)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
trigger: always_on
|
|
||||||
---
|
|
||||||
|
|
||||||
@../../AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development Workflows
|
## 🔧 Development Workflows
|
||||||
|
|
||||||
@@ -85,9 +74,8 @@ If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an al
|
|||||||
### Extension Development
|
### Extension Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# make sure you ran `npm run build:libs` first and every time you changed the core libs
|
npm run dev:ext
|
||||||
npm run dev -w @page-agent/ext
|
npm run build:ext
|
||||||
npm run zip -w @page-agent/ext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Update `packages/extension/docs/extension_api.md` for API integration details
|
- Update `packages/extension/docs/extension_api.md` for API integration details
|
||||||
|
|||||||
Generated
+730
-2319
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -22,33 +22,35 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.13.0 || >=24"
|
"node": "^22.13.0 || >=24",
|
||||||
|
"npm": "^11.6.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run dev --workspace=@page-agent/website",
|
"start": "npm run dev --workspace=@page-agent/website",
|
||||||
"dev:ext": "npm run dev -w @page-agent/ext",
|
"dev:ext": "npm run dev -w @page-agent/ext",
|
||||||
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
||||||
"build": "npm run build:libs && npm run build:website",
|
"build": "node scripts/build.js",
|
||||||
"build:libs": "npm run build --workspaces --if-present",
|
"build:libs": "node scripts/build-libs.js",
|
||||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||||
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
|
"build:ext": "npm run zip -w @page-agent/ext",
|
||||||
"version": "node scripts/sync-version.js",
|
"version": "node scripts/sync-version.js",
|
||||||
"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",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"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": "^20.5.0",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.5.0",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"@eslint-react/eslint-plugin": "^4.2.3",
|
"@eslint-react/eslint-plugin": "^4.2.3",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@microsoft/api-extractor": "^7.58.2",
|
"@microsoft/api-extractor": "^7.58.7",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
@@ -58,15 +60,13 @@
|
|||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "^3.8.2",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0",
|
||||||
"vite": "^7.3.2",
|
"vite": "^8.0.12",
|
||||||
"vite-bundle-analyzer": "^1.3.7",
|
"vite-plugin-css-injected-by-js": "^5.0.1"
|
||||||
"vite-plugin-css-injected-by-js": "^4.0.1"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2"
|
||||||
"@vitejs/plugin-react": "^5.2.0"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/PageAgentCore.ts",
|
"main": "./src/PageAgentCore.ts",
|
||||||
"types": "./src/PageAgentCore.ts",
|
"types": "./src/PageAgentCore.ts",
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"@page-agent/llms": "1.8.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.8.0"
|
"@page-agent/page-controller": "1.8.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
system_prompt.md
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
|
import { InvokeError, InvokeErrorTypes } from '@page-agent/llms'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
|||||||
if (!tool) {
|
if (!tool) {
|
||||||
const available = Array.from(tools.keys()).join(', ')
|
const available = Array.from(tools.keys()).join(', ')
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
`Unknown action "${toolName}". Available: ${available}`
|
`Unknown action "${toolName}". Available: ${available}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
|||||||
const result = schema.safeParse(value)
|
const result = schema.safeParse(value)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.wxt
|
|
||||||
src/components/ui
|
|
||||||
@@ -42,33 +42,28 @@ localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
// Wait for extension injection (up to 1 second)
|
// Wait for extension injection (up to 1 second)
|
||||||
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
if (window.PAGE_AGENT_EXT) return true
|
if (window.PAGE_AGENT_EXT) return true
|
||||||
await new Promise((r) => setTimeout(r, 100))
|
await new Promise((r) => setTimeout(r, 100))
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
if (await waitForExtension()) {
|
if (await waitForExtension()) {
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
|
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
onStatusChange: (status) => console.log('Status:', status),
|
onStatusChange: (status) => console.log('Status:', status),
|
||||||
onActivity: (activity) => console.log('Activity:', activity),
|
onActivity: (activity) => console.log('Activity:', activity),
|
||||||
})
|
})
|
||||||
console.log('Result:', result)
|
console.log('Result:', result)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,10 +85,10 @@ Execute one agent task.
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
| Name | Type | Required | Description |
|
||||||
| ---- | ---- | -------- | ----------- |
|
| -------- | --------------- | -------- | ------------------------------------ |
|
||||||
| `task` | `string` | Yes | Task description |
|
| `task` | `string` | Yes | Task description |
|
||||||
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
||||||
|
|
||||||
Returns: `Promise<ExecutionResult>`
|
Returns: `Promise<ExecutionResult>`
|
||||||
|
|
||||||
@@ -106,33 +101,28 @@ Stop the current task.
|
|||||||
Install `@page-agent/core` for complete types:
|
Install `@page-agent/core` for complete types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
export interface ExecuteConfig {
|
export interface ExecuteConfig {
|
||||||
baseURL: string
|
baseURL: string
|
||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
// Global system-level instructions for the agent.
|
// Global system-level instructions for the agent.
|
||||||
// Equivalent to AgentConfig.instructions.system.
|
// Equivalent to AgentConfig.instructions.system.
|
||||||
systemInstruction?: string
|
systemInstruction?: string
|
||||||
|
|
||||||
// Include the initial tab where page JS starts. Default: true.
|
// Include the initial tab where page JS starts. Default: true.
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
|
|
||||||
// Control all unpinned tabs in the window instead of only the tab group.
|
// Control all unpinned tabs in the window instead of only the tab group.
|
||||||
// When enabled, agent sees and can switch to every non-pinned tab.
|
// When enabled, agent sees and can switch to every non-pinned tab.
|
||||||
// Default: false. Experimental.
|
// Default: false. Experimental.
|
||||||
experimentalIncludeAllTabs?: boolean
|
experimentalIncludeAllTabs?: boolean
|
||||||
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
@@ -148,31 +138,31 @@ type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentActivity =
|
type AgentActivity =
|
||||||
| { type: 'thinking' }
|
| { type: 'thinking' }
|
||||||
| { type: 'executing'; tool: string; input: unknown }
|
| { type: 'executing'; tool: string; input: unknown }
|
||||||
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
||||||
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
||||||
| { type: 'error'; message: string }
|
| { type: 'error'; message: string }
|
||||||
```
|
```
|
||||||
|
|
||||||
`HistoricalEvent`
|
`HistoricalEvent`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type HistoricalEvent =
|
type HistoricalEvent =
|
||||||
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
||||||
| { type: 'observation'; content: string }
|
| { type: 'observation'; content: string }
|
||||||
| { type: 'user_takeover' }
|
| { type: 'user_takeover' }
|
||||||
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExecutionResult`
|
`ExecutionResult`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ExecutionResult {
|
interface ExecutionResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
data: string
|
data: string
|
||||||
history: HistoricalEvent[]
|
history: HistoricalEvent[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -182,15 +172,15 @@ interface ExecutionResult {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
const result = await window.PAGE_AGENT_EXT!.execute(
|
||||||
'Fill in the email field with test@example.com and click Submit',
|
'Fill in the email field with test@example.com and click Submit',
|
||||||
{
|
{
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
includeInitialTab: false, // Optional: exclude current tab
|
includeInitialTab: false, // Optional: exclude current tab
|
||||||
onStatusChange: (status) => console.log(status),
|
onStatusChange: (status) => console.log(status),
|
||||||
onActivity: (activity) => console.log(activity),
|
onActivity: (activity) => console.log(activity),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -205,35 +195,30 @@ window.PAGE_AGENT_EXT!.stop()
|
|||||||
If you are not importing `@page-agent/core`, add:
|
If you are not importing `@page-agent/core`, add:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
interface ExecuteConfig {
|
interface ExecuteConfig {
|
||||||
baseURL: string
|
baseURL: string
|
||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
systemInstruction?: string
|
systemInstruction?: string
|
||||||
|
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
experimentalIncludeAllTabs?: boolean
|
experimentalIncludeAllTabs?: boolean
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
onActivity?: (activity: AgentActivity) => void
|
onActivity?: (activity: AgentActivity) => void
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
PAGE_AGENT_EXT_VERSION?: string
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
PAGE_AGENT_EXT?: {
|
PAGE_AGENT_EXT?: {
|
||||||
version: string
|
version: string
|
||||||
execute: Execute
|
execute: Execute
|
||||||
stop: () => void
|
stop: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
@@ -16,32 +16,32 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@types/chrome": "^0.1.40",
|
"@types/chrome": "^0.1.42",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@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.8.0",
|
"lucide-react": "^1.14.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.16.0",
|
"simple-icons": "^16.19.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wxt": "^0.20.20"
|
"wxt": "^0.20.25"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.8.0",
|
"@page-agent/core": "1.8.2",
|
||||||
"@page-agent/llms": "1.8.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.8.0",
|
"@page-agent/page-controller": "1.8.2",
|
||||||
"@page-agent/ui": "1.8.0",
|
"@page-agent/ui": "1.8.2",
|
||||||
"ai-motion": "^0.4.8",
|
"ai-motion": "^0.4.8",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extName": {
|
"extName": {
|
||||||
"message": "Page Agent Ext"
|
"message": "Page Agent Ext"
|
||||||
},
|
},
|
||||||
"extDescription": {
|
"extDescription": {
|
||||||
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
||||||
},
|
},
|
||||||
"extActionTitle": {
|
"extActionTitle": {
|
||||||
"message": "Open Page Agent"
|
"message": "Open Page Agent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extName": {
|
"extName": {
|
||||||
"message": "Page Agent Ext"
|
"message": "Page Agent Ext"
|
||||||
},
|
},
|
||||||
"extDescription": {
|
"extDescription": {
|
||||||
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
||||||
},
|
},
|
||||||
"extActionTitle": {
|
"extActionTitle": {
|
||||||
"message": "打开 Page Agent"
|
"message": "打开 Page Agent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
system_prompt.md
|
|
||||||
@@ -108,7 +108,6 @@ export function useAgent(): UseAgentResult {
|
|||||||
|
|
||||||
const execute = useCallback(async (task: string) => {
|
const execute = useCallback(async (task: string) => {
|
||||||
const agent = agentRef.current
|
const agent = agentRef.current
|
||||||
console.log('🚀 [useAgent] start executing task:', task)
|
|
||||||
if (!agent) throw new Error('Agent not initialized')
|
if (!agent) throw new Error('Agent not initialized')
|
||||||
|
|
||||||
setCurrentTask(task)
|
setCurrentTask(task)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-2 right-3 cursor-pointer"
|
className="absolute top-2 right-3 cursor-pointer"
|
||||||
|
aria-label="Back"
|
||||||
>
|
>
|
||||||
<CornerUpLeft className="size-3.5" />
|
<CornerUpLeft className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -130,12 +131,15 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
|
|
||||||
{/* User Auth Token Section */}
|
{/* User Auth Token Section */}
|
||||||
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
||||||
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
<label htmlFor="user-auth-token" className="text-xs font-medium text-muted-foreground">
|
||||||
|
User Auth Token
|
||||||
|
</label>
|
||||||
<p className="text-[10px] text-muted-foreground mb-1">
|
<p className="text-[10px] text-muted-foreground mb-1">
|
||||||
Give a website the ability to call this extension.
|
Give a website the ability to call this extension.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
|
id="user-auth-token"
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
userAuthToken
|
userAuthToken
|
||||||
@@ -152,6 +156,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
disabled={!userAuthToken}
|
disabled={!userAuthToken}
|
||||||
|
aria-label={showToken ? 'Hide token' : 'Show token'}
|
||||||
|
aria-pressed={showToken}
|
||||||
>
|
>
|
||||||
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -161,9 +167,13 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={handleCopyToken}
|
onClick={handleCopyToken}
|
||||||
disabled={!userAuthToken}
|
disabled={!userAuthToken}
|
||||||
|
aria-label="Copy token"
|
||||||
>
|
>
|
||||||
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
|
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
<span role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
||||||
|
{copied ? 'Token copied' : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -171,6 +181,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
<a
|
<a
|
||||||
href="/hub.html"
|
href="/hub.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
||||||
>
|
>
|
||||||
Manage Page Agent Hub
|
Manage Page Agent Hub
|
||||||
@@ -178,8 +189,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Base URL</label>
|
<label htmlFor="base-url" className="text-xs text-muted-foreground">
|
||||||
|
Base URL
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="base-url"
|
||||||
placeholder="https://api.openai.com/v1"
|
placeholder="https://api.openai.com/v1"
|
||||||
value={baseURL}
|
value={baseURL}
|
||||||
onChange={(e) => setBaseURL(e.target.value)}
|
onChange={(e) => setBaseURL(e.target.value)}
|
||||||
@@ -204,8 +218,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Model</label>
|
<label htmlFor="model" className="text-xs text-muted-foreground">
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="model"
|
||||||
placeholder="gpt-5.1"
|
placeholder="gpt-5.1"
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
@@ -214,9 +231,12 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">API Key</label>
|
<label htmlFor="api-key" className="text-xs text-muted-foreground">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
|
id="api-key"
|
||||||
type={showApiKey ? 'text' : 'password'}
|
type={showApiKey ? 'text' : 'password'}
|
||||||
// placeholder="sk-..."
|
// placeholder="sk-..."
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
@@ -228,6 +248,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={() => setShowApiKey(!showApiKey)}
|
onClick={() => setShowApiKey(!showApiKey)}
|
||||||
|
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
||||||
>
|
>
|
||||||
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -260,8 +281,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
{advancedOpen && (
|
{advancedOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Max Steps</label>
|
<label htmlFor="max-steps" className="text-xs text-muted-foreground">
|
||||||
|
Max Steps
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="max-steps"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="40"
|
placeholder="40"
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
|
import {
|
||||||
|
ArrowDownToLine,
|
||||||
|
ArrowLeft,
|
||||||
|
CheckCircle,
|
||||||
|
History,
|
||||||
|
RotateCcw,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -29,8 +37,13 @@ export function HistoryList({
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setSessions(await listSessions())
|
try {
|
||||||
setLoading(false)
|
setSessions(await listSessions())
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[HistoryList] Failed to load sessions:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,7 +70,14 @@ export function HistoryList({
|
|||||||
<div className="flex flex-col h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center gap-2 border-b px-3 py-2">
|
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||||
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={onBack}
|
||||||
|
className="cursor-pointer"
|
||||||
|
aria-label="Back"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
<ArrowLeft className="size-3.5" />
|
<ArrowLeft className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm font-medium flex-1">History</span>
|
<span className="text-sm font-medium flex-1">History</span>
|
||||||
@@ -80,14 +100,23 @@ export function HistoryList({
|
|||||||
{/* List */}
|
{/* List */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
<div className="flex flex-col" aria-label="Loading history" aria-busy="true">
|
||||||
Loading...
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2 px-3 py-2.5 border-b">
|
||||||
|
<div className="size-3.5 mt-0.5 rounded-full bg-muted animate-pulse shrink-0" />
|
||||||
|
<div className="flex-1 space-y-1.5">
|
||||||
|
<div className="h-2.5 bg-muted animate-pulse rounded w-3/4" />
|
||||||
|
<div className="h-2 bg-muted animate-pulse rounded w-1/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && sessions.length === 0 && (
|
{!loading && sessions.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
<div className="flex flex-col items-center justify-center h-40 gap-2 text-muted-foreground">
|
||||||
No history yet
|
<History className="size-8 opacity-30" />
|
||||||
|
<p className="text-xs">No history yet</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
|||||||
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
||||||
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
||||||
if (!messages) return null
|
if (!messages) return null
|
||||||
|
if (!Array.isArray(messages)) return null
|
||||||
const msg =
|
const msg =
|
||||||
role === 'system'
|
role === 'system'
|
||||||
? messages.find((m) => m.role === role)
|
? messages.find((m) => m.role === role)
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export default function App() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => setView({ name: 'history' })}
|
onClick={() => setView({ name: 'history' })}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
aria-label="History"
|
||||||
|
title="History"
|
||||||
>
|
>
|
||||||
<History className="size-3.5" />
|
<History className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -155,6 +157,8 @@ export default function App() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => setView({ name: 'config' })}
|
onClick={() => setView({ name: 'config' })}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
aria-label="Settings"
|
||||||
|
title="Settings"
|
||||||
>
|
>
|
||||||
<Settings className="size-3.5" />
|
<Settings className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -205,6 +209,8 @@ export default function App() {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
className="size-7"
|
className="size-7"
|
||||||
|
aria-label="Stop task"
|
||||||
|
title="Stop task"
|
||||||
>
|
>
|
||||||
<Square className="size-3" />
|
<Square className="size-3" />
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
@@ -215,6 +221,8 @@ export default function App() {
|
|||||||
onClick={() => handleSubmit()}
|
onClick={() => handleSubmit()}
|
||||||
disabled={!inputValue.trim()}
|
disabled={!inputValue.trim()}
|
||||||
className="size-7 cursor-pointer"
|
className="size-7 cursor-pointer"
|
||||||
|
aria-label="Send"
|
||||||
|
title="Send"
|
||||||
>
|
>
|
||||||
<Send className="size-3" />
|
<Send className="size-3" />
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
@@ -53,6 +53,6 @@
|
|||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
import { modelPatch, zodToOpenAITool } from './utils'
|
import { modelPatch, zodToOpenAITool } from './utils'
|
||||||
|
|
||||||
@@ -45,6 +45,17 @@ export class OpenAIClient implements LLMClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelPatch(requestBody)
|
modelPatch(requestBody)
|
||||||
|
let transformedBody: Record<string, unknown> | undefined
|
||||||
|
try {
|
||||||
|
transformedBody = this.config.transformRequestBody(requestBody)
|
||||||
|
} catch (error) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorTypes.CONFIG_ERROR,
|
||||||
|
`transformRequestBody failed: ${(error as Error).message}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const finalRequestBody = transformedBody ?? requestBody
|
||||||
|
|
||||||
// 2. Call API
|
// 2. Call API
|
||||||
let response: Response
|
let response: Response
|
||||||
@@ -55,14 +66,14 @@ export class OpenAIClient implements LLMClient {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(finalRequestBody),
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const isAbortError = (error as any)?.name === 'AbortError'
|
const isAbortError = (error as any)?.name === 'AbortError'
|
||||||
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
|
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
|
||||||
if (!isAbortError) console.error(error)
|
if (!isAbortError) console.error(error)
|
||||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
|
throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, errorMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Handle HTTP errors
|
// 3. Handle HTTP errors
|
||||||
@@ -73,27 +84,27 @@ export class OpenAIClient implements LLMClient {
|
|||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.AUTH_ERROR,
|
InvokeErrorTypes.AUTH_ERROR,
|
||||||
`Authentication failed: ${errorMessage}`,
|
`Authentication failed: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.RATE_LIMIT,
|
InvokeErrorTypes.RATE_LIMIT,
|
||||||
`Rate limit exceeded: ${errorMessage}`,
|
`Rate limit exceeded: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.status >= 500) {
|
if (response.status >= 500) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.SERVER_ERROR,
|
InvokeErrorTypes.SERVER_ERROR,
|
||||||
`Server error: ${errorMessage}`,
|
`Server error: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`HTTP ${response.status}: ${errorMessage}`,
|
`HTTP ${response.status}: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
@@ -104,7 +115,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
|
|
||||||
const choice = data.choices?.[0]
|
const choice = data.choices?.[0]
|
||||||
if (!choice) {
|
if (!choice) {
|
||||||
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
|
throw new InvokeError(InvokeErrorTypes.UNKNOWN, 'No choices in response', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check finish_reason
|
// Check finish_reason
|
||||||
@@ -115,21 +126,21 @@ export class OpenAIClient implements LLMClient {
|
|||||||
break
|
break
|
||||||
case 'length':
|
case 'length':
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.CONTEXT_LENGTH,
|
InvokeErrorTypes.CONTEXT_LENGTH,
|
||||||
'Response truncated: max tokens reached',
|
'Response truncated: max tokens reached',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
case 'content_filter':
|
case 'content_filter':
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.CONTENT_FILTER,
|
InvokeErrorTypes.CONTENT_FILTER,
|
||||||
'Content filtered by safety system',
|
'Content filtered by safety system',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`Unexpected finish_reason: ${choice.finish_reason}`,
|
`Unexpected finish_reason: ${choice.finish_reason}`,
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -144,7 +155,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
|
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
|
||||||
if (!toolCallName) {
|
if (!toolCallName) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.NO_TOOL_CALL,
|
InvokeErrorTypes.NO_TOOL_CALL,
|
||||||
'No tool call found in response',
|
'No tool call found in response',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -154,7 +165,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const tool = tools[toolCallName]
|
const tool = tools[toolCallName]
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`Tool "${toolCallName}" not found in tools`,
|
`Tool "${toolCallName}" not found in tools`,
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -165,7 +176,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
|
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
|
||||||
if (!argString) {
|
if (!argString) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'No tool call arguments found',
|
'No tool call arguments found',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -177,7 +188,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
parsedArgs = JSON.parse(argString)
|
parsedArgs = JSON.parse(argString)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'Failed to parse tool arguments as JSON',
|
'Failed to parse tool arguments as JSON',
|
||||||
error,
|
error,
|
||||||
data
|
data
|
||||||
@@ -189,7 +200,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
console.error(z.prettifyError(validation.error))
|
console.error(z.prettifyError(validation.error))
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'Tool arguments validation failed',
|
'Tool arguments validation failed',
|
||||||
validation.error,
|
validation.error,
|
||||||
data
|
data
|
||||||
@@ -203,7 +214,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
toolResult = await tool.execute(toolInput)
|
toolResult = await tool.execute(toolInput)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.TOOL_EXECUTION_ERROR,
|
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
||||||
`Tool execution failed: ${(e as Error).message}`,
|
`Tool execution failed: ${(e as Error).message}`,
|
||||||
e,
|
e,
|
||||||
data
|
data
|
||||||
@@ -225,7 +236,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
||||||
},
|
},
|
||||||
rawResponse: data,
|
rawResponse: data,
|
||||||
rawRequest: requestBody,
|
rawRequest: finalRequestBody,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Error types and error handling for LLM invocations
|
* Error types and error handling for LLM invocations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const InvokeErrorType = {
|
export const InvokeErrorTypes = {
|
||||||
// Retryable
|
// Retryable
|
||||||
NETWORK_ERROR: 'network_error', // Network error, retry
|
NETWORK_ERROR: 'network_error', // Network error, retry
|
||||||
RATE_LIMIT: 'rate_limit', // Rate limit, retry
|
RATE_LIMIT: 'rate_limit', // Rate limit, retry
|
||||||
@@ -14,12 +14,13 @@ export const InvokeErrorType = {
|
|||||||
UNKNOWN: 'unknown',
|
UNKNOWN: 'unknown',
|
||||||
|
|
||||||
// Non-retryable
|
// Non-retryable
|
||||||
|
CONFIG_ERROR: 'config_error', // Invalid local configuration or hook
|
||||||
AUTH_ERROR: 'auth_error', // Authentication failed
|
AUTH_ERROR: 'auth_error', // Authentication failed
|
||||||
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
||||||
CONTENT_FILTER: 'content_filter', // Content filtered
|
CONTENT_FILTER: 'content_filter', // Content filtered
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type InvokeErrorType = (typeof InvokeErrorType)[keyof typeof InvokeErrorType]
|
type InvokeErrorType = (typeof InvokeErrorTypes)[keyof typeof InvokeErrorTypes]
|
||||||
|
|
||||||
export class InvokeError extends Error {
|
export class InvokeError extends Error {
|
||||||
type: InvokeErrorType
|
type: InvokeErrorType
|
||||||
@@ -44,13 +45,13 @@ export class InvokeError extends Error {
|
|||||||
if (isAbortError) return false
|
if (isAbortError) return false
|
||||||
|
|
||||||
const retryableTypes: InvokeErrorType[] = [
|
const retryableTypes: InvokeErrorType[] = [
|
||||||
InvokeErrorType.NETWORK_ERROR,
|
InvokeErrorTypes.NETWORK_ERROR,
|
||||||
InvokeErrorType.RATE_LIMIT,
|
InvokeErrorTypes.RATE_LIMIT,
|
||||||
InvokeErrorType.SERVER_ERROR,
|
InvokeErrorTypes.SERVER_ERROR,
|
||||||
InvokeErrorType.NO_TOOL_CALL,
|
InvokeErrorTypes.NO_TOOL_CALL,
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
InvokeErrorType.TOOL_EXECUTION_ERROR,
|
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
]
|
]
|
||||||
return retryableTypes.includes(type)
|
return retryableTypes.includes(type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
|
|
||||||
export { InvokeError, InvokeErrorType }
|
export { InvokeError, InvokeErrorTypes }
|
||||||
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||||
@@ -21,6 +21,7 @@ export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
|||||||
apiKey: config.apiKey || '',
|
apiKey: config.apiKey || '',
|
||||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||||
|
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
||||||
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
||||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ export interface LLMConfig {
|
|||||||
temperature?: number
|
temperature?: number
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the final request body before sending it to the provider.
|
||||||
|
* Use this to implement provider-specific request tweaks such as caching hints or custom flags.
|
||||||
|
*
|
||||||
|
* Return a new object, or mutate the input object and return undefined.
|
||||||
|
*/
|
||||||
|
transformRequestBody?: (
|
||||||
|
requestBody: Record<string, unknown>
|
||||||
|
) => Record<string, unknown> | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove the tool_choice field from the request.
|
* remove the tool_choice field from the request.
|
||||||
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
debug('Applying Claude patch: convert tool_choice format')
|
debug('Applying Claude patch: convert tool_choice format')
|
||||||
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
|
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')) {
|
||||||
|
debug('Applying Claude-4.7 patch: remove temperature')
|
||||||
|
delete body.temperature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('grok')) {
|
if (modelName.startsWith('grok')) {
|
||||||
@@ -74,10 +81,12 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
debug('Applying GPT-51 patch: disable reasoning')
|
debug('Applying GPT-51 patch: disable reasoning')
|
||||||
body.reasoning_effort = 'none'
|
body.reasoning_effort = 'none'
|
||||||
} else if (modelName.startsWith('gpt-54')) {
|
} else if (modelName.startsWith('gpt-54')) {
|
||||||
debug(
|
debug('Applying GPT-5.4 patch: remove reasoning_effort')
|
||||||
'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'
|
|
||||||
)
|
|
||||||
delete body.reasoning_effort
|
delete body.reasoning_effort
|
||||||
|
} else if (modelName.startsWith('gpt-55')) {
|
||||||
|
debug('Applying GPT-5.4 patch: remove reasoning_effort and temperature')
|
||||||
|
delete body.reasoning_effort
|
||||||
|
delete body.temperature
|
||||||
} else if (modelName.startsWith('gpt-5-mini')) {
|
} else if (modelName.startsWith('gpt-5-mini')) {
|
||||||
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
||||||
body.reasoning_effort = 'low'
|
body.reasoning_effort = 'low'
|
||||||
@@ -93,6 +102,11 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
body.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('minimax')) {
|
if (modelName.startsWith('minimax')) {
|
||||||
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
||||||
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ Same format — add the config to the MCP settings of your client.
|
|||||||
|
|
||||||
## MCP Tools
|
## MCP Tools
|
||||||
|
|
||||||
| Tool | Input | Description |
|
| Tool | Input | Description |
|
||||||
| -------------- | ------------------ | ---------------------------------------------------- |
|
| -------------- | ------------------ | ----------------------------------------------------- |
|
||||||
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
||||||
| `get_status` | — | Returns `{ connected, busy }` |
|
| `get_status` | — | Returns `{ connected, busy }` |
|
||||||
| `stop_task` | — | Stop the currently running task. |
|
| `stop_task` | — | Stop the currently running task. |
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -88,7 +88,6 @@ src/
|
|||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build:libs
|
|
||||||
npm run dev:ext
|
npm run dev:ext
|
||||||
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/mcp",
|
"name": "@page-agent/mcp",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"page-agent-mcp": "src/index.js"
|
"page-agent-mcp": "src/index.js"
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
import { exec } from 'node:child_process'
|
import { exec } from 'node:child_process'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import { platform } from 'node:os'
|
import { platform } from 'node:os'
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ import { HubBridge } from './hub-bridge.js'
|
|||||||
|
|
||||||
const env = process.env
|
const env = process.env
|
||||||
const port = parseInt(env.PORT || '38401')
|
const port = parseInt(env.PORT || '38401')
|
||||||
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
/** @type {Record<string, string>} */
|
||||||
const llmConfig = {}
|
const llmConfig = {}
|
||||||
@@ -30,7 +32,7 @@ exec(`${cmd} "${url}"`, (err) => {
|
|||||||
|
|
||||||
// --- MCP server (stdio) ---
|
// --- MCP server (stdio) ---
|
||||||
|
|
||||||
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
|
const mcpServer = new McpServer({ name: 'page-agent', version })
|
||||||
|
|
||||||
mcpServer.registerTool(
|
mcpServer.registerTool(
|
||||||
'execute_task',
|
'execute_task',
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png"
|
||||||
|
/>
|
||||||
<title>Page Agent MCP Launcher</title>
|
<title>Page Agent MCP Launcher</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@@ -172,16 +175,26 @@
|
|||||||
If the extension is outdated, please update it to the latest version.
|
If the extension is outdated, please update it to the latest version.
|
||||||
</li>
|
</li>
|
||||||
<li data-i18n="tip_other_browser">
|
<li data-i18n="tip_other_browser">
|
||||||
If the extension is not installed in this browser, open this page from the
|
If the extension is not installed in this browser, open this page from the browser that
|
||||||
browser that has it installed.
|
has it installed.
|
||||||
</li>
|
</li>
|
||||||
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
|
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank" data-i18n="link_docs">Docs</a>
|
<a
|
||||||
<a href="https://github.com/alibaba/page-agent/issues" target="_blank" data-i18n="link_issues">Report an Issue</a>
|
href="https://alibaba.github.io/page-agent/docs/introduction/overview"
|
||||||
|
target="_blank"
|
||||||
|
data-i18n="link_docs"
|
||||||
|
>Docs</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/issues"
|
||||||
|
target="_blank"
|
||||||
|
data-i18n="link_issues"
|
||||||
|
>Report an Issue</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,8 +210,7 @@
|
|||||||
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
|
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
|
||||||
install_btn: '从 Chrome 应用商店安装',
|
install_btn: '从 Chrome 应用商店安装',
|
||||||
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
|
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
|
||||||
tip_other_browser:
|
tip_other_browser: '如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
|
||||||
'如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
|
|
||||||
tip_refresh: '安装或更新后,请刷新此页面。',
|
tip_refresh: '安装或更新后,请刷新此页面。',
|
||||||
link_docs: '文档',
|
link_docs: '文档',
|
||||||
link_issues: '问题反馈',
|
link_issues: '问题反馈',
|
||||||
@@ -220,13 +232,9 @@
|
|||||||
if (!globalThis.chrome?.runtime?.sendMessage) {
|
if (!globalThis.chrome?.runtime?.sendMessage) {
|
||||||
showInstall()
|
showInstall()
|
||||||
} else {
|
} else {
|
||||||
chrome.runtime.sendMessage(
|
chrome.runtime.sendMessage(EXT_ID, { type: 'OPEN_HUB', wsPort }, (response) => {
|
||||||
EXT_ID,
|
if (chrome.runtime.lastError || !response?.ok) showInstall()
|
||||||
{ type: 'OPEN_HUB', wsPort },
|
})
|
||||||
(response) => {
|
|
||||||
if (chrome.runtime.lastError || !response?.ok) showInstall()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showInstall()
|
showInstall()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/PageAgent.ts",
|
"main": "./src/PageAgent.ts",
|
||||||
"types": "./src/PageAgent.ts",
|
"types": "./src/PageAgent.ts",
|
||||||
@@ -54,16 +54,16 @@
|
|||||||
"postpublish": "node ../../scripts/post-publish.js"
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.8.0",
|
"@page-agent/core": "1.8.2",
|
||||||
"@page-agent/llms": "1.8.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.8.0",
|
"@page-agent/page-controller": "1.8.2",
|
||||||
"@page-agent/ui": "1.8.0",
|
"@page-agent/ui": "1.8.2",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
||||||
|
|
||||||
|
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||||
|
const currentScriptURL = currentScript?.src ? new URL(currentScript.src) : null
|
||||||
|
const autoInit = currentScriptURL?.searchParams.get('autoInit') !== 'false'
|
||||||
|
|
||||||
// Clean up existing instances to prevent multiple injections from bookmarklet
|
// Clean up existing instances to prevent multiple injections from bookmarklet
|
||||||
if (window.pageAgent) {
|
if (autoInit && window.pageAgent) {
|
||||||
window.pageAgent.dispose()
|
window.pageAgent.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,32 +21,35 @@ const DEMO_MODEL = 'qwen3.5-plus'
|
|||||||
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
const DEMO_API_KEY = 'NA'
|
const DEMO_API_KEY = 'NA'
|
||||||
|
|
||||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
setTimeout(() => {
|
if (autoInit) {
|
||||||
let config: PageAgentConfig
|
setTimeout(() => {
|
||||||
|
let config: PageAgentConfig
|
||||||
|
let showPanel = true
|
||||||
|
|
||||||
if (currentScript) {
|
if (currentScriptURL) {
|
||||||
console.log('🚀 page-agent.js detected current script:', currentScript.src)
|
const url = currentScriptURL
|
||||||
const url = new URL(currentScript.src)
|
const model = url.searchParams.get('model') || DEMO_MODEL
|
||||||
const model = url.searchParams.get('model') || DEMO_MODEL
|
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
||||||
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
||||||
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
||||||
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
showPanel = ((url.searchParams.get('showPanel') as 'true' | 'false') || 'true') === 'true'
|
||||||
config = { model, baseURL, apiKey, language }
|
config = { model, baseURL, apiKey, language }
|
||||||
} else {
|
} else {
|
||||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
||||||
config = {
|
config = {
|
||||||
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
|
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
|
||||||
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
|
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
|
||||||
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
|
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create agent
|
// Create agent
|
||||||
window.pageAgent = new PageAgent(config)
|
window.pageAgent = new PageAgent(config)
|
||||||
window.pageAgent.panel.show()
|
if (showPanel) {
|
||||||
|
window.pageAgent.panel.show()
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { config as dotenvConfig } from 'dotenv'
|
|||||||
import { dirname, resolve } from 'path'
|
import { dirname, resolve } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
// import { analyzer } from 'vite-bundle-analyzer'
|
|
||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/PageController.ts",
|
"main": "./src/PageController.ts",
|
||||||
"types": "./src/PageController.ts",
|
"types": "./src/PageController.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
|
|||||||
@@ -49,10 +49,34 @@
|
|||||||
<div id="sk">
|
<div id="sk">
|
||||||
<p class="sk-text" id="sk-text">Loading...</p>
|
<p class="sk-text" id="sk-text">Loading...</p>
|
||||||
<style>
|
<style>
|
||||||
#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}
|
#sk {
|
||||||
@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}
|
display: flex;
|
||||||
.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}
|
align-items: center;
|
||||||
@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #eff6ff, #f5f3ff);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#sk {
|
||||||
|
background: linear-gradient(135deg, #111827, #1f2937);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sk-text {
|
||||||
|
font:
|
||||||
|
400 14px/1 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
color: #94a3b8;
|
||||||
|
animation: skf 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes skf {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
@@ -18,16 +18,16 @@
|
|||||||
"@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.8.0",
|
"lucide-react": "^1.14.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.16.0",
|
"simple-icons": "^16.19.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type Level = 2 | 3
|
type Level = 2 | 3 | 4
|
||||||
|
|
||||||
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
||||||
id: string
|
id: string
|
||||||
@@ -11,8 +11,9 @@ interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const levelStyles = {
|
const levelStyles = {
|
||||||
2: { tag: 'h2', className: 'text-2xl font-semibold mb-4' },
|
2: { tag: 'h2', className: 'text-3xl font-semibold mb-4' },
|
||||||
3: { tag: 'h3', className: 'text-xl font-semibold mb-3' },
|
3: { tag: 'h3', className: 'text-[1.375rem] font-semibold mb-3' },
|
||||||
|
4: { tag: 'h4', className: 'text-[1.0625rem] font-medium mb-2' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
||||||
|
|||||||
@@ -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.8.0/dist/iife/page-agent.demo.js'
|
'https://cdn.jsdelivr.net/npm/page-agent@1.8.2/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.8.0/files/dist/iife/page-agent.demo.js'
|
'https://registry.npmmirror.com/page-agent/1.8.2/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'
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题使用中等字重(相对细体更重,但比默认 bold 更轻) */
|
/* 文档标题层级需要比正文标题更清晰,避免 h2/h3/h4 视觉差异过小。 */
|
||||||
.prose h1,
|
.prose h1,
|
||||||
.prose h2,
|
.prose h2,
|
||||||
.prose h3,
|
.prose h3,
|
||||||
@@ -103,6 +103,33 @@ body {
|
|||||||
.prose h5,
|
.prose h5,
|
||||||
.prose h6 {
|
.prose h6 {
|
||||||
font-weight: 480;
|
font-weight: 480;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2[id] {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.45rem;
|
||||||
|
border-bottom: 1px solid rgba(23, 23, 23, 0.12);
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3[id] {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h4[id] {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
font-weight: 560;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h2[id] {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* strong/b 也用中等字重 */
|
/* strong/b 也用中等字重 */
|
||||||
|
|||||||
@@ -156,6 +156,34 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
defaultValue: '3',
|
defaultValue: '3',
|
||||||
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'transformRequestBody',
|
||||||
|
type: '(requestBody) => Record<string, unknown> | undefined',
|
||||||
|
description: isZh ? (
|
||||||
|
<>
|
||||||
|
在请求发送前转换最终 request body。可用于处理供应商特定的缓存提示或私有参数。查看{' '}
|
||||||
|
<Link
|
||||||
|
href="/features/models#prompt-caching"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
主动缓存示例
|
||||||
|
</Link>
|
||||||
|
。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Transform the final request body before sending it. Useful for provider-specific
|
||||||
|
cache hints or private request parameters. See{' '}
|
||||||
|
<Link
|
||||||
|
href="/features/models#prompt-caching"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
prompt caching examples
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'disableNamedToolChoice',
|
name: 'disableNamedToolChoice',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
import { Heading } from '@/components/Heading'
|
import { Heading } from '@/components/Heading'
|
||||||
@@ -6,9 +7,10 @@ import { useLanguage } from '@/i18n/context'
|
|||||||
|
|
||||||
const BASELINE = new Set([
|
const BASELINE = new Set([
|
||||||
'gpt-5.1',
|
'gpt-5.1',
|
||||||
|
'gpt-5.4-mini',
|
||||||
'claude-haiku-4.5',
|
'claude-haiku-4.5',
|
||||||
'gemini-3-flash',
|
'gemini-3.1-flash-lite',
|
||||||
'deepseek-3.2',
|
'deepseek-v4-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
])
|
])
|
||||||
@@ -16,17 +18,31 @@ const BASELINE = new Set([
|
|||||||
// Models grouped by brand, newest first
|
// Models grouped by brand, newest first
|
||||||
const MODEL_GROUPS: Record<string, string[]> = {
|
const MODEL_GROUPS: Record<string, string[]> = {
|
||||||
Qwen: [
|
Qwen: [
|
||||||
|
'qwen3.6-max',
|
||||||
'qwen3.6-plus',
|
'qwen3.6-plus',
|
||||||
|
'qwen3.6-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
'qwen3-coder-next',
|
'qwen3-coder-next',
|
||||||
'qwen-3-max',
|
'qwen-3-max',
|
||||||
'qwen-3-plus',
|
'qwen-3-plus',
|
||||||
],
|
],
|
||||||
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
OpenAI: [
|
||||||
DeepSeek: ['deepseek-3.2'],
|
'gpt-5.5',
|
||||||
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.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.1-flash-lite', 'gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
|
||||||
Anthropic: [
|
Anthropic: [
|
||||||
|
'claude-opus-4.7',
|
||||||
'claude-opus-4.6',
|
'claude-opus-4.6',
|
||||||
'claude-opus-4.5',
|
'claude-opus-4.5',
|
||||||
'claude-sonnet-4.5',
|
'claude-sonnet-4.5',
|
||||||
@@ -124,46 +140,6 @@ const pageAgent = new PageAgent({
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-10">
|
|
||||||
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
|
||||||
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
|
||||||
</Heading>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
{isZh
|
|
||||||
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
|
|
||||||
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
{isZh ? (
|
|
||||||
<>
|
|
||||||
如果你计划将它集成到你的 Web 应用中,建议搭建一个后端代理来转发 LLM 请求,并使用{' '}
|
|
||||||
<code>customFetch</code> 携带 Cookie 或其他鉴权信息:
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
If you plan to integrate it into your web app, it's better to have a backend proxy for
|
|
||||||
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
|
|
||||||
other methods:
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
code={`const agent = new PageAgent({
|
|
||||||
baseURL: '/api/llm-proxy',
|
|
||||||
model: 'gpt-5.1',
|
|
||||||
customFetch: (url, init) =>
|
|
||||||
fetch(url, { ...init, credentials: 'include' }),
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
|
|
||||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
|
||||||
{isZh
|
|
||||||
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'
|
|
||||||
: '⚠️ NEVER commit real LLM API keys to your frontend code'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
@@ -213,7 +189,139 @@ LLM_MODEL_NAME="qwen3.5-plus"`}
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="local-runtimes">{isZh ? '本地运行时' : 'Local Runtimes'}</Heading>
|
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
|
||||||
|
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
如果你计划将它集成到你的 Web 应用中,建议搭建一个后端代理来转发 LLM 请求,并使用{' '}
|
||||||
|
<code>customFetch</code> 携带 Cookie 或其他鉴权信息:
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
If you plan to integrate it into your web app, it's better to have a backend proxy for
|
||||||
|
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
|
||||||
|
other methods:
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`const agent = new PageAgent({
|
||||||
|
baseURL: '/api/llm-proxy',
|
||||||
|
model: 'gpt-5.1',
|
||||||
|
customFetch: (url, init) =>
|
||||||
|
fetch(url, { ...init, credentials: 'include' }),
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
|
||||||
|
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||||
|
{isZh
|
||||||
|
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'
|
||||||
|
: '⚠️ NEVER commit real LLM API keys to your frontend code'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-10">
|
||||||
|
<Heading id="prompt-caching">{isZh ? '主动缓存' : 'Prompt Caching'}</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
一些 LLM 能从主动缓存中受益很多。由于各个供应商的主动缓存接口不同,推荐使用{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core#configuration"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
transformRequestBody
|
||||||
|
</Link>{' '}
|
||||||
|
为你的模型供应商配置缓存提示。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Some LLMs benefit significantly from prompt caching. Because each provider exposes
|
||||||
|
caching differently, use{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core#configuration"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
transformRequestBody
|
||||||
|
</Link>{' '}
|
||||||
|
to add provider-specific cache hints.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<section>
|
||||||
|
<Heading id="prompt-caching-claude" level={3}>
|
||||||
|
Claude
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? 'Claude 支持全局 Automatic prompt caching。使用兼容 Claude 的代理时,只需要在请求体顶层添加 cache_control。'
|
||||||
|
: 'Claude supports global Automatic prompt caching. When using a Claude-compatible proxy, add cache_control at the top level of the request body.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'https://your-claude-proxy.example/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'claude-sonnet-4.5',
|
||||||
|
transformRequestBody: (requestBody) => ({
|
||||||
|
...requestBody,
|
||||||
|
cache_control: { type: 'ephemeral' },
|
||||||
|
}),
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<Heading id="prompt-caching-qwen" level={3}>
|
||||||
|
{isZh ? '阿里云百炼 Qwen' : 'Alibaba Cloud Bailian Qwen'}
|
||||||
|
</Heading>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'qwen3.5-plus',
|
||||||
|
transformRequestBody: (requestBody) => {
|
||||||
|
const [systemMessage, ...restMessages] = requestBody.messages
|
||||||
|
|
||||||
|
if (systemMessage.role !== 'system' || typeof systemMessage.content !== 'string') {
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...requestBody,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
...systemMessage,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: systemMessage.content,
|
||||||
|
cache_control: { type: 'ephemeral' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...restMessages,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-10">
|
||||||
|
<Heading id="local-runtimes">{isZh ? '本地 LLMs' : 'Local LLMs'}</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
{isZh
|
{isZh
|
||||||
? '通过 Ollama、LM Studio 等本地 OpenAI-compatible 运行时接入 PageAgent,实现离线或局域网部署。'
|
? '通过 Ollama、LM Studio 等本地 OpenAI-compatible 运行时接入 PageAgent,实现离线或局域网部署。'
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export default function QuickStart() {
|
|||||||
code={`<script src="DEMO_CDN_URL" crossorigin="true"></script>`}
|
code={`<script src="DEMO_CDN_URL" crossorigin="true"></script>`}
|
||||||
language="html"
|
language="html"
|
||||||
/>
|
/>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '在 URL 后添加 ?autoInit=false 可只加载脚本,不自动创建 Demo Agent;之后可通过 new window.PageAgent(...) 手动初始化。'
|
||||||
|
: 'Add ?autoInit=false to load the script without creating the demo agent automatically. You can then instantiate it with new window.PageAgent(...).'}
|
||||||
|
</p>
|
||||||
<table className="w-full border-collapse text-sm">
|
<table className="w-full border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-200 dark:border-gray-700">
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ export default function HeroSection() {
|
|||||||
instructions: {
|
instructions: {
|
||||||
system: 'You are a helpful assistant on PageAgent website.',
|
system: 'You are a helpful assistant on PageAgent website.',
|
||||||
getPageInstructions: (url: string) => {
|
getPageInstructions: (url: string) => {
|
||||||
const hint = url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined
|
return url.includes('page-agent') ? 'This is PageAgent demo page.' : undefined
|
||||||
console.log('[instructions] getPageInstructions:', url, '->', hint)
|
|
||||||
return hint
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -98,8 +96,7 @@ export default function HeroSection() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await win.pageAgent.execute(task)
|
await win.pageAgent.execute(task)
|
||||||
console.log(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react'
|
||||||
import { config as dotenvConfig } from 'dotenv'
|
import { config as dotenvConfig } from 'dotenv'
|
||||||
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
@@ -82,8 +82,10 @@ export default defineConfig(({ mode }) => ({
|
|||||||
handler(message)
|
handler(message)
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks(id) {
|
||||||
vendor: ['react', 'react-dom', 'wouter'],
|
if (/[\\/]node_modules[\\/](react|react-dom|wouter)([\\/]|$)/.test(id)) {
|
||||||
|
return 'vendor'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Equivalent to: npm run build --workspaces --if-present
|
||||||
|
*
|
||||||
|
* Reads the workspace list from root package.json, filters to those with a
|
||||||
|
* "build" script, and runs them all concurrently via parallelTask.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { parallelTask } from './parallel-task.js'
|
||||||
|
|
||||||
|
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const rootPkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'))
|
||||||
|
|
||||||
|
const tasks = rootPkg.workspaces
|
||||||
|
.map((ws) => {
|
||||||
|
const dir = join(rootDir, ws)
|
||||||
|
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
|
||||||
|
return pkg.scripts?.build ? { label: pkg.name, command: 'npm run build', cwd: dir } : null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
await parallelTask(tasks, { timeoutMs: 120_000 })
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Full build pipeline. Equivalent to:
|
||||||
|
* npm run cleanup && npm run build --workspaces --if-present
|
||||||
|
* && npm run build:website -w @page-agent/website
|
||||||
|
* && npm run zip -w @page-agent/ext
|
||||||
|
*
|
||||||
|
* 1. cleanup
|
||||||
|
* 2. build everything in parallel (libs + website + extension)
|
||||||
|
*/
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { parallelTask } from './parallel-task.js'
|
||||||
|
|
||||||
|
const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const rootPkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'))
|
||||||
|
|
||||||
|
// Step 1: cleanup
|
||||||
|
console.log(chalk.bgBlue.white.bold(' ▸ cleanup '))
|
||||||
|
execSync('npm run cleanup', { cwd: rootDir, stdio: 'inherit' })
|
||||||
|
|
||||||
|
// Step 2: build all in parallel
|
||||||
|
console.log(chalk.bgBlue.white.bold(' ▸ build '))
|
||||||
|
const tasks = rootPkg.workspaces
|
||||||
|
.map((ws) => {
|
||||||
|
const dir = join(rootDir, ws)
|
||||||
|
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
|
||||||
|
return pkg.scripts?.build ? { label: pkg.name, command: 'npm run build', cwd: dir } : null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
{
|
||||||
|
label: '@page-agent/website',
|
||||||
|
command: 'npm run build:website',
|
||||||
|
cwd: join(rootDir, 'packages/website'),
|
||||||
|
},
|
||||||
|
{ label: '@page-agent/ext', command: 'npm run zip', cwd: join(rootDir, 'packages/extension') }
|
||||||
|
)
|
||||||
|
|
||||||
|
await parallelTask(tasks, { timeoutMs: 120_000 })
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* CI check script. Run locally before commit or in GitHub Actions.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/ci.js # run all checks
|
||||||
|
* node scripts/ci.js --no-build # skip build step
|
||||||
|
*/
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
|
import { parallelTask } from './parallel-task.js'
|
||||||
|
|
||||||
|
const args = new Set(process.argv.slice(2))
|
||||||
|
const skipBuild = args.has('--no-build')
|
||||||
|
|
||||||
|
function run(label, command) {
|
||||||
|
console.log(chalk.bgBlue.white.bold(` ▸ ${label} `))
|
||||||
|
execSync(command, { stdio: 'inherit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMainBranch() {
|
||||||
|
if (process.env.GITHUB_REF) return process.env.GITHUB_REF === 'refs/heads/main'
|
||||||
|
try {
|
||||||
|
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim() === 'main'
|
||||||
|
} catch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Commitlint — skip on main
|
||||||
|
if (isMainBranch()) {
|
||||||
|
console.log(chalk.dim(' ▸ commitlint (skipped on main)'))
|
||||||
|
} else {
|
||||||
|
const from = execSync('git merge-base origin/main HEAD', { encoding: 'utf-8' }).trim()
|
||||||
|
run('commitlint', `npx commitlint --from ${from} --to HEAD`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Lint + Format + Typecheck in parallel
|
||||||
|
console.log(chalk.bgBlue.white.bold(' ▸ lint + format + typecheck '))
|
||||||
|
await parallelTask(
|
||||||
|
[
|
||||||
|
{ label: 'lint', command: 'npm run lint' },
|
||||||
|
{ label: 'format', command: 'npx prettier --check .' },
|
||||||
|
{ label: 'typecheck', command: 'npm run typecheck' },
|
||||||
|
],
|
||||||
|
{ timeoutMs: 120_000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Build
|
||||||
|
if (skipBuild) {
|
||||||
|
console.log(chalk.dim(' ▸ build (skipped)'))
|
||||||
|
} else {
|
||||||
|
run('build', 'npm run build')
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import chalk from 'chalk'
|
||||||
|
import { spawn } from 'child_process'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run multiple shell commands in parallel with progress reporting.
|
||||||
|
*
|
||||||
|
* @param {{ label: string, command: string, cwd?: string }[]} tasks
|
||||||
|
* @param {{ timeoutMs?: number }} options - Default timeout 30s per task
|
||||||
|
* @returns {Promise<void>} Rejects (process.exit) if any task fails
|
||||||
|
*/
|
||||||
|
export async function parallelTask(tasks, options = {}) {
|
||||||
|
const { timeoutMs = 30_000 } = options
|
||||||
|
const total = tasks.length
|
||||||
|
|
||||||
|
const bgColors = [
|
||||||
|
chalk.bgCyan,
|
||||||
|
chalk.bgMagenta,
|
||||||
|
chalk.bgBlue,
|
||||||
|
chalk.bgYellow,
|
||||||
|
chalk.bgGreenBright,
|
||||||
|
]
|
||||||
|
const fgOnBg = [chalk.black, chalk.white, chalk.white, chalk.black, chalk.black]
|
||||||
|
|
||||||
|
let done = 0
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
const spinner = ['◐', '◓', '◑', '◒']
|
||||||
|
let tick = 0
|
||||||
|
|
||||||
|
const printProgress = () => {
|
||||||
|
const running = total - done - failed
|
||||||
|
const s = spinner[tick++ % spinner.length]
|
||||||
|
const status = failed
|
||||||
|
? `${running} running, ${done} done, ${chalk.bgRed.white.bold(` ${failed} failed `)}`
|
||||||
|
: `${running} running, ${done} done`
|
||||||
|
process.stderr.write(`\r${chalk.bgCyan.black.bold(` ${s} ${done}/${total} `)} ${status} `)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setInterval(printProgress, 1000)
|
||||||
|
printProgress()
|
||||||
|
|
||||||
|
/** @type {{ label: string, output: string, exitCode: number | null, timedOut: boolean }[]} */
|
||||||
|
const results = await Promise.all(
|
||||||
|
tasks.map(
|
||||||
|
(task) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const chunks = /** @type {Buffer[]} */ ([])
|
||||||
|
|
||||||
|
const child = spawn('sh', ['-c', task.command], {
|
||||||
|
cwd: task.cwd,
|
||||||
|
env: { ...process.env, FORCE_COLOR: '1', NO_COLOR: '' },
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
|
||||||
|
child.stdout.on('data', (d) => chunks.push(d))
|
||||||
|
child.stderr.on('data', (d) => chunks.push(d))
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
child.kill('SIGTERM')
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
|
child.on('close', (code, signal) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
const timedOut = signal === 'SIGTERM'
|
||||||
|
const exitCode = timedOut ? 1 : code
|
||||||
|
|
||||||
|
if (exitCode === 0) done++
|
||||||
|
else failed++
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
label: task.label,
|
||||||
|
output: Buffer.concat(chunks).toString(),
|
||||||
|
exitCode,
|
||||||
|
timedOut,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
clearInterval(timer)
|
||||||
|
process.stderr.write('\r\x1b[K')
|
||||||
|
|
||||||
|
const failedTasks = /** @type {typeof results} */ ([])
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const r = results[i]
|
||||||
|
if (r.exitCode !== 0) {
|
||||||
|
failedTasks.push(r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const bg = bgColors[i % bgColors.length]
|
||||||
|
const fg = fgOnBg[i % fgOnBg.length]
|
||||||
|
const banner = bg(fg.bold(` ✔ ${r.label} `))
|
||||||
|
console.log(`\n${banner}`)
|
||||||
|
if (r.output.trim()) process.stdout.write(r.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedTasks.length) {
|
||||||
|
for (const r of failedTasks) {
|
||||||
|
const banner = chalk.bgRed(
|
||||||
|
chalk.white.bold(` ✘ ${r.label} ${r.timedOut ? '· timed out' : '· failed'} `)
|
||||||
|
)
|
||||||
|
console.log(`\n${banner}`)
|
||||||
|
if (r.output.trim()) process.stdout.write(r.output)
|
||||||
|
}
|
||||||
|
const summary = failedTasks.map((t) => t.label).join(', ')
|
||||||
|
console.error(
|
||||||
|
`\n${chalk.bgRed.white.bold(` ✘ ${failedTasks.length}/${total} failed: ${summary} `)}`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${chalk.bgGreen.black.bold(` ✔ All ${total} tasks completed `)}`)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user