@hao-gd/page-agent-mcp 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/package.json +35 -0
- package/src/hub-bridge.js +139 -0
- package/src/index.js +101 -0
- package/src/launcher.html +245 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @hao-gd/page-agent-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- Node.js >= 20
|
|
8
|
+
- [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome
|
|
9
|
+
- An LLM API key (OpenAI-compatible)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### Claude Desktop
|
|
14
|
+
|
|
15
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"page-agent": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@hao-gd/page-agent-mcp"],
|
|
23
|
+
"env": {
|
|
24
|
+
"LLM_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
25
|
+
"LLM_API_KEY": "sk-xxx",
|
|
26
|
+
"LLM_MODEL_NAME": "qwen3.5-plus"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Cursor / Copilot
|
|
34
|
+
|
|
35
|
+
Same format — add the config to the MCP settings of your client.
|
|
36
|
+
|
|
37
|
+
## MCP Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Input | Description |
|
|
40
|
+
| -------------- | ------------------ | ----------------------------------------------------- |
|
|
41
|
+
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
|
42
|
+
| `get_status` | — | Returns `{ connected, busy }` |
|
|
43
|
+
| `stop_task` | — | Stop the currently running task. |
|
|
44
|
+
|
|
45
|
+
## Environment Variables
|
|
46
|
+
|
|
47
|
+
| Variable | Default | Description |
|
|
48
|
+
| ---------------- | ------- | --------------------- |
|
|
49
|
+
| `LLM_BASE_URL` | — | LLM API base URL |
|
|
50
|
+
| `LLM_API_KEY` | — | LLM API key |
|
|
51
|
+
| `LLM_MODEL_NAME` | — | Model name |
|
|
52
|
+
| `PORT` | `38401` | HTTP + WebSocket port |
|
|
53
|
+
|
|
54
|
+
## How It Works
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
┌──────────────┐ stdio ┌──────────────────┐ WebSocket ┌──────────────┐
|
|
58
|
+
│ Claude / │◄────────►│ @hao-gd/page-agent-mcp │◄────────────►│ Hub tab │
|
|
59
|
+
│ Copilot │ (MCP) │ (Node.js) │ (localhost) │ (extension) │
|
|
60
|
+
└──────────────┘ └──────────────────┘ └──────┬───────┘
|
|
61
|
+
│ │
|
|
62
|
+
│ HTTP │ useAgent
|
|
63
|
+
▼ ▼
|
|
64
|
+
┌──────────────────┐ ┌──────────────┐
|
|
65
|
+
│ Launcher page │ │ MultiPage │
|
|
66
|
+
│ (localhost:PORT) │ │ Agent │
|
|
67
|
+
└──────────────────┘ └──────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
1. Agent client starts the MCP server via stdio (`npx @hao-gd/page-agent-mcp`).
|
|
71
|
+
2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.
|
|
72
|
+
3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).
|
|
73
|
+
4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.
|
|
74
|
+
|
|
75
|
+
The hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
Pure JS ESM, no build step. Source files are the published artifacts.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/
|
|
83
|
+
├── index.js # CLI entry: MCP server (stdio) + opens launcher
|
|
84
|
+
├── hub-bridge.js # HTTP server + WebSocket bridge to hub tab
|
|
85
|
+
└── launcher.html # Bootstrap page: detects extension, triggers hub open
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Dev
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run dev:ext
|
|
92
|
+
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
|
93
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hao-gd/page-agent-mcp",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.10.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"page-agent-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/"
|
|
11
|
+
],
|
|
12
|
+
"description": "MCP server for controlling the browser via Page Agent extension",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"page-agent",
|
|
15
|
+
"mcp",
|
|
16
|
+
"browser-automation",
|
|
17
|
+
"chrome-extension"
|
|
18
|
+
],
|
|
19
|
+
"author": "hao-gd<guodonghao520@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://gitee.com/hao_guo_dong/page-agent.git",
|
|
24
|
+
"directory": "packages/mcp"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://gitee.com/hao_guo_dong/page-agent",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"ws": "^8.21.0",
|
|
33
|
+
"zod": "^4.4.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { WebSocketServer } from 'ws'
|
|
6
|
+
|
|
7
|
+
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
|
|
8
|
+
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
|
|
9
|
+
const LOOPBACK_HOST = 'localhost'
|
|
10
|
+
|
|
11
|
+
const launcherTemplate = readFileSync(
|
|
12
|
+
fileURLToPath(new URL('./launcher.html', import.meta.url)),
|
|
13
|
+
'utf-8'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* HTTP + WebSocket bridge to the hub.html extension tab.
|
|
18
|
+
* - HTTP serves the launcher page (triggers extension to open hub)
|
|
19
|
+
* - WS carries execute/stop commands and result/error responses
|
|
20
|
+
*/
|
|
21
|
+
export class HubBridge {
|
|
22
|
+
/** @type {number} */
|
|
23
|
+
port
|
|
24
|
+
|
|
25
|
+
/** @type {http.Server} */
|
|
26
|
+
#httpServer
|
|
27
|
+
|
|
28
|
+
/** @type {WebSocketServer} */
|
|
29
|
+
#wss
|
|
30
|
+
|
|
31
|
+
/** @type {import('ws').WebSocket | null} */
|
|
32
|
+
#hub = null
|
|
33
|
+
|
|
34
|
+
/** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */
|
|
35
|
+
#pendingTask = null
|
|
36
|
+
|
|
37
|
+
/** @param {number} port */
|
|
38
|
+
constructor(port) {
|
|
39
|
+
this.port = port
|
|
40
|
+
this.#httpServer = http.createServer((_req, res) => {
|
|
41
|
+
const html = launcherTemplate
|
|
42
|
+
.replaceAll('__EXT_ID__', EXT_ID)
|
|
43
|
+
.replaceAll('__STORE_URL__', STORE_URL)
|
|
44
|
+
.replaceAll('__WS_PORT__', String(port))
|
|
45
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
46
|
+
res.end(html)
|
|
47
|
+
})
|
|
48
|
+
this.#wss = new WebSocketServer({ server: this.#httpServer })
|
|
49
|
+
this.#wss.on('connection', (ws) => this.#onConnection(ws))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @returns {Promise<void>} */
|
|
53
|
+
async start() {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
this.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
|
|
56
|
+
if (err.code === 'EADDRINUSE') {
|
|
57
|
+
reject(
|
|
58
|
+
new Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)
|
|
59
|
+
)
|
|
60
|
+
} else {
|
|
61
|
+
reject(err)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
|
|
65
|
+
console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
|
|
66
|
+
resolve()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get connected() {
|
|
72
|
+
return this.#hub?.readyState === 1
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get busy() {
|
|
76
|
+
return this.#pendingTask !== null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} task
|
|
81
|
+
* @param {Record<string, unknown>} [config]
|
|
82
|
+
* @returns {Promise<{success: boolean, data: string}>}
|
|
83
|
+
*/
|
|
84
|
+
async executeTask(task, config) {
|
|
85
|
+
if (!this.connected) throw new Error('Hub is not connected. Is the extension running?')
|
|
86
|
+
if (this.#pendingTask) throw new Error('Agent is already running a task.')
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
this.#pendingTask = { resolve, reject }
|
|
90
|
+
this.#hub.send(JSON.stringify({ type: 'execute', task, config }))
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stopTask() {
|
|
95
|
+
if (this.connected) {
|
|
96
|
+
this.#hub.send(JSON.stringify({ type: 'stop' }))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// TODO: Add version checking
|
|
101
|
+
|
|
102
|
+
/** @param {import('ws').WebSocket} ws */
|
|
103
|
+
#onConnection(ws) {
|
|
104
|
+
if (this.#hub && this.#hub.readyState === 1) {
|
|
105
|
+
ws.close(4000, 'Another hub is already connected')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.#hub = ws
|
|
110
|
+
console.error('[page-agent-mcp] Hub connected')
|
|
111
|
+
|
|
112
|
+
ws.on('message', (/** @type {Buffer} */ rawData) => {
|
|
113
|
+
/** @type {{ type: string, success?: boolean, data?: string, message?: string }} */
|
|
114
|
+
let msg
|
|
115
|
+
try {
|
|
116
|
+
msg = JSON.parse(rawData.toString('utf-8'))
|
|
117
|
+
} catch {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (msg.type === 'result') {
|
|
122
|
+
this.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })
|
|
123
|
+
this.#pendingTask = null
|
|
124
|
+
} else if (msg.type === 'error') {
|
|
125
|
+
this.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))
|
|
126
|
+
this.#pendingTask = null
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
ws.on('close', () => {
|
|
131
|
+
console.error('[page-agent-mcp] Hub disconnected')
|
|
132
|
+
if (this.#hub === ws) this.#hub = null
|
|
133
|
+
if (this.#pendingTask) {
|
|
134
|
+
this.#pendingTask.reject(new Error('Hub disconnected while task was running'))
|
|
135
|
+
this.#pendingTask = null
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
4
|
+
import { spawn } from 'node:child_process'
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { platform } from 'node:os'
|
|
7
|
+
import * as z from 'zod/v4'
|
|
8
|
+
|
|
9
|
+
import { HubBridge } from './hub-bridge.js'
|
|
10
|
+
|
|
11
|
+
const env = process.env
|
|
12
|
+
const port = parseInt(env.PORT || '38401')
|
|
13
|
+
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
14
|
+
|
|
15
|
+
/** @type {Record<string, string>} */
|
|
16
|
+
const llmConfig = {}
|
|
17
|
+
if (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL
|
|
18
|
+
if (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME
|
|
19
|
+
if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
|
|
20
|
+
|
|
21
|
+
// --- Hub bridge (HTTP + WebSocket) ---
|
|
22
|
+
|
|
23
|
+
const hub = new HubBridge(port)
|
|
24
|
+
await hub.start()
|
|
25
|
+
|
|
26
|
+
// Open launcher in default browser (use spawn() with array args to avoid shell injection)
|
|
27
|
+
const url = `http://localhost:${port}`
|
|
28
|
+
const openCmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open'
|
|
29
|
+
const openArgs = platform() === 'win32' ? ['""', url] : [url]
|
|
30
|
+
const child = spawn(openCmd, openArgs, { stdio: 'ignore', detached: true })
|
|
31
|
+
child.on('error', (err) => console.error(`[page-agent-mcp] Could not open browser: ${err.message}`))
|
|
32
|
+
child.unref()
|
|
33
|
+
|
|
34
|
+
// --- MCP server (stdio) ---
|
|
35
|
+
|
|
36
|
+
const mcpServer = new McpServer({ name: '@hao-gd/page-agent', version })
|
|
37
|
+
|
|
38
|
+
mcpServer.registerTool(
|
|
39
|
+
'execute_task',
|
|
40
|
+
{
|
|
41
|
+
description: "Execute a task in user's browser.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
task: z
|
|
44
|
+
.string()
|
|
45
|
+
.describe(
|
|
46
|
+
'Task description. Give specific instructions for the task. Steps preferable. And the information you want to get after the task is done.'
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
async ({ task }) => {
|
|
51
|
+
try {
|
|
52
|
+
const config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined
|
|
53
|
+
const result = await hub.executeTask(task, config)
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: 'text',
|
|
58
|
+
text: result.success
|
|
59
|
+
? `Task completed.\n\n${result.data}`
|
|
60
|
+
: `Task failed.\n\n${result.data}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
67
|
+
isError: true,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
mcpServer.registerTool(
|
|
74
|
+
'get_status',
|
|
75
|
+
{
|
|
76
|
+
description: 'Check the current status of the Page Agent hub.',
|
|
77
|
+
},
|
|
78
|
+
async () => ({
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
mcpServer.registerTool(
|
|
89
|
+
'stop_task',
|
|
90
|
+
{
|
|
91
|
+
description: 'Stop the currently running browser automation task.',
|
|
92
|
+
},
|
|
93
|
+
async () => {
|
|
94
|
+
hub.stopTask()
|
|
95
|
+
return { content: [{ type: 'text', text: 'Stop signal sent.' }] }
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const transport = new StdioServerTransport()
|
|
100
|
+
await mcpServer.connect(transport)
|
|
101
|
+
console.error('[page-agent-mcp] MCP server ready (stdio)')
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link
|
|
7
|
+
rel="icon"
|
|
8
|
+
href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png"
|
|
9
|
+
/>
|
|
10
|
+
<title>Page Agent MCP Launcher</title>
|
|
11
|
+
<style>
|
|
12
|
+
* {
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
body {
|
|
18
|
+
font-family:
|
|
19
|
+
system-ui,
|
|
20
|
+
-apple-system,
|
|
21
|
+
sans-serif;
|
|
22
|
+
background: #09090b;
|
|
23
|
+
color: #e5e5e5;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
}
|
|
29
|
+
.card {
|
|
30
|
+
text-align: center;
|
|
31
|
+
max-width: 480px;
|
|
32
|
+
padding: 3rem 2rem;
|
|
33
|
+
}
|
|
34
|
+
.logo {
|
|
35
|
+
width: 72px;
|
|
36
|
+
height: 72px;
|
|
37
|
+
border-radius: 18px;
|
|
38
|
+
margin-bottom: 1rem;
|
|
39
|
+
}
|
|
40
|
+
.badge {
|
|
41
|
+
font-size: 0.6875rem;
|
|
42
|
+
font-weight: 500;
|
|
43
|
+
color: #52525b;
|
|
44
|
+
letter-spacing: 0.05em;
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
margin-bottom: 1.5rem;
|
|
47
|
+
}
|
|
48
|
+
h1 {
|
|
49
|
+
font-size: 1.35rem;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
margin-bottom: 0.5rem;
|
|
52
|
+
}
|
|
53
|
+
.sub {
|
|
54
|
+
font-size: 0.875rem;
|
|
55
|
+
color: #a1a1aa;
|
|
56
|
+
line-height: 1.7;
|
|
57
|
+
}
|
|
58
|
+
.spinner {
|
|
59
|
+
width: 32px;
|
|
60
|
+
height: 32px;
|
|
61
|
+
border: 3px solid #27272a;
|
|
62
|
+
border-top-color: #fff;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
animation: spin 0.8s linear infinite;
|
|
65
|
+
margin: 0 auto 1.5rem;
|
|
66
|
+
}
|
|
67
|
+
@keyframes spin {
|
|
68
|
+
to {
|
|
69
|
+
transform: rotate(360deg);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
a {
|
|
73
|
+
color: #60a5fa;
|
|
74
|
+
text-decoration: none;
|
|
75
|
+
}
|
|
76
|
+
a:hover {
|
|
77
|
+
text-decoration: underline;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.install {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
.install.show {
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.tips {
|
|
88
|
+
margin-top: 1.75rem;
|
|
89
|
+
text-align: left;
|
|
90
|
+
background: #18181b;
|
|
91
|
+
border: 1px solid #27272a;
|
|
92
|
+
border-radius: 12px;
|
|
93
|
+
padding: 1.25rem 1.5rem;
|
|
94
|
+
}
|
|
95
|
+
.tips li {
|
|
96
|
+
font-size: 0.8125rem;
|
|
97
|
+
color: #a1a1aa;
|
|
98
|
+
line-height: 1.7;
|
|
99
|
+
margin-left: 1rem;
|
|
100
|
+
}
|
|
101
|
+
.tips li + li {
|
|
102
|
+
margin-top: 0.35rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.store-btn {
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 0.625rem;
|
|
109
|
+
margin-top: 1.5rem;
|
|
110
|
+
padding: 0.625rem 1.5rem;
|
|
111
|
+
background: #2563eb;
|
|
112
|
+
color: #fff;
|
|
113
|
+
border-radius: 10px;
|
|
114
|
+
font-size: 0.875rem;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
transition: background 0.15s;
|
|
117
|
+
}
|
|
118
|
+
.store-btn:hover {
|
|
119
|
+
background: #1d4ed8;
|
|
120
|
+
text-decoration: none;
|
|
121
|
+
}
|
|
122
|
+
.store-btn img {
|
|
123
|
+
width: 20px;
|
|
124
|
+
height: 20px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.links {
|
|
128
|
+
margin-top: 1.75rem;
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
gap: 1.5rem;
|
|
132
|
+
font-size: 0.8125rem;
|
|
133
|
+
}
|
|
134
|
+
.links a {
|
|
135
|
+
color: #71717a;
|
|
136
|
+
transition: color 0.15s;
|
|
137
|
+
}
|
|
138
|
+
.links a:hover {
|
|
139
|
+
color: #a1a1aa;
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<div class="card">
|
|
146
|
+
<img
|
|
147
|
+
class="logo"
|
|
148
|
+
src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
|
|
149
|
+
alt="Page Agent"
|
|
150
|
+
/>
|
|
151
|
+
<div class="badge">Page Agent MCP Launcher</div>
|
|
152
|
+
|
|
153
|
+
<div id="connecting">
|
|
154
|
+
<div class="spinner"></div>
|
|
155
|
+
<h1 data-i18n="connecting_title">Connecting to Page Agent</h1>
|
|
156
|
+
<p class="sub" data-i18n="connecting_sub">Opening the hub in your browser…</p>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div id="install" class="install">
|
|
160
|
+
<h1 data-i18n="install_title">Extension Required</h1>
|
|
161
|
+
<p class="sub" data-i18n="install_sub">
|
|
162
|
+
Page Agent requires the latest browser extension to work.
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
<a class="store-btn" href="__STORE_URL__" target="_blank">
|
|
166
|
+
<img
|
|
167
|
+
src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
|
|
168
|
+
alt=""
|
|
169
|
+
/>
|
|
170
|
+
<span data-i18n="install_btn">Install from Chrome Web Store</span>
|
|
171
|
+
</a>
|
|
172
|
+
|
|
173
|
+
<ul class="tips">
|
|
174
|
+
<li data-i18n="tip_outdated">
|
|
175
|
+
If the extension is outdated, please update it to the latest version.
|
|
176
|
+
</li>
|
|
177
|
+
<li data-i18n="tip_other_browser">
|
|
178
|
+
If the extension is not installed in this browser, open this page from the browser that
|
|
179
|
+
has it installed.
|
|
180
|
+
</li>
|
|
181
|
+
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
|
|
182
|
+
</ul>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="links">
|
|
186
|
+
<a
|
|
187
|
+
href="https://alibaba.github.io/page-agent/docs/introduction/overview"
|
|
188
|
+
target="_blank"
|
|
189
|
+
data-i18n="link_docs"
|
|
190
|
+
>Docs</a
|
|
191
|
+
>
|
|
192
|
+
<a
|
|
193
|
+
href="https://github.com/alibaba/page-agent/issues"
|
|
194
|
+
target="_blank"
|
|
195
|
+
data-i18n="link_issues"
|
|
196
|
+
>Report an Issue</a
|
|
197
|
+
>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<script>
|
|
202
|
+
{
|
|
203
|
+
const EXT_ID = '__EXT_ID__'
|
|
204
|
+
const wsPort = __WS_PORT__
|
|
205
|
+
|
|
206
|
+
const zh = {
|
|
207
|
+
connecting_title: '正在连接 Page Agent',
|
|
208
|
+
connecting_sub: '正在浏览器中打开 Hub…',
|
|
209
|
+
install_title: '需要安装浏览器插件',
|
|
210
|
+
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
|
|
211
|
+
install_btn: '从 Chrome 应用商店安装',
|
|
212
|
+
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
|
|
213
|
+
tip_other_browser: '如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
|
|
214
|
+
tip_refresh: '安装或更新后,请刷新此页面。',
|
|
215
|
+
link_docs: '文档',
|
|
216
|
+
link_issues: '问题反馈',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (/^zh\b/i.test(navigator.language)) {
|
|
220
|
+
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
221
|
+
const key = el.getAttribute('data-i18n')
|
|
222
|
+
if (zh[key]) el.textContent = zh[key]
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const showInstall = () => {
|
|
227
|
+
document.getElementById('connecting').style.display = 'none'
|
|
228
|
+
document.getElementById('install').classList.add('show')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
if (!globalThis.chrome?.runtime?.sendMessage) {
|
|
233
|
+
showInstall()
|
|
234
|
+
} else {
|
|
235
|
+
chrome.runtime.sendMessage(EXT_ID, { type: 'OPEN_HUB', wsPort }, (response) => {
|
|
236
|
+
if (chrome.runtime.lastError || !response?.ok) showInstall()
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
showInstall()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
</script>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|