@opentiny/webmcp-cli 0.0.1-alpha.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/bin.cjs +515 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +491 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.cjs +420 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +379 -0
- package/dist/index.js.map +1 -0
- package/dist/inject-bundle.js +25318 -0
- package/dist/webmcp-tools/excalidraw.com.js +219 -0
- package/dist/webmcp-tools/www.baidu.com.js +76 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 - present OpenTiny Authors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @opentiny/webmcp-cli
|
|
2
|
+
|
|
3
|
+
`@opentiny/webmcp-cli` 是一个用于控制 Chrome 浏览器并暴露 WebMCP 接口的 CLI 工具。它基于 `puppeteer-core`,通过 CDP(Chrome DevTools Protocol)连接或启动本地浏览器,支持自动为页面注入 WebMCP 运行环境及页面操作工具 (`page-agent-tool`),从而让 AI Agent 可以轻松感知和操控网页。
|
|
4
|
+
|
|
5
|
+
## 安装与开发
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 全局安装(发布后)
|
|
9
|
+
npm install -g @opentiny/webmcp-cli
|
|
10
|
+
# 或
|
|
11
|
+
pnpm add -g @opentiny/webmcp-cli
|
|
12
|
+
|
|
13
|
+
# 本地联调(在 packages/webmcp-cli 目录)
|
|
14
|
+
pnpm build
|
|
15
|
+
npm install -g .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> **注意:** 在本地联调时,建议使用 `npm install -g .`,这会确保在你的 PATH 中生成有效的可执行文件(`webmcp-cli`)。
|
|
19
|
+
|
|
20
|
+
## 核心架构特性
|
|
21
|
+
|
|
22
|
+
- **后台浏览器驻留**:如果当前没有开启带有调试端口 (`9222`) 的 Chrome,CLI 会自动在后台拉起一个基于你本地 Profile 的独立 Chrome 实例。
|
|
23
|
+
- **自动环境注入**:当获取页面状态时,CLI 会自动探测并向页面注入 `webmcp-polyfill` 以及内置的 `page-agent-tool` 工具。
|
|
24
|
+
- **统一工具协议**:采用标准 MCP (Model Context Protocol) 规范。所有的页面操作(点击、输入等)不再是生硬的命令,而是直接调用页面上注册好的 `page-agent-tool`。
|
|
25
|
+
|
|
26
|
+
## CLI 命令使用
|
|
27
|
+
|
|
28
|
+
全局命令为 **`webmcp-cli`**,支持全局参数:
|
|
29
|
+
- `-w, --workspace <path>`: 指定自定义的浏览器工作空间(用户配置目录)路径。如果不传默认使用 `~/.webmcp_chrome_profile`。
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### 1. `state` 命令
|
|
34
|
+
|
|
35
|
+
获取浏览器当前活跃页签(或指定页签)的详细状态,包括当前页面的标题、URL、页面中包含的可操作 DOM 树,以及当前页面所有可调用的 MCP 工具列表(包含自动注入的 `page-agent-tool`)。
|
|
36
|
+
|
|
37
|
+
**用法:**
|
|
38
|
+
```bash
|
|
39
|
+
webmcp-cli state
|
|
40
|
+
webmcp-cli state -t <tabid>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**返回格式(JSON):**
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"content": "浏览器状态:包含 [index]<type>text</type> 格式的页面树...",
|
|
47
|
+
"url": "https://example.com",
|
|
48
|
+
"title": "Example Domain",
|
|
49
|
+
"webmcpTools": [
|
|
50
|
+
{
|
|
51
|
+
"name": "page-agent-tool",
|
|
52
|
+
"description": "...",
|
|
53
|
+
"inputSchema": "..."
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"tabs": [
|
|
57
|
+
{
|
|
58
|
+
"tabid": 1234,
|
|
59
|
+
"title": "Example Domain",
|
|
60
|
+
"url": "https://example.com"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### 2. `run` 命令
|
|
69
|
+
|
|
70
|
+
向指定页签调用并执行任意的 WebMCP 工具。工具名与参数须与 `state` 命令中获取到的 `webmcpTools` 清单匹配。
|
|
71
|
+
|
|
72
|
+
**用法:**
|
|
73
|
+
```bash
|
|
74
|
+
webmcp-cli run <toolName> <argsJson> [-t tabid]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**示例一:执行页面自带工具**
|
|
78
|
+
假设网页使用 `navigator.modelContext.registerTool` 注册了一个名为 `change-color` 的工具:
|
|
79
|
+
```bash
|
|
80
|
+
webmcp-cli run change-color '{"color": "#ff0000"}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**示例二:使用内置的 `page-agent-tool` 操控浏览器**
|
|
84
|
+
CLI 自动注入的 `page-agent-tool` 支持丰富的动作(action):`browserState`, `click`, `fill`, `select`, `scroll`, `executeJavascript`。
|
|
85
|
+
|
|
86
|
+
获取状态(执行前必须调用一次):
|
|
87
|
+
```bash
|
|
88
|
+
webmcp-cli run page-agent-tool '{"action": "browserState"}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
点击索引为 35 的元素:
|
|
92
|
+
```bash
|
|
93
|
+
webmcp-cli run page-agent-tool '{"action": "click", "index": 35}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
向索引为 40 的输入框填入文本:
|
|
97
|
+
```bash
|
|
98
|
+
webmcp-cli run page-agent-tool '{"action": "fill", "index": 40, "text": "OpenTiny"}'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 3. `open` 命令
|
|
104
|
+
|
|
105
|
+
在浏览器中打开指定的网页,并且可以选择是在当前页签导航,还是开启全新页签。
|
|
106
|
+
|
|
107
|
+
**用法:**
|
|
108
|
+
```bash
|
|
109
|
+
webmcp-cli open <url>
|
|
110
|
+
webmcp-cli open <url> -t <tabid>
|
|
111
|
+
webmcp-cli open <url> -n # 在新页签中打开
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**示例:**
|
|
115
|
+
```bash
|
|
116
|
+
webmcp-cli open "https://github.com/opentiny/tiny-vue" -n
|
|
117
|
+
```
|
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/bin.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_picocolors3 = __toESM(require("picocolors"), 1);
|
|
29
|
+
|
|
30
|
+
// src/browser.ts
|
|
31
|
+
var import_puppeteer_core = __toESM(require("puppeteer-core"), 1);
|
|
32
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
33
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
34
|
+
var import_os = __toESM(require("os"), 1);
|
|
35
|
+
var import_path = __toESM(require("path"), 1);
|
|
36
|
+
var import_child_process = require("child_process");
|
|
37
|
+
var import_url = require("url");
|
|
38
|
+
var import_meta = {};
|
|
39
|
+
var __filename = (0, import_url.fileURLToPath)(import_meta.url);
|
|
40
|
+
var __dirname = import_path.default.dirname(__filename);
|
|
41
|
+
var CDP_PORT = 9222;
|
|
42
|
+
var CDP_URL = `http://localhost:${CDP_PORT}`;
|
|
43
|
+
function getDefaultChromePath() {
|
|
44
|
+
const platform = import_os.default.platform();
|
|
45
|
+
if (platform === "darwin") {
|
|
46
|
+
return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
47
|
+
} else if (platform === "win32") {
|
|
48
|
+
const paths = [
|
|
49
|
+
process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe",
|
|
50
|
+
process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe",
|
|
51
|
+
process.env["PROGRAMFILES(X86)"] + "\\Google\\Chrome\\Application\\chrome.exe"
|
|
52
|
+
];
|
|
53
|
+
return paths.find((p) => import_fs.default.existsSync(p)) || null;
|
|
54
|
+
} else {
|
|
55
|
+
const paths = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
|
|
56
|
+
return paths.find((p) => import_fs.default.existsSync(p)) || null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function startChromeInBackground() {
|
|
60
|
+
const chromePath = getDefaultChromePath();
|
|
61
|
+
if (!chromePath || !import_fs.default.existsSync(chromePath)) {
|
|
62
|
+
throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
|
|
63
|
+
}
|
|
64
|
+
console.log(import_picocolors.default.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 Chrome \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
|
|
65
|
+
const userDataDir = process.env.WEBMCP_WORKSPACE || import_path.default.join(import_os.default.homedir(), ".webmcp_chrome_profile");
|
|
66
|
+
const child = (0, import_child_process.spawn)(
|
|
67
|
+
chromePath,
|
|
68
|
+
[
|
|
69
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
70
|
+
`--user-data-dir=${userDataDir}`,
|
|
71
|
+
"--no-first-run",
|
|
72
|
+
"--no-default-browser-check"
|
|
73
|
+
],
|
|
74
|
+
{
|
|
75
|
+
detached: true,
|
|
76
|
+
stdio: "ignore"
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
child.unref();
|
|
80
|
+
for (let i = 0; i < 20; i++) {
|
|
81
|
+
try {
|
|
82
|
+
const urls = [`http://localhost:${CDP_PORT}/json/version`, `http://127.0.0.1:${CDP_PORT}/json/version`];
|
|
83
|
+
for (const url of urls) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url);
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
console.log(import_picocolors.default.green("Chrome \u542F\u52A8\u5E76\u5C31\u7EEA\u3002"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
}
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Chrome \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002");
|
|
98
|
+
}
|
|
99
|
+
async function connectBrowser() {
|
|
100
|
+
const targetFilter = (target) => {
|
|
101
|
+
try {
|
|
102
|
+
const info = typeof target._getTargetInfo === "function" ? target._getTargetInfo() : target;
|
|
103
|
+
const type = info.type || "";
|
|
104
|
+
const url = info.url || "";
|
|
105
|
+
if (type === "service_worker" || type === "shared_worker" || type === "iframe" || type === "other" || type === "webview" || type === "background_page") {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (url.startsWith("devtools://") || url.startsWith("chrome-extension://")) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
|
|
118
|
+
const browser = await import_puppeteer_core.default.connect({
|
|
119
|
+
browserURL: `http://127.0.0.1:${CDP_PORT}`,
|
|
120
|
+
defaultViewport: null,
|
|
121
|
+
targetFilter
|
|
122
|
+
});
|
|
123
|
+
console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
|
|
124
|
+
return browser;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
|
|
128
|
+
const browser = await import_puppeteer_core.default.connect({
|
|
129
|
+
browserURL: `http://localhost:${CDP_PORT}`,
|
|
130
|
+
defaultViewport: null,
|
|
131
|
+
targetFilter
|
|
132
|
+
});
|
|
133
|
+
console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
|
|
134
|
+
return browser;
|
|
135
|
+
} catch (error2) {
|
|
136
|
+
console.log(import_picocolors.default.yellow(`connectBrowser: \u8FDE\u63A5\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5\u5524\u8D77\u6D4F\u89C8\u5668\u3002\u9519\u8BEF\u539F\u56E0: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
137
|
+
try {
|
|
138
|
+
await startChromeInBackground();
|
|
139
|
+
try {
|
|
140
|
+
console.log(import_picocolors.default.yellow("connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
|
|
141
|
+
const browser = await import_puppeteer_core.default.connect({
|
|
142
|
+
browserURL: `http://127.0.0.1:${CDP_PORT}`,
|
|
143
|
+
defaultViewport: null,
|
|
144
|
+
targetFilter
|
|
145
|
+
});
|
|
146
|
+
console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
|
|
147
|
+
return browser;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
|
|
150
|
+
const browser = await import_puppeteer_core.default.connect({
|
|
151
|
+
browserURL: `http://localhost:${CDP_PORT}`,
|
|
152
|
+
defaultViewport: null,
|
|
153
|
+
targetFilter
|
|
154
|
+
});
|
|
155
|
+
console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
|
|
156
|
+
return browser;
|
|
157
|
+
}
|
|
158
|
+
} catch (launchError) {
|
|
159
|
+
const msg = launchError instanceof Error ? launchError.message : String(launchError);
|
|
160
|
+
console.error(import_picocolors.default.red(`\u65E0\u6CD5\u8FDE\u63A5\u6216\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
|
|
161
|
+
console.error(import_picocolors.default.yellow(`\u{1F4A1} \u63D0\u793A\uFF1A\u7531\u4E8E\u6211\u4EEC\u8981\u4F7F\u7528\u4F60\u65E5\u5E38\u7684\u9ED8\u8BA4\u6D4F\u89C8\u5668\uFF08\u5305\u542B\u4F60\u7684\u4E66\u7B7E and \u767B\u5F55\u6001\uFF09\uFF0C\u5982\u679C\u4F60\u7684 Chrome \u76EE\u524D\u6B63\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u5B83\u4F1A\u62D2\u7EDD\u4F7F\u7528\u5E26\u6709\u8C03\u8BD5\u7AEF\u53E3\u7684\u65B0\u53C2\u6570\u542F\u52A8\u3002`));
|
|
162
|
+
console.error(import_picocolors.default.yellow(`\u{1F449} \u89E3\u51B3\u529E\u6CD5\uFF1A\u8BF7\u5148\u5B8C\u5168\u9000\u51FA\u5F53\u524D\u7684 Chrome \u6D4F\u89C8\u5668\uFF08\u5728 Mac \u4E0A\u6309 Cmd+Q\uFF09\uFF0C\u7136\u540E\u518D\u91CD\u65B0\u8FD0\u884C\u547D\u4EE4\u3002`));
|
|
163
|
+
throw new Error("Browser connection failed.");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function getPageTargetId(page) {
|
|
169
|
+
const session = await page.target().createCDPSession();
|
|
170
|
+
try {
|
|
171
|
+
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
172
|
+
return targetInfo.targetId;
|
|
173
|
+
} finally {
|
|
174
|
+
await session.detach().catch(() => {
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function getTargetPage(browser, tabid) {
|
|
179
|
+
const targets = browser.targets();
|
|
180
|
+
const pageTargets = targets.filter((t) => {
|
|
181
|
+
try {
|
|
182
|
+
const type = (typeof t.type === "function" ? t.type() : t.type) || "";
|
|
183
|
+
const url = (typeof t.url === "function" ? t.url() : t.url) || "";
|
|
184
|
+
return type === "page" && !url.startsWith("devtools://");
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (pageTargets.length === 0) {
|
|
190
|
+
const newPage = await browser.newPage();
|
|
191
|
+
await injectWebMCPPolyfillAndTools(newPage);
|
|
192
|
+
return newPage;
|
|
193
|
+
}
|
|
194
|
+
let targetPage = null;
|
|
195
|
+
if (tabid !== void 0) {
|
|
196
|
+
for (const target of pageTargets) {
|
|
197
|
+
const tid = typeof target._getTargetInfo === "function" ? target._getTargetInfo().targetId : target._targetId || target.targetId || "";
|
|
198
|
+
if (tid === tabid || tid.includes(tabid)) {
|
|
199
|
+
targetPage = await target.page();
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!targetPage) {
|
|
204
|
+
throw new Error(`Tab with targetId "${tabid}" not found.`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
try {
|
|
208
|
+
const urls = [
|
|
209
|
+
`http://localhost:${CDP_PORT}/json/list`,
|
|
210
|
+
`http://127.0.0.1:${CDP_PORT}/json/list`
|
|
211
|
+
];
|
|
212
|
+
let activeTargetId = null;
|
|
213
|
+
for (const url of urls) {
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(url);
|
|
216
|
+
if (res.ok) {
|
|
217
|
+
const targetsData = await res.json();
|
|
218
|
+
const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
|
|
219
|
+
if (active) {
|
|
220
|
+
activeTargetId = active.id;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (activeTargetId) {
|
|
228
|
+
for (const target of pageTargets) {
|
|
229
|
+
const tid = typeof target._getTargetInfo === "function" ? target._getTargetInfo().targetId : target._targetId || target.targetId || "";
|
|
230
|
+
if (tid === activeTargetId) {
|
|
231
|
+
targetPage = await target.page();
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
if (!targetPage) {
|
|
239
|
+
const lastTarget = pageTargets[pageTargets.length - 1];
|
|
240
|
+
targetPage = await lastTarget.page();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!targetPage) {
|
|
244
|
+
throw new Error("\u65E0\u6CD5\u83B7\u53D6\u76EE\u6807\u9875\u9762");
|
|
245
|
+
}
|
|
246
|
+
await injectWebMCPPolyfillAndTools(targetPage);
|
|
247
|
+
return targetPage;
|
|
248
|
+
}
|
|
249
|
+
async function injectIntoPage(page) {
|
|
250
|
+
await injectWebMCPPolyfillAndTools(page, true);
|
|
251
|
+
}
|
|
252
|
+
async function injectWebMCPPolyfillAndTools(page, force = false) {
|
|
253
|
+
const polyfillReady = !force && await page.evaluate(() => {
|
|
254
|
+
return !!window.__webmcpcli_init;
|
|
255
|
+
}).catch(() => false);
|
|
256
|
+
if (!polyfillReady) {
|
|
257
|
+
console.log(import_picocolors.default.cyan("\u5F53\u524D\u9875\u9762\u5C1A\u672A\u6CE8\u5165 WebMCP \u73AF\u5883\uFF0C\u6B63\u5728\u6267\u884C\u81EA\u52A8\u6CE8\u5165..."));
|
|
258
|
+
const injectScriptPath = import_path.default.resolve(__dirname, "inject-bundle.js");
|
|
259
|
+
if (!import_fs.default.existsSync(injectScriptPath)) {
|
|
260
|
+
throw new Error(`Cannot find inject-bundle.js at ${injectScriptPath}. Please ensure you run 'pnpm build:inject' first.`);
|
|
261
|
+
}
|
|
262
|
+
const scriptContent = import_fs.default.readFileSync(injectScriptPath, "utf-8");
|
|
263
|
+
try {
|
|
264
|
+
await page.evaluate(scriptContent);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
267
|
+
throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
|
|
268
|
+
}
|
|
269
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
270
|
+
}
|
|
271
|
+
await injectDomainTools(page);
|
|
272
|
+
}
|
|
273
|
+
async function injectDomainTools(page) {
|
|
274
|
+
let hostname;
|
|
275
|
+
try {
|
|
276
|
+
const url = new URL(page.url());
|
|
277
|
+
hostname = url.hostname;
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const toolBundlePath = import_path.default.resolve(__dirname, "webmcp-tools", `${hostname}.js`);
|
|
282
|
+
if (!import_fs.default.existsSync(toolBundlePath)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
console.log(import_picocolors.default.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
|
|
286
|
+
const toolScript = import_fs.default.readFileSync(toolBundlePath, "utf-8");
|
|
287
|
+
try {
|
|
288
|
+
await page.evaluate(toolScript);
|
|
289
|
+
console.log(import_picocolors.default.green(`\u5DF2\u4E3A ${hostname} \u6CE8\u5165\u9884\u7F6E\u5DE5\u5177`));
|
|
290
|
+
} catch (err) {
|
|
291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
292
|
+
console.warn(import_picocolors.default.yellow(`\u57DF\u540D\u5DE5\u5177\u6CE8\u5165\u5931\u8D25 (${hostname}): ${msg}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/commands/state.ts
|
|
297
|
+
async function stateCommand({ tabid }) {
|
|
298
|
+
const browser = await connectBrowser();
|
|
299
|
+
try {
|
|
300
|
+
const page = await getTargetPage(browser, tabid);
|
|
301
|
+
const state = await page.evaluate(async () => {
|
|
302
|
+
const url = document.URL;
|
|
303
|
+
const title = document.title;
|
|
304
|
+
const mcp = navigator.modelContextTesting || navigator.modelContext;
|
|
305
|
+
let webmcpTools = [];
|
|
306
|
+
let contentData = `\u9875\u9762\u5DF2\u51C6\u5907\u597D: ${title}`;
|
|
307
|
+
if (mcp) {
|
|
308
|
+
if (typeof mcp.listTools === "function") {
|
|
309
|
+
const toolsResult = await mcp.listTools();
|
|
310
|
+
webmcpTools = toolsResult?.tools || toolsResult || [];
|
|
311
|
+
}
|
|
312
|
+
if (typeof mcp.executeTool === "function") {
|
|
313
|
+
try {
|
|
314
|
+
const argsString = JSON.stringify({ action: "browserState" });
|
|
315
|
+
let stateRes = await mcp.executeTool("page-agent-tool", argsString);
|
|
316
|
+
if (typeof stateRes === "string") {
|
|
317
|
+
try {
|
|
318
|
+
stateRes = JSON.parse(stateRes);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (stateRes && stateRes.content && stateRes.content.length > 0) {
|
|
323
|
+
const textContent = stateRes.content.map((c) => c.text).join("\\n");
|
|
324
|
+
const prefix = "\u6D4F\u89C8\u5668\u72B6\u6001: ";
|
|
325
|
+
if (textContent.startsWith(prefix)) {
|
|
326
|
+
try {
|
|
327
|
+
const jsonStr = textContent.substring(prefix.length);
|
|
328
|
+
const parsedState = JSON.parse(jsonStr);
|
|
329
|
+
contentData = parsedState.content;
|
|
330
|
+
} catch (e) {
|
|
331
|
+
contentData = textContent;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
contentData = textContent;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (e) {
|
|
338
|
+
console.error("Snapshot error:", e.message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
content: contentData,
|
|
344
|
+
url,
|
|
345
|
+
title,
|
|
346
|
+
webmcpTools
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
const pages = await browser.pages();
|
|
350
|
+
const tabs = await Promise.all(pages.map(async (p) => {
|
|
351
|
+
const pUrl = p.url();
|
|
352
|
+
if (pUrl.startsWith("devtools://")) return null;
|
|
353
|
+
const pTitle = await Promise.race([
|
|
354
|
+
p.title().catch(() => "Unknown"),
|
|
355
|
+
new Promise((resolve) => setTimeout(() => resolve("Unknown"), 500))
|
|
356
|
+
]);
|
|
357
|
+
return {
|
|
358
|
+
// 使用真实的 Chrome target ID,而非数组下标
|
|
359
|
+
tabid: await getPageTargetId(p).catch(() => pUrl),
|
|
360
|
+
title: pTitle,
|
|
361
|
+
url: pUrl
|
|
362
|
+
};
|
|
363
|
+
}));
|
|
364
|
+
return {
|
|
365
|
+
...state,
|
|
366
|
+
tabs: tabs.filter(Boolean)
|
|
367
|
+
};
|
|
368
|
+
} finally {
|
|
369
|
+
await browser.disconnect();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/commands/run.ts
|
|
374
|
+
async function runCommand({
|
|
375
|
+
toolName,
|
|
376
|
+
argsJson,
|
|
377
|
+
tabid
|
|
378
|
+
}) {
|
|
379
|
+
const browser = await connectBrowser();
|
|
380
|
+
try {
|
|
381
|
+
const page = await getTargetPage(browser, tabid);
|
|
382
|
+
try {
|
|
383
|
+
JSON.parse(argsJson);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON: ${e.message}`);
|
|
386
|
+
}
|
|
387
|
+
const result = await page.evaluate(async (name, inputString) => {
|
|
388
|
+
const mcp = navigator.modelContextTesting || navigator.modelContext;
|
|
389
|
+
if (!mcp || typeof mcp.executeTool !== "function") {
|
|
390
|
+
throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
|
|
391
|
+
}
|
|
392
|
+
let res = await mcp.executeTool(name, inputString);
|
|
393
|
+
if (typeof res === "string") {
|
|
394
|
+
try {
|
|
395
|
+
res = JSON.parse(res);
|
|
396
|
+
} catch (e) {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return res;
|
|
400
|
+
}, toolName, argsJson);
|
|
401
|
+
return result;
|
|
402
|
+
} finally {
|
|
403
|
+
await browser.disconnect();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/commands/open.ts
|
|
408
|
+
var import_picocolors2 = __toESM(require("picocolors"), 1);
|
|
409
|
+
async function openCommand(url, { tabid, newTab }) {
|
|
410
|
+
const browser = await connectBrowser();
|
|
411
|
+
try {
|
|
412
|
+
let page;
|
|
413
|
+
if (newTab) {
|
|
414
|
+
console.log(import_picocolors2.default.yellow("openCommand: \u6B63\u5728\u521B\u5EFA\u65B0\u9875\u9762..."));
|
|
415
|
+
page = await browser.newPage();
|
|
416
|
+
} else {
|
|
417
|
+
console.log(import_picocolors2.default.yellow("openCommand: \u83B7\u53D6\u6D4F\u89C8\u5668 targets..."));
|
|
418
|
+
const targets = browser.targets();
|
|
419
|
+
const pageTargets = targets.filter((t) => {
|
|
420
|
+
try {
|
|
421
|
+
const type = (typeof t.type === "function" ? t.type() : t.type) || "";
|
|
422
|
+
const urlStr = (typeof t.url === "function" ? t.url() : t.url) || "";
|
|
423
|
+
return type === "page" && !urlStr.startsWith("devtools://");
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
console.log(import_picocolors2.default.yellow(`openCommand: \u666E\u901A\u9875\u9762 targets \u6570\u91CF: ${pageTargets.length}`));
|
|
429
|
+
let selectedTarget;
|
|
430
|
+
if (tabid !== void 0) {
|
|
431
|
+
console.log(import_picocolors2.default.yellow(`openCommand: \u6B63\u5728\u5339\u914D\u6307\u5B9A\u7684 tabid: ${tabid}...`));
|
|
432
|
+
selectedTarget = pageTargets.find((t) => {
|
|
433
|
+
const tid = typeof t._getTargetInfo === "function" ? t._getTargetInfo().targetId : t._targetId || t.targetId || "";
|
|
434
|
+
return tid === tabid || tid.includes(tabid);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (selectedTarget) {
|
|
438
|
+
console.log(import_picocolors2.default.yellow("openCommand: \u6B63\u5728\u5C06\u5339\u914D\u7684 target \u8F6C\u6362\u4E3A page..."));
|
|
439
|
+
page = await selectedTarget.page();
|
|
440
|
+
} else if (pageTargets.length > 0) {
|
|
441
|
+
console.log(import_picocolors2.default.yellow("openCommand: \u6B63\u5728\u5C06\u6700\u540E\u4E00\u4E2A target \u8F6C\u6362\u4E3A page..."));
|
|
442
|
+
page = await pageTargets[pageTargets.length - 1].page();
|
|
443
|
+
}
|
|
444
|
+
if (!page) {
|
|
445
|
+
console.log(import_picocolors2.default.yellow("openCommand: \u6CA1\u6709\u627E\u5230\u53EF\u7528 page\uFF0C\u6B63\u5728\u521B\u5EFA\u65B0\u9875\u9762..."));
|
|
446
|
+
page = await browser.newPage();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
450
|
+
url = "https://" + url;
|
|
451
|
+
}
|
|
452
|
+
console.log(import_picocolors2.default.cyan(`\u6B63\u5728\u6253\u5F00: ${url}`));
|
|
453
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
454
|
+
await injectIntoPage(page);
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
url: page.url(),
|
|
458
|
+
title: await page.title()
|
|
459
|
+
};
|
|
460
|
+
} finally {
|
|
461
|
+
await browser.disconnect();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/bin.ts
|
|
466
|
+
var program = new import_commander.Command();
|
|
467
|
+
function parseTabId(id) {
|
|
468
|
+
if (!id) return void 0;
|
|
469
|
+
return id;
|
|
470
|
+
}
|
|
471
|
+
program.name("webmcp-cli").description("WebMCP CLI for interacting with browser via CDP").version("1.0.0").option("-w, --workspace <path>", "\u6307\u5B9A\u81EA\u5B9A\u4E49\u7684\u6D4F\u89C8\u5668\u5DE5\u4F5C\u7A7A\u95F4\uFF08\u7528\u6237\u914D\u7F6E\u76EE\u5F55\uFF09\u8DEF\u5F84").hook("preAction", (thisCommand) => {
|
|
472
|
+
const opts = thisCommand.opts();
|
|
473
|
+
if (opts.workspace) {
|
|
474
|
+
process.env.WEBMCP_WORKSPACE = opts.workspace;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D\u9875\u7B7E\u6216\u6307\u5B9A\u9875\u7B7E\u7684\u72B6\u6001\uFF08\u5185\u5BB9\u3001\u6240\u6709\u9875\u7B7E\u5217\u8868\u3001\u53EF\u7528 WebMCP \u5DE5\u5177\u5217\u8868\uFF09").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (options) => {
|
|
478
|
+
try {
|
|
479
|
+
const result = await stateCommand({ tabid: parseTabId(options.tabid) });
|
|
480
|
+
console.log(JSON.stringify(result, null, 2));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
483
|
+
console.error(import_picocolors3.default.red(`Error executing state command: ${msg}`));
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
program.command("run <toolName> <argsJson>").description("\u5411\u6307\u5B9A\u9875\u7B7E\u8C03\u7528\u6307\u5B9A\u7684 WebMCP \u5DE5\u5177\u6267\u884C\u64CD\u4F5C").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (toolName, argsJson, options) => {
|
|
488
|
+
try {
|
|
489
|
+
const result = await runCommand({
|
|
490
|
+
toolName,
|
|
491
|
+
argsJson,
|
|
492
|
+
tabid: parseTabId(options.tabid)
|
|
493
|
+
});
|
|
494
|
+
console.log(JSON.stringify(result, null, 2));
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
497
|
+
console.error(import_picocolors3.default.red(`Error executing run command: ${msg}`));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
program.command("open <url>").description("\u5728\u5F53\u524D\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u6307\u5B9A\u7F51\u9875").option("-t, --tabid <id>", "\u5728\u6307\u5B9A\u9875\u7B7E\u4E2D\u6253\u5F00").option("-n, --new-tab", "\u5728\u4E00\u4E2A\u5168\u65B0\u7684\u9875\u7B7E\u4E2D\u6253\u5F00").action(async (url, options) => {
|
|
502
|
+
try {
|
|
503
|
+
const result = await openCommand(url, {
|
|
504
|
+
tabid: parseTabId(options.tabid),
|
|
505
|
+
newTab: options.newTab
|
|
506
|
+
});
|
|
507
|
+
console.log(JSON.stringify(result, null, 2));
|
|
508
|
+
} catch (error) {
|
|
509
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
510
|
+
console.error(import_picocolors3.default.red(`Error executing open command: ${msg}`));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
program.parse(process.argv);
|
|
515
|
+
//# sourceMappingURL=bin.cjs.map
|