Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb55daf47d | |||
| 43415d3139 | |||
| 9cc9edf17e | |||
| e290e2f67a | |||
| 4272939217 | |||
| 148bdb6839 | |||
| 68cee4cc64 | |||
| 7e9027167d | |||
| 6630774dc9 | |||
| 0ca1c8de0b | |||
| 85a33ac1a4 | |||
| b8fd1aaebc | |||
| 13d952dc9d | |||
| 1f2f5a44d3 | |||
| 8159aa58a6 | |||
| 2bdb3be81c | |||
| bde630f55d | |||
| 3efef0ec42 | |||
| 1141287733 | |||
| ec08b05da1 | |||
| 5ea2edd13c | |||
| 1999bbeda9 | |||
| b3ec571341 | |||
| d0de8ad838 | |||
| 0402a6be03 | |||
| f2b6c9dfd2 | |||
| 005bc8ab44 |
@@ -50,8 +50,8 @@ Fastest way to try PageAgent with our free Demo LLM:
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.1/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.7.1/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
### NPM Installation
|
### NPM Installation
|
||||||
|
|
||||||
@@ -74,6 +74,16 @@ await agent.execute('Click the login button')
|
|||||||
|
|
||||||
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
|
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
|
||||||
|
|
||||||
|
## 🌟 Awesome Page Agent
|
||||||
|
|
||||||
|
Built something cool with PageAgent? Add it here! Open a PR to share your project.
|
||||||
|
|
||||||
|
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
|
||||||
|
|
||||||
|
| Project | Description |
|
||||||
|
| -------- | ----------------------------------------------------------- |
|
||||||
|
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
|
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
|
||||||
@@ -104,16 +114,6 @@ excellent work on web automation and DOM interaction patterns that helped make
|
|||||||
this project possible.
|
this project possible.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌟 Awesome Page Agent
|
|
||||||
|
|
||||||
Built something cool with PageAgent? Add it here! Open a PR to share your project.
|
|
||||||
|
|
||||||
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
|
|
||||||
|
|
||||||
| Project | Description |
|
|
||||||
| -------- | ----------------------------------------------------------- |
|
|
||||||
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**⭐ Star this repo if you find PageAgent helpful!**
|
**⭐ Star this repo if you find PageAgent helpful!**
|
||||||
|
|||||||
+2
-2
@@ -49,8 +49,8 @@
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.1/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.7.1/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
### NPM 安装
|
### NPM 安装
|
||||||
|
|
||||||
|
|||||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -11799,11 +11799,11 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.7.1",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -11815,13 +11815,13 @@
|
|||||||
},
|
},
|
||||||
"packages/extension": {
|
"packages/extension": {
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.7.1",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.7.1",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.7.1",
|
||||||
"ai-motion": "^0.4.8",
|
"ai-motion": "^0.4.8",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
@@ -11858,7 +11858,7 @@
|
|||||||
},
|
},
|
||||||
"packages/llms": {
|
"packages/llms": {
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
@@ -11872,7 +11872,7 @@
|
|||||||
},
|
},
|
||||||
"packages/mcp": {
|
"packages/mcp": {
|
||||||
"name": "@page-agent/mcp",
|
"name": "@page-agent/mcp",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
@@ -11887,13 +11887,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/page-agent": {
|
"packages/page-agent": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.7.1",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.7.1",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.7.1",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -11905,7 +11905,7 @@
|
|||||||
},
|
},
|
||||||
"packages/page-controller": {
|
"packages/page-controller": {
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ai-motion": "^0.4.8"
|
"ai-motion": "^0.4.8"
|
||||||
@@ -11913,12 +11913,12 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"packages/website": {
|
"packages/website": {
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent-core.js",
|
"main": "./dist/esm/page-agent-core.js",
|
||||||
"module": "./dist/esm/page-agent-core.js",
|
"module": "./dist/esm/page-agent-core.js",
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0"
|
"@page-agent/page-controller": "1.7.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ tools.set(
|
|||||||
tools.set(
|
tools.set(
|
||||||
'scroll',
|
'scroll',
|
||||||
tool({
|
tool({
|
||||||
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
description:
|
||||||
|
'Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
down: z.boolean().default(true),
|
down: z.boolean().default(true),
|
||||||
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
||||||
@@ -155,7 +156,7 @@ tools.set(
|
|||||||
'scroll_horizontally',
|
'scroll_horizontally',
|
||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
|
'Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
right: z.boolean().default(true),
|
right: z.boolean().default(true),
|
||||||
pixels: z.number().int().min(0),
|
pixels: z.number().int().min(0),
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ export interface ExecuteConfig {
|
|||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
|
// Global system-level instructions for the agent.
|
||||||
|
// Equivalent to AgentConfig.instructions.system.
|
||||||
|
systemInstruction?: string
|
||||||
|
|
||||||
// Include the initial tab where page JS starts. Default: true.
|
// Include the initial tab where page JS starts. Default: true.
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
|
|
||||||
@@ -212,6 +216,9 @@ interface ExecuteConfig {
|
|||||||
baseURL: string
|
baseURL: string
|
||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
|
systemInstruction?: string
|
||||||
|
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
experimentalIncludeAllTabs?: boolean
|
experimentalIncludeAllTabs?: boolean
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
"wxt": "^0.20.20"
|
"wxt": "^0.20.20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.7.1",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.7.1",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.7.1",
|
||||||
"ai-motion": "^0.4.8",
|
"ai-motion": "^0.4.8",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export function initPageController() {
|
|||||||
|
|
||||||
function getPC(): PageController {
|
function getPC(): PageController {
|
||||||
if (!pageController) {
|
if (!pageController) {
|
||||||
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
|
pageController = new PageController({
|
||||||
|
enableMask: false,
|
||||||
|
viewportExpansion: 400,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return pageController
|
return pageController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export class TabsController {
|
|||||||
this.port = null
|
this.port = null
|
||||||
if (this.disposed) return
|
if (this.disposed) return
|
||||||
if (this.portRetries >= 7) {
|
if (this.portRetries >= 7) {
|
||||||
console.error(PREFIX, 'tab events port failed after 3 retries, giving up')
|
console.error(PREFIX, 'tab events port failed after 7 retries, giving up')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
debug('port disconnected, reconnecting...')
|
debug('port disconnected, reconnecting...')
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ async function exposeAgentToPage() {
|
|||||||
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
|
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
|
||||||
|
|
||||||
window.addEventListener('message', async (e) => {
|
window.addEventListener('message', async (e) => {
|
||||||
|
if (e.source !== window) return
|
||||||
|
|
||||||
const data = e.data
|
const data = e.data
|
||||||
if (typeof data !== 'object' || data === null) return
|
if (typeof data !== 'object' || data === null) return
|
||||||
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
|
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
|
||||||
@@ -70,11 +72,15 @@ async function exposeAgentToPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { task, config } = payload
|
const { task, config } = payload
|
||||||
|
const { systemInstruction, ...agentConfig } = config
|
||||||
|
|
||||||
// Dispose old instance before creating new one
|
// Dispose old instance before creating new one
|
||||||
multiPageAgent?.dispose()
|
multiPageAgent?.dispose()
|
||||||
|
|
||||||
multiPageAgent = new MultiPageAgent(config)
|
multiPageAgent = new MultiPageAgent({
|
||||||
|
...agentConfig,
|
||||||
|
instructions: systemInstruction ? { system: systemInstruction } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
// events
|
// events
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export interface ExecuteConfig {
|
|||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global system-level instructions for the agent.
|
||||||
|
* Equivalent to `AgentConfig.instructions.system`.
|
||||||
|
*/
|
||||||
|
systemInstruction?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to include the initial tab (that holds this main world script) in the task.
|
* Whether to include the initial tab (that holds this main world script) in the task.
|
||||||
* @default true
|
* @default true
|
||||||
@@ -39,6 +45,8 @@ export default defineUnlistedScript(() => {
|
|||||||
|
|
||||||
const promise = new Promise<ExecutionResult>((resolve, reject) => {
|
const promise = new Promise<ExecutionResult>((resolve, reject) => {
|
||||||
function handleMessage(e: MessageEvent) {
|
function handleMessage(e: MessageEvent) {
|
||||||
|
if (e.source !== window) return
|
||||||
|
|
||||||
const data = e.data
|
const data = e.data
|
||||||
if (typeof data !== 'object' || data === null) return
|
if (typeof data !== 'object' || data === null) return
|
||||||
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
|
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
|
||||||
@@ -89,6 +97,7 @@ export default defineUnlistedScript(() => {
|
|||||||
baseURL: config.baseURL,
|
baseURL: config.baseURL,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
|
systemInstruction: config.systemInstruction,
|
||||||
includeInitialTab: config.includeInitialTab,
|
includeInitialTab: config.includeInitialTab,
|
||||||
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
|
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/mcp",
|
"name": "@page-agent/mcp",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"page-agent-mcp": "src/index.js"
|
"page-agent-mcp": "src/index.js"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./dist/esm/page-agent.js",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"module": "./dist/esm/page-agent.js",
|
||||||
@@ -44,10 +44,10 @@
|
|||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.7.1",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.7.1",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.7.1",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.7.1",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ const DEMO_MODEL = 'qwen3.5-plus'
|
|||||||
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
||||||
const DEMO_API_KEY = 'NA'
|
const DEMO_API_KEY = 'NA'
|
||||||
|
|
||||||
|
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
|
||||||
let config: PageAgentConfig
|
let config: PageAgentConfig
|
||||||
|
|
||||||
if (currentScript) {
|
if (currentScript) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./dist/lib/page-controller.js",
|
||||||
"module": "./dist/lib/page-controller.js",
|
"module": "./dist/lib/page-controller.js",
|
||||||
|
|||||||
@@ -193,7 +193,11 @@ export class PageController extends EventTarget {
|
|||||||
interactiveBlacklist: blacklist,
|
interactiveBlacklist: blacklist,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes)
|
this.simplifiedHTML = dom.flatTreeToString(
|
||||||
|
this.flatTree,
|
||||||
|
this.config.includeAttributes,
|
||||||
|
this.config.keepSemanticTags
|
||||||
|
)
|
||||||
|
|
||||||
this.selectorMap.clear()
|
this.selectorMap.clear()
|
||||||
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
||||||
@@ -322,11 +326,11 @@ export class PageController extends EventTarget {
|
|||||||
|
|
||||||
this.assertIndexed()
|
this.assertIndexed()
|
||||||
|
|
||||||
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
|
const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1)
|
||||||
|
|
||||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||||
|
|
||||||
const message = await scrollVertically(down, scrollAmount, element)
|
const message = await scrollVertically(scrollAmount, element)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -357,7 +361,7 @@ export class PageController extends EventTarget {
|
|||||||
|
|
||||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||||
|
|
||||||
const message = await scrollHorizontally(right, scrollAmount, element)
|
const message = await scrollHorizontally(scrollAmount, element)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -274,14 +274,7 @@ export async function scrollIntoViewIfNeeded(element: Element) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function scrollVertically(scroll_amount: number, element?: HTMLElement | null) {
|
||||||
* @private Internal method, subject to change at any time.
|
|
||||||
*/
|
|
||||||
export async function scrollVertically(
|
|
||||||
down: boolean,
|
|
||||||
scroll_amount: number,
|
|
||||||
element?: HTMLElement | null
|
|
||||||
) {
|
|
||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
@@ -294,7 +287,10 @@ export async function scrollVertically(
|
|||||||
|
|
||||||
while (currentElement && attempts < 10) {
|
while (currentElement && attempts < 10) {
|
||||||
const computedStyle = window.getComputedStyle(currentElement)
|
const computedStyle = window.getComputedStyle(currentElement)
|
||||||
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
const hasScrollableY =
|
||||||
|
/(auto|scroll|overlay)/.test(computedStyle.overflowY) ||
|
||||||
|
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
|
||||||
|
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
|
||||||
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
||||||
|
|
||||||
if (hasScrollableY && canScrollVertically) {
|
if (hasScrollableY && canScrollVertically) {
|
||||||
@@ -346,9 +342,20 @@ export async function scrollVertically(
|
|||||||
el.scrollHeight > el.clientHeight &&
|
el.scrollHeight > el.clientHeight &&
|
||||||
bigEnough(el)
|
bigEnough(el)
|
||||||
|
|
||||||
|
// @deprecated Heuristic container search.
|
||||||
|
// Unreliable in multi-panel layouts. Should guide LLMs to use indexed scroll for consistency.
|
||||||
|
// TODO: remove this fallback
|
||||||
|
|
||||||
|
// try to find the nearest scrollable container
|
||||||
|
// document.activeElement is usually body.
|
||||||
|
// After a successful element.focus(), activeElement become the nearest focusable parent
|
||||||
|
|
||||||
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
||||||
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
||||||
|
|
||||||
|
// Something is wrong if it falls back to global '*' search
|
||||||
|
// TODO: Return error message instead of global '*' search
|
||||||
|
|
||||||
el = canScroll(el)
|
el = canScroll(el)
|
||||||
? el
|
? el
|
||||||
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
|
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
|
||||||
@@ -379,6 +386,10 @@ export async function scrollVertically(
|
|||||||
return `✅ Scrolled page by ${scrolled}px.`
|
return `✅ Scrolled page by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
// Container scroll
|
// Container scroll
|
||||||
|
|
||||||
|
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
|
||||||
|
console.log(`[PageController] ${warningMsg}`)
|
||||||
|
|
||||||
const scrollBefore = el!.scrollTop
|
const scrollBefore = el!.scrollTop
|
||||||
const scrollMax = el!.scrollHeight - el!.clientHeight
|
const scrollMax = el!.scrollHeight - el!.clientHeight
|
||||||
|
|
||||||
@@ -390,29 +401,22 @@ export async function scrollVertically(
|
|||||||
|
|
||||||
if (Math.abs(scrolled) < 1) {
|
if (Math.abs(scrolled) < 1) {
|
||||||
return dy > 0
|
return dy > 0
|
||||||
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
|
? `⚠️ ${warningMsg} Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
|
||||||
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
|
: `⚠️ ${warningMsg} Already at the top of container (${el!.tagName}), cannot scroll up further.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
||||||
const reachedTop = dy < 0 && scrollAfter <= 1
|
const reachedTop = dy < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
if (reachedBottom)
|
if (reachedBottom)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
|
||||||
if (reachedTop)
|
if (reachedTop)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function scrollHorizontally(scroll_amount: number, element?: HTMLElement | null) {
|
||||||
* @private Internal method, subject to change at any time.
|
|
||||||
*/
|
|
||||||
export async function scrollHorizontally(
|
|
||||||
right: boolean,
|
|
||||||
scroll_amount: number,
|
|
||||||
element?: HTMLElement | null
|
|
||||||
) {
|
|
||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
@@ -421,11 +425,14 @@ export async function scrollHorizontally(
|
|||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
let scrollDelta = 0
|
let scrollDelta = 0
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
const dx = right ? scroll_amount : -scroll_amount
|
const dx = scroll_amount
|
||||||
|
|
||||||
while (currentElement && attempts < 10) {
|
while (currentElement && attempts < 10) {
|
||||||
const computedStyle = window.getComputedStyle(currentElement)
|
const computedStyle = window.getComputedStyle(currentElement)
|
||||||
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
const hasScrollableX =
|
||||||
|
/(auto|scroll|overlay)/.test(computedStyle.overflowX) ||
|
||||||
|
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
|
||||||
|
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
|
||||||
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
||||||
|
|
||||||
if (hasScrollableX && canScrollHorizontally) {
|
if (hasScrollableX && canScrollHorizontally) {
|
||||||
@@ -469,7 +476,8 @@ export async function scrollHorizontally(
|
|||||||
|
|
||||||
// Page-level scrolling (default or fallback)
|
// Page-level scrolling (default or fallback)
|
||||||
|
|
||||||
const dx = right ? scroll_amount : -scroll_amount
|
const dx = scroll_amount
|
||||||
|
|
||||||
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
|
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
|
||||||
const canScroll = (el: HTMLElement | null) =>
|
const canScroll = (el: HTMLElement | null) =>
|
||||||
el &&
|
el &&
|
||||||
@@ -477,6 +485,9 @@ export async function scrollHorizontally(
|
|||||||
el.scrollWidth > el.clientWidth &&
|
el.scrollWidth > el.clientWidth &&
|
||||||
bigEnough(el)
|
bigEnough(el)
|
||||||
|
|
||||||
|
// @deprecated Same heuristic container search as scrollVertically.
|
||||||
|
// TODO: Remove once LLMs reliably use indexed scrolling via data-scrollable.
|
||||||
|
|
||||||
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
||||||
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
||||||
|
|
||||||
@@ -511,6 +522,9 @@ export async function scrollHorizontally(
|
|||||||
return `✅ Scrolled page horizontally by ${scrolled}px.`
|
return `✅ Scrolled page horizontally by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
// Container scroll
|
// Container scroll
|
||||||
|
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
|
||||||
|
console.log(`[PageController] ${warningMsg}`)
|
||||||
|
|
||||||
const scrollBefore = el!.scrollLeft
|
const scrollBefore = el!.scrollLeft
|
||||||
const scrollMax = el!.scrollWidth - el!.clientWidth
|
const scrollMax = el!.scrollWidth - el!.clientWidth
|
||||||
|
|
||||||
@@ -522,17 +536,17 @@ export async function scrollHorizontally(
|
|||||||
|
|
||||||
if (Math.abs(scrolled) < 1) {
|
if (Math.abs(scrolled) < 1) {
|
||||||
return dx > 0
|
return dx > 0
|
||||||
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
|
? `⚠️ ${warningMsg} Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
|
||||||
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
|
: `⚠️ ${warningMsg} Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
||||||
const reachedLeft = dx < 0 && scrollAfter <= 1
|
const reachedLeft = dx < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
if (reachedRight)
|
if (reachedRight)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
|
||||||
if (reachedLeft)
|
if (reachedLeft)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
|
||||||
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
* @edit improve `sampleRect`, filter out rects with 0 area
|
* @edit improve `sampleRect`, filter out rects with 0 area
|
||||||
* @edit exclude aria-hidden elements
|
* @edit exclude aria-hidden elements
|
||||||
* @edit make sure attributes exist for interactive candidates.
|
* @edit make sure attributes exist for interactive candidates.
|
||||||
|
* @edit fix "aria-*" attributes check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
@@ -502,11 +503,16 @@ export default (
|
|||||||
const overflowX = style.overflowX
|
const overflowX = style.overflowX
|
||||||
const overflowY = style.overflowY
|
const overflowY = style.overflowY
|
||||||
|
|
||||||
// Check scrollable distances
|
// scrollbar-width/scrollbar-gutter are only set on elements designed to scroll;
|
||||||
|
// their presence signals scroll intent even when overflow is hidden (e.g. overflow: auto on :hover)
|
||||||
|
const hasScrollbarSignal =
|
||||||
|
(style.scrollbarWidth && style.scrollbarWidth !== 'auto') ||
|
||||||
|
(style.scrollbarGutter && style.scrollbarGutter !== 'auto')
|
||||||
|
|
||||||
const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
|
const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
|
||||||
const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
|
const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
|
||||||
|
|
||||||
if (!scrollableX && !scrollableY) {
|
if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
|
||||||
return null // Not scrollable in any direction
|
return null // Not scrollable in any direction
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,11 +526,11 @@ export default (
|
|||||||
return null // Not scrollable
|
return null // Not scrollable
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollableY && scrollWidth < threshold) {
|
if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
|
||||||
return null // Not scrollable horizontally
|
return null // Not scrollable horizontally
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollableX && scrollHeight < threshold) {
|
if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
|
||||||
return null // Not scrollable vertically
|
return null // Not scrollable vertically
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +552,8 @@ export default (
|
|||||||
scrollData: scrollData,
|
scrollData: scrollData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('scrollData!!!', scrollData)
|
||||||
|
|
||||||
return scrollData
|
return scrollData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1151,31 @@ export default (
|
|||||||
* @param {HTMLElement} element - The element to check.
|
* @param {HTMLElement} element - The element to check.
|
||||||
* @returns {boolean} Whether the element is an interactive candidate.
|
* @returns {boolean} Whether the element is an interactive candidate.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @edit fix "aria-*" attributes check
|
||||||
|
const INTERACTIVE_ARIA_ATTRS = [
|
||||||
|
'aria-expanded',
|
||||||
|
'aria-checked',
|
||||||
|
'aria-selected',
|
||||||
|
'aria-pressed',
|
||||||
|
'aria-haspopup',
|
||||||
|
'aria-controls',
|
||||||
|
'aria-owns',
|
||||||
|
'aria-activedescendant',
|
||||||
|
'aria-valuenow',
|
||||||
|
'aria-valuetext',
|
||||||
|
'aria-valuemax',
|
||||||
|
'aria-valuemin',
|
||||||
|
'aria-autocomplete',
|
||||||
|
]
|
||||||
|
|
||||||
|
function hasInteractiveAria(el) {
|
||||||
|
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
|
||||||
|
if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function isInteractiveCandidate(element) {
|
function isInteractiveCandidate(element) {
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
|
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
|
||||||
|
|
||||||
@@ -1167,7 +1200,7 @@ export default (
|
|||||||
element.hasAttribute('onclick') ||
|
element.hasAttribute('onclick') ||
|
||||||
element.hasAttribute('role') ||
|
element.hasAttribute('role') ||
|
||||||
element.hasAttribute('tabindex') ||
|
element.hasAttribute('tabindex') ||
|
||||||
element.hasAttribute('aria-') ||
|
hasInteractiveAria(element) ||
|
||||||
element.hasAttribute('data-action') ||
|
element.hasAttribute('data-action') ||
|
||||||
element.getAttribute('contenteditable') === 'true'
|
element.getAttribute('contenteditable') === 'true'
|
||||||
|
|
||||||
@@ -1185,8 +1218,9 @@ export default (
|
|||||||
'details',
|
'details',
|
||||||
'label',
|
'label',
|
||||||
'option',
|
'option',
|
||||||
|
'li',
|
||||||
])
|
])
|
||||||
const INTERACTIVE_ROLES = new Set([
|
const DISTINCT_INTERACTIVE_ROLES = new Set([
|
||||||
'button',
|
'button',
|
||||||
'link',
|
'link',
|
||||||
'menuitem',
|
'menuitem',
|
||||||
@@ -1202,6 +1236,9 @@ export default (
|
|||||||
'searchbox',
|
'searchbox',
|
||||||
'textbox',
|
'textbox',
|
||||||
'listbox',
|
'listbox',
|
||||||
|
'listitem',
|
||||||
|
'treeitem',
|
||||||
|
'row',
|
||||||
'option',
|
'option',
|
||||||
'scrollbar',
|
'scrollbar',
|
||||||
])
|
])
|
||||||
@@ -1278,7 +1315,7 @@ export default (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Check interactive roles
|
// Check interactive roles
|
||||||
if (role && INTERACTIVE_ROLES.has(role)) {
|
if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Check contenteditable
|
// Check contenteditable
|
||||||
@@ -1297,6 +1334,10 @@ export default (
|
|||||||
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
|
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// ARIA state attributes imply the element manages its own interaction state
|
||||||
|
if (hasInteractiveAria(element)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// return false
|
// return false
|
||||||
|
|
||||||
@@ -1352,6 +1393,12 @@ export default (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrollable containers are always distinct — the LLM needs their index for targeted scrolling.
|
||||||
|
// Check extraData (already set by isScrollableElement in isInteractiveElement) to avoid redundant layout reads.
|
||||||
|
if (extraData.get(element)?.scrollable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Default to false: if it's interactive but doesn't match above,
|
// Default to false: if it's interactive but doesn't match above,
|
||||||
// assume it triggers the same action as the parent.
|
// assume it triggers the same action as the parent.
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -28,8 +28,27 @@ export interface DomConfig {
|
|||||||
includeAttributes?: string[]
|
includeAttributes?: string[]
|
||||||
highlightOpacity?: number
|
highlightOpacity?: number
|
||||||
highlightLabelOpacity?: number
|
highlightLabelOpacity?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preserve semantic landmark tags in dehydrated output even if not interactive
|
||||||
|
* @note maybe confusing for LLM combining with page scrolling, use with caution
|
||||||
|
**/
|
||||||
|
keepSemanticTags?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: corresponding roles
|
||||||
|
const SEMANTIC_TAGS = new Set([
|
||||||
|
'nav',
|
||||||
|
'menu',
|
||||||
|
// 'main',
|
||||||
|
'header',
|
||||||
|
'footer',
|
||||||
|
'aside',
|
||||||
|
// 'article',
|
||||||
|
// 'form',
|
||||||
|
'dialog',
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于检测可交互元素是否是新出现的。
|
* 用于检测可交互元素是否是新出现的。
|
||||||
*/
|
*/
|
||||||
@@ -171,7 +190,11 @@ interface TreeNode {
|
|||||||
*
|
*
|
||||||
* @todo 数据脱敏过滤器
|
* @todo 数据脱敏过滤器
|
||||||
*/
|
*/
|
||||||
export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string {
|
export function flatTreeToString(
|
||||||
|
flatTree: FlatDomTree,
|
||||||
|
includeAttributes: string[] = [],
|
||||||
|
keepSemanticTags = false
|
||||||
|
): string {
|
||||||
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
||||||
'title',
|
'title',
|
||||||
'type',
|
'type',
|
||||||
@@ -203,7 +226,7 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
'contenteditable',
|
'contenteditable',
|
||||||
]
|
]
|
||||||
|
|
||||||
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
|
const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES]
|
||||||
|
|
||||||
// Helper function to cap text length
|
// Helper function to cap text length
|
||||||
const capTextLength = (text: string, maxLength: number): string => {
|
const capTextLength = (text: string, maxLength: number): string => {
|
||||||
@@ -294,6 +317,8 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
const depthStr = '\t'.repeat(depth)
|
const depthStr = '\t'.repeat(depth)
|
||||||
|
|
||||||
if (node.type === 'element') {
|
if (node.type === 'element') {
|
||||||
|
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName)
|
||||||
|
|
||||||
// Add element with highlight_index
|
// Add element with highlight_index
|
||||||
if (node.highlightIndex !== undefined) {
|
if (node.highlightIndex !== undefined) {
|
||||||
nextDepth += 1
|
nextDepth += 1
|
||||||
@@ -391,10 +416,30 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
result.push(line)
|
result.push(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process children regardless
|
// special treatment for semantic tags
|
||||||
|
// even if they are not interactive, we can keep them for clear context
|
||||||
|
|
||||||
|
const emitSemantic = isSemantic && node.highlightIndex === undefined
|
||||||
|
// to check if this tag is empty
|
||||||
|
const mark = emitSemantic ? result.length : -1
|
||||||
|
|
||||||
|
if (emitSemantic) {
|
||||||
|
result.push(`${depthStr}<${node.tagName}>`)
|
||||||
|
nextDepth += 1
|
||||||
|
}
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
processNode(child, nextDepth, result)
|
processNode(child, nextDepth, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emitSemantic) {
|
||||||
|
// empty tag should be removed
|
||||||
|
if (result.length === mark + 1) {
|
||||||
|
result.pop()
|
||||||
|
} else {
|
||||||
|
result.push(`${depthStr}</${node.tagName}>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (node.type === 'text') {
|
} else if (node.type === 'text') {
|
||||||
// Add text only if it doesn't have a highlighted parent
|
// Add text only if it doesn't have a highlighted parent
|
||||||
if (hasParentWithHighlightIndex(node)) {
|
if (hasParentWithHighlightIndex(node)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Uses wouter browser routing with base path for GitHub Pages deployment at `https
|
|||||||
```tsx
|
```tsx
|
||||||
// main.tsx
|
// main.tsx
|
||||||
<Router base="/page-agent">
|
<Router base="/page-agent">
|
||||||
<PagesRouter />
|
<PagesRouter />
|
||||||
</Router>
|
</Router>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -109,6 +109,7 @@ Instead of `404.html` redirects, the build copies `index.html` into every route
|
|||||||
2. Add route in `src/pages/docs/index.tsx`
|
2. Add route in `src/pages/docs/index.tsx`
|
||||||
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
||||||
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
||||||
|
5. Keep `<slug>` consistent across the folder name, import path, route path, sidebar link, and any related cross-links. If the public route changes, rename the folder too.
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function Header() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
|
className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
|
||||||
role="banner"
|
role="banner"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||||
|
|||||||
@@ -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.7.0/dist/iife/page-agent.demo.js'
|
'https://cdn.jsdelivr.net/npm/page-agent@1.7.1/dist/iife/page-agent.demo.js'
|
||||||
export const CDN_DEMO_CN_URL =
|
export const CDN_DEMO_CN_URL =
|
||||||
'https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js'
|
'https://registry.npmmirror.com/page-agent/1.7.1/files/dist/iife/page-agent.demo.js'
|
||||||
|
|
||||||
// Demo LLM for website testing (homepage quick trial uses flash)
|
// Demo LLM for website testing (homepage quick trial uses flash)
|
||||||
export const DEMO_MODEL = 'qwen3.5-flash'
|
export const DEMO_MODEL = 'qwen3.5-flash'
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ const agent = new PageAgentCore({
|
|||||||
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
|
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
|
||||||
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
|
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'keepSemanticTags',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
description: isZh
|
||||||
|
? '在简化输出中保留语义标签(如 nav, main, header, footer, aside 等),即使它们不可交互。帮助 LLM 理解页面结构。'
|
||||||
|
: 'Preserve semantic landmark tags (e.g. nav, main, header, footer, aside) in dehydrated output even if not interactive. Helps LLM understand page structure.',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ interface ExecuteConfig {
|
|||||||
model: string // Model name
|
model: string // Model name
|
||||||
apiKey?: string // LLM AK
|
apiKey?: string // LLM AK
|
||||||
|
|
||||||
|
systemInstruction?: string // Global system-level instructions
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window
|
experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ const BASELINE = new Set([
|
|||||||
// Models grouped by brand, newest first
|
// Models grouped by brand, newest first
|
||||||
const MODEL_GROUPS: Record<string, string[]> = {
|
const MODEL_GROUPS: Record<string, string[]> = {
|
||||||
Qwen: [
|
Qwen: [
|
||||||
|
'qwen3.6-plus',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
'qwen3-coder-next',
|
'qwen3-coder-next',
|
||||||
'qwen-3-max',
|
'qwen-3-max',
|
||||||
'qwen-3-plus',
|
'qwen-3-plus',
|
||||||
'qwen3:14b (ollama)',
|
|
||||||
],
|
],
|
||||||
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
||||||
DeepSeek: ['deepseek-3.2'],
|
DeepSeek: ['deepseek-3.2'],
|
||||||
@@ -64,7 +64,6 @@ export default function Models() {
|
|||||||
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
|
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Models Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
||||||
{isZh ? '已测试模型' : 'Tested Models'}
|
{isZh ? '已测试模型' : 'Tested Models'}
|
||||||
@@ -87,9 +86,10 @@ export default function Models() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Tips Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2>
|
<Heading id="tips" level={3}>
|
||||||
|
Tips
|
||||||
|
</Heading>
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
<li>
|
<li>
|
||||||
@@ -111,7 +111,6 @@ export default function Models() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Configuration Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
|
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
@@ -121,25 +120,50 @@ const pageAgent = new PageAgent({
|
|||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'qwen3.5-plus'
|
model: 'qwen3.5-plus'
|
||||||
});
|
});
|
||||||
|
|
||||||
// MiniMax
|
|
||||||
const pageAgent = new PageAgent({
|
|
||||||
baseURL: 'https://api.minimax.io/v1',
|
|
||||||
apiKey: 'your-minimax-api-key',
|
|
||||||
model: 'MiniMax-M2.7'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Self-hosted models (e.g., Ollama) — no apiKey needed
|
|
||||||
const pageAgent = new PageAgent({
|
|
||||||
baseURL: 'http://localhost:11434/v1',
|
|
||||||
model: 'qwen3:14b'
|
|
||||||
});
|
|
||||||
|
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Free Testing API Section */}
|
<section className="mb-10">
|
||||||
|
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
||||||
|
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
|
||||||
|
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
如果你计划将它集成到你的 Web 应用中,建议搭建一个后端代理来转发 LLM 请求,并使用{' '}
|
||||||
|
<code>customFetch</code> 携带 Cookie 或其他鉴权信息:
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
If you plan to integrate it into your web app, it's better to have a backend proxy for
|
||||||
|
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
|
||||||
|
other methods:
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`const agent = new PageAgent({
|
||||||
|
baseURL: '/api/llm-proxy',
|
||||||
|
model: 'gpt-5.1',
|
||||||
|
customFetch: (url, init) =>
|
||||||
|
fetch(url, { ...init, credentials: 'include' }),
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
|
||||||
|
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
||||||
|
{isZh
|
||||||
|
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'
|
||||||
|
: '⚠️ NEVER commit real LLM API keys to your frontend code'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
@@ -181,115 +205,143 @@ const pageAgent = new PageAgent({
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`# qwen3.5-plus (default for demos) or qwen3.5-flash (lighter)
|
code={`# qwen3.5-plus / qwen3.5-flash
|
||||||
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
|
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
|
||||||
LLM_MODEL_NAME="qwen3.5-plus"
|
LLM_MODEL_NAME="qwen3.5-plus"`}
|
||||||
LLM_API_KEY="NA"`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Ollama Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="ollama">Ollama</Heading>
|
<Heading id="local-runtimes">{isZh ? '本地运行时' : 'Local Runtimes'}</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
{isZh
|
{isZh
|
||||||
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
? '通过 Ollama、LM Studio 等本地 OpenAI-compatible 运行时接入 PageAgent,实现离线或局域网部署。'
|
||||||
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
|
: 'Use local OpenAI-compatible runtimes such as Ollama and LM Studio with PageAgent for offline or LAN deployments.'}
|
||||||
</p>
|
</p>
|
||||||
<CodeEditor
|
|
||||||
code={`LLM_BASE_URL="http://localhost:11434/v1"
|
|
||||||
LLM_API_KEY="NA"
|
|
||||||
LLM_MODEL_NAME="qwen3:14b"`}
|
|
||||||
/>
|
|
||||||
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
|
||||||
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
|
||||||
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
|
||||||
</h3>
|
|
||||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '确保 OLLAMA_ORIGINS 设置为 * 以避免 403 错误'
|
|
||||||
: 'Add * to OLLAMA_ORIGINS to avoid 403 errors'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '小于 10B 参数的模型通常效果不佳'
|
|
||||||
: 'Models smaller than 10B are unlikely to be strong enough'}
|
|
||||||
</li>
|
|
||||||
<li>{isZh ? '需要支持 tool_call 的模型' : 'Requires tool_call capable models'}</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作'
|
|
||||||
: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="space-y-10">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
<section>
|
||||||
{isZh ? '建议启动参数' : 'Recommended Startup'}
|
<Heading id="requirements" level={3}>
|
||||||
</h3>
|
Requirements
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
</Heading>
|
||||||
{isZh
|
<div className="mt-4 p-5 bg-amber-50 dark:bg-amber-950/20 rounded-xl border border-amber-200 dark:border-amber-800">
|
||||||
? '启动 Ollama 时建议配置以下环境变量:扩大上下文窗口、允许跨域访问、监听所有网络接口。'
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
: 'Start Ollama with these environment variables: larger context window, allow cross-origin access, and listen on all interfaces.'}
|
<li>
|
||||||
</p>
|
{isZh
|
||||||
|
? '务必打开 CORS,否则浏览器无法直接请求本地 LLM 服务。'
|
||||||
|
: 'Enable CORS, otherwise the browser cannot call your local LLM endpoint directly.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '将 context length 或 content length 至少设置为 8000。普通页面常常需要 15k token 左右,默认 4k 很容易被截断。'
|
||||||
|
: 'Set context length or content length to at least 8000. A typical page often needs around 15k tokens, so the default 4k usually truncates prompts.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh ? '需要支持 tool_call 的模型。' : 'Use a model with tool_call support.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '小于 10B 参数的模型通常效果不佳。'
|
||||||
|
: 'Models smaller than 10B are usually not strong enough.'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<section>
|
||||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">macOS / Linux</p>
|
<Heading id="local-configuration" level={3}>
|
||||||
|
{isZh ? '基础配置' : 'Basic Configuration'}
|
||||||
|
</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
|
code={`// Local OpenAI-compatible runtime - no apiKey needed
|
||||||
/>
|
const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'http://localhost:11434/v1',
|
||||||
|
model: 'qwen3:14b'
|
||||||
|
});
|
||||||
|
|
||||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
|
// Or connect to LM Studio
|
||||||
Windows (PowerShell)
|
const lmStudioAgent = new PageAgent({
|
||||||
|
baseURL: 'http://127.0.0.1:1234/v1',
|
||||||
|
model: 'qwen/qwen3.5-27b'
|
||||||
|
});
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Heading id="ollama" level={3}>
|
||||||
|
Ollama
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
||||||
|
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
|
||||||
</p>
|
</p>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
|
code={`LLM_BASE_URL="http://localhost:11434/v1"
|
||||||
|
LLM_MODEL_NAME="qwen3:14b"`}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
</div>
|
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
||||||
</section>
|
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{isZh
|
||||||
|
? '如果浏览器侧请求失败,优先检查 Ollama 是否已按上面的要求开启 CORS。'
|
||||||
|
: 'If browser-side requests fail, check whether Ollama has CORS enabled as required above.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Production Authentication */}
|
<div className="mt-4">
|
||||||
<section className="mb-10">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
{isZh ? '建议启动参数' : 'Recommended Startup'}
|
||||||
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
</h3>
|
||||||
</Heading>
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
{isZh
|
||||||
{isZh
|
? '启动 Ollama 时建议同时放大上下文窗口并开启跨域访问。'
|
||||||
? '如果你只是将它用作个人助手,可以直接连接你的 LLM 服务。'
|
: 'When starting Ollama, increase the context window and enable cross-origin access.'}
|
||||||
: 'If you only use it as a personal assistant, you can connect to your LLM service directly.'}
|
</p>
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
<div className="space-y-2">
|
||||||
{isZh ? (
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
<>
|
macOS / Linux
|
||||||
如果你计划将它集成到你的 Web 应用中,建议搭建一个后端代理来转发 LLM 请求,并使用{' '}
|
</p>
|
||||||
<code>customFetch</code> 携带 Cookie 或其他鉴权信息:
|
<CodeEditor
|
||||||
</>
|
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
|
||||||
) : (
|
/>
|
||||||
<>
|
|
||||||
If you plan to integrate it into your web app, it's better to have a backend proxy for
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
|
||||||
the LLM and use <code>customFetch</code> to authenticate the request with cookies or
|
Windows (PowerShell)
|
||||||
other methods:
|
</p>
|
||||||
</>
|
<CodeEditor
|
||||||
)}
|
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
|
||||||
</p>
|
/>
|
||||||
<CodeEditor
|
</div>
|
||||||
code={`const agent = new PageAgent({
|
</div>
|
||||||
baseURL: '/api/llm-proxy',
|
</section>
|
||||||
model: 'gpt-5.1',
|
|
||||||
customFetch: (url, init) =>
|
<section>
|
||||||
fetch(url, { ...init, credentials: 'include' }),
|
<Heading id="lm-studio" level={3}>
|
||||||
});`}
|
LM Studio
|
||||||
/>
|
</Heading>
|
||||||
<div className="mt-4 bg-yellow-50 dark:bg-yellow-950/20 border-l-4 border-yellow-500 p-4 rounded-r-lg">
|
<CodeEditor
|
||||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200">
|
code={`LLM_BASE_URL="http://127.0.0.1:1234/v1"
|
||||||
{isZh
|
LLM_MODEL_NAME="qwen/qwen3.5-27b"`}
|
||||||
? '⚠️ 永远不要把真实的 LLM API Key 提交到前端代码中'
|
/>
|
||||||
: '⚠️ NEVER commit real LLM API keys to your frontend code'}
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
</p>
|
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
||||||
|
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
||||||
|
</h3>
|
||||||
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? 'Agent 必须启用 disableNamedToolChoice,否则 tool_choice 参数会报错。'
|
||||||
|
: 'Enable disableNamedToolChoice in the agent config, otherwise the tool_choice parameter may fail.'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -274,10 +274,12 @@ function ApiErrorsContent(isZh: boolean) {
|
|||||||
code={`const agent = new PageAgent({
|
code={`const agent = new PageAgent({
|
||||||
// ...
|
// ...
|
||||||
customFetch: async (url, init) => {
|
customFetch: async (url, init) => {
|
||||||
const body = JSON.parse(init.body)
|
|
||||||
// Adapt parameters for your provider
|
// Adapt parameters for your provider
|
||||||
delete body.stream_options
|
const body = JSON.parse(init.body)
|
||||||
return fetch(url, { ...init, body: JSON.stringify(body) })
|
delete body.tool_choice
|
||||||
|
const bodyStr = JSON.stringify(body)
|
||||||
|
|
||||||
|
return fetch(url, { ...init, body: bodyStr })
|
||||||
},
|
},
|
||||||
})`}
|
})`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ const LLM_CLOUD: {
|
|||||||
opacity: number
|
opacity: number
|
||||||
}[] = [
|
}[] = [
|
||||||
{ name: 'OpenAI', color: '#10b981', x: 18, y: 22, size: 1.5, opacity: 1 },
|
{ name: 'OpenAI', color: '#10b981', x: 18, y: 22, size: 1.5, opacity: 1 },
|
||||||
{ name: 'Claude', color: '#f97316', x: 62, y: 15, size: 1.35, opacity: 0.95 },
|
{ name: 'Claude', color: '#f97316', x: 58, y: 15, size: 1.35, opacity: 0.95 },
|
||||||
{ name: 'Qwen', color: '#8b5cf6', x: 38, y: 50, size: 1.8, opacity: 0.9 },
|
{ name: 'Qwen', color: '#8b5cf6', x: 35, y: 50, size: 1.8, opacity: 0.9 },
|
||||||
{ name: 'Gemini', color: '#3b82f6', x: 68, y: 48, size: 1.2, opacity: 0.85 },
|
{ name: 'Gemini', color: '#3b82f6', x: 72, y: 48, size: 1.2, opacity: 0.85 },
|
||||||
{ name: 'DeepSeek', color: '#06b6d4', x: 10, y: 65, size: 1.1, opacity: 0.8 },
|
{ name: 'DeepSeek', color: '#06b6d4', x: 13, y: 65, size: 1.1, opacity: 0.8 },
|
||||||
{ name: 'Grok', color: '#f43f5e', x: 52, y: 78, size: 1.0, opacity: 0.75 },
|
{ name: 'Grok', color: '#f43f5e', x: 52, y: 78, size: 1.0, opacity: 0.75 },
|
||||||
{ name: 'Ollama', color: '#9ca3af', x: 82, y: 25, size: 1.1, opacity: 0.8 },
|
{ name: 'Ollama', color: '#9ca3af', x: 84, y: 25, size: 1.1, opacity: 0.8 },
|
||||||
|
{ name: 'LM Studio', color: '#4338ca', x: 49, y: 36, size: 0.8, opacity: 0.9 },
|
||||||
{ name: 'Kimi', color: '#14b8a6', x: 30, y: 82, size: 0.85, opacity: 0.6 },
|
{ name: 'Kimi', color: '#14b8a6', x: 30, y: 82, size: 0.85, opacity: 0.6 },
|
||||||
{ name: 'GLM', color: '#f59e0b', x: 70, y: 72, size: 0.85, opacity: 0.55 },
|
{ name: 'GLM', color: '#f59e0b', x: 70, y: 72, size: 0.85, opacity: 0.55 },
|
||||||
{ name: 'LLaMA', color: '#60a5fa', x: 88, y: 70, size: 0.8, opacity: 0.45 },
|
{ name: 'LLaMA', color: '#60a5fa', x: 88, y: 70, size: 0.8, opacity: 0.45 },
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const SPA_ROUTES = [
|
|||||||
'docs/features/data-masking',
|
'docs/features/data-masking',
|
||||||
'docs/features/custom-instructions',
|
'docs/features/custom-instructions',
|
||||||
'docs/features/models',
|
'docs/features/models',
|
||||||
|
'docs/features/local-llms',
|
||||||
'docs/features/chrome-extension',
|
'docs/features/chrome-extension',
|
||||||
'docs/features/mcp-server',
|
'docs/features/mcp-server',
|
||||||
'docs/features/third-party-agent',
|
'docs/features/third-party-agent',
|
||||||
|
|||||||
Reference in New Issue
Block a user