@mearl/client 2.11.0 → 2.13.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/README.md +81 -8
- package/dist/browserRegistry.d.ts +10 -1
- package/dist/check.d.ts +14 -1
- package/dist/check.js +13 -1
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +171 -14
- package/dist/commands.d.ts +8 -4
- package/dist/commands.js +9 -3
- package/dist/generated-browser-action-protocol.d.ts +35 -6
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.js +173 -0
- package/dist/pluginRegistry.d.ts +16 -0
- package/dist/pluginRegistry.js +116 -0
- package/dist/socket.js +74 -10
- package/dist/unified.js +6 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -77,10 +77,36 @@ mearl browser_list
|
|
|
77
77
|
mearl browser_release
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### 外部浏览器 Provider
|
|
81
|
+
|
|
82
|
+
Provider 浏览器与扩展、CDP 和 AgentBay 浏览器一起出现在 `browser_list` 中,并使用同一个
|
|
83
|
+
全局 `browserId` 参与路由。Provider 的安装、更新和卸载由 `@mearl/setup` 管理:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @mearl/setup provider install @ali/mearl-provider-brow
|
|
87
|
+
|
|
88
|
+
# 让 Brow 创建一个配对实例;debugId 可省略并自动生成
|
|
89
|
+
mearl browser_launch --payload '{"provider":"brow","providerOptions":{"debugId":"my-device"}}'
|
|
90
|
+
|
|
91
|
+
# 扫码后确认同一个实例已经连接
|
|
92
|
+
mearl browser_list
|
|
93
|
+
|
|
94
|
+
# 后续操作使用 browser_list 返回的全局 Browser ID
|
|
95
|
+
mearl page_snapshot --browser '<browserId>'
|
|
96
|
+
|
|
97
|
+
npx @mearl/setup provider update brow
|
|
98
|
+
npx @mearl/setup provider uninstall brow
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`browser_launch` 创建 `pairing` 实例时会直接返回 `pairing.url` 和可选的配对码;Agent 应把 URL 直接渲染为
|
|
102
|
+
二维码交给用户,扫码后确认同一 Browser ID 变为 `connected`,再开始页面操作。在设备连入前
|
|
103
|
+
该实例不会成为默认操作目标;可用 `browser_close` 删除对应 Provider 实例。每个 Provider
|
|
104
|
+
只接收它为该浏览器显式声明支持的 actions,截图能力同时标明为原生截图、重建画面或不支持。
|
|
105
|
+
|
|
80
106
|
输入不存在或拼写有误的 action 时,CLI 会基于可用命令和常见别名直接返回最多三个候选及调用示例,
|
|
81
107
|
不连接浏览器,也不会自动执行推荐命令。例如 `mearl tab new` 会优先推荐 `mearl tab_open`。
|
|
82
108
|
|
|
83
|
-
|
|
109
|
+
**常用选项:**
|
|
84
110
|
|
|
85
111
|
| 选项 | 说明 |
|
|
86
112
|
| ------------------------ | ------------------------ |
|
|
@@ -90,12 +116,55 @@ mearl browser_release
|
|
|
90
116
|
| `--compact` | stdout 紧凑输出 |
|
|
91
117
|
| `--output <path>` | 将结果写入文件(如截图) |
|
|
92
118
|
| `--browser <id\|名称>` | 指定目标浏览器 |
|
|
93
|
-
| `--connector <id\|名称>` | 限制到一台远端机器 |
|
|
94
|
-
| `--local` | 只使用当前机器 |
|
|
95
|
-
| `--cdp` | 当前机器强制使用 CDP |
|
|
96
|
-
| `--server <url>` | 覆盖 cloud-server 地址 |
|
|
97
119
|
|
|
98
|
-
|
|
120
|
+
Mearl 默认聚合当前机器和已配置 cloud-server 下的浏览器,并通过 `browser_list` 返回的全局
|
|
121
|
+
`browserId` 自动路由。以下覆盖参数仅在限定宿主或连接、传输排障时使用,因此不出现在默认 help:
|
|
122
|
+
|
|
123
|
+
| 选项 | 说明 |
|
|
124
|
+
| ------------------------ | ------------------------------------- |
|
|
125
|
+
| `--connector <id\|名称>` | 限制到一台远端机器 |
|
|
126
|
+
| `--local` | 只使用当前机器 |
|
|
127
|
+
| `--cdp` | 当前机器强制使用 CDP,并隐含 `--local` |
|
|
128
|
+
| `--server <url>` | 覆盖自动读取的 cloud-server 地址 |
|
|
129
|
+
|
|
130
|
+
`browser_list --help` 和 `browser_launch --help` 会按场景展示宿主选择参数;连接或传输排障运行
|
|
131
|
+
`mearl check --help`。其中 `--json` 仅用于输出机器可读的检查结果。
|
|
132
|
+
|
|
133
|
+
## Skill CLI 插件
|
|
134
|
+
|
|
135
|
+
安装 skill 并自动注册命令,或直接注册已有入口:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @mearl/setup plugin install trip/mearl --skill yuque-doc-fetch --yes
|
|
139
|
+
npx @mearl/setup plugin yuque /absolute/path/to/yuque-doc-fetch
|
|
140
|
+
npx @mearl/setup plugin uninstall yuque
|
|
141
|
+
mearl yuque read "https://aliyuque.antfin.com/group/book/doc"
|
|
142
|
+
mearl yuque --payload '{"command":"read","input":{"url":"https://aliyuque.antfin.com/group/book/doc"}}'
|
|
143
|
+
mearl yuque --help
|
|
144
|
+
mearl yuque --version
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
目录默认使用 `scripts/main.mjs`。入口默认导出 `register(app)`,可导出简短的 `description`
|
|
148
|
+
显示在 `mearl` 插件列表中,并通过 `app.command(signature, handler, description?)`
|
|
149
|
+
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。CLI 默认只输出最终结果,传入
|
|
150
|
+
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
151
|
+
自动注册默认以 skill 安装目录名作为命令名,也可在入口导出 `name` 指定短名称。
|
|
152
|
+
setup 会补齐缺失或低于自身版本的 client、native-host;安装参数透传给 Ali Skills,并固定全局共享安装。
|
|
153
|
+
|
|
154
|
+
需要单独检查插件时运行 `mearl <name> --version`。命令会解析注册记录并加载入口,但不连接浏览器;
|
|
155
|
+
退出码为 0 表示插件已注册且入口可加载,stdout 返回同目录 `SKILL.md` 的版本,缺少版本元数据时返回 `unknown`。
|
|
156
|
+
未注册、入口丢失或加载失败会以退出码 2 和对应的 `PLUGIN_*` 错误结束。普通业务调用无需预先检查,直接处理同样的错误即可。
|
|
157
|
+
|
|
158
|
+
插件接受位置参数或包含 command/input 的 `--payload`,两种形式不能混用。JSON 保留业务字段类型;正文等文件输入
|
|
159
|
+
按具体 skill 的字段约定处理。插件不使用上表中内置 action 的 `--payload-file` 入口。
|
|
160
|
+
|
|
161
|
+
注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
|
|
162
|
+
`plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
|
|
163
|
+
成功后清除相关注册;即使入口目录已被手动删除,也会完成 Ali Skills 跟踪记录的清理。
|
|
164
|
+
手工路径只解除注册并保留源码。
|
|
165
|
+
`PLUGIN_NOT_REGISTERED` 表示插件尚未注册。`PLUGIN_ENTRY_MISSING` 会显示失效入口,并给出带有实际插件名的
|
|
166
|
+
卸载清理命令。其他错误按实际原因处理。
|
|
167
|
+
编写规范见 [Mearl Skill Creator 的 CLI 插件规范](../skills/mearl-skill-creator/references/cli-plugin.md)。
|
|
99
168
|
|
|
100
169
|
## 支持的操作
|
|
101
170
|
|
|
@@ -120,7 +189,9 @@ mearl browser_release
|
|
|
120
189
|
|
|
121
190
|
重复文本点击可用 `page_click.scope` 限定 CSS / ref 子树;浏览器扩展后端中,容器或 iframe 内的点坐标使用 `coordinateSpace: "active-frame"`,CSS/text 查询可用 `frameId` 显式指定 frame。点击结果通过 `requestedTarget`、`resolvedTarget`、`dispatchTarget`、`hitTarget` 和 `coordinates` 区分请求语义、DOM 解析、派发节点与可信输入实际命中;ref 指向滚动容器内的子节点时,可用 `page_scroll.containerPolicy: "nearest"` 自动解析最近可滚动祖先。
|
|
122
191
|
|
|
123
|
-
`browser_list`
|
|
192
|
+
`browser_list` 统一列出普通浏览器、Provider 浏览器和托管浏览器。`type` 区分
|
|
193
|
+
`regular` / `managed`,`status` 区分 `connected` / `pairing` / `running_disconnected` /
|
|
194
|
+
`stopped`;只有 `connected` 的浏览器可作为常规操作目标。
|
|
124
195
|
|
|
125
196
|
按 `sessionId` 接入的 AgentBay 浏览器用 `browser_release` 收尾:它会移除本机 attachment 并退出对应宿主,但不会关闭云端 Session。其他浏览器仅在用户明确要求结束调试、退出接管或清理会话资源时调用;它会释放 debugger/CDP 控制和会话级临时状态(包括设备/时区模拟),保留浏览器及全部标签页,后续浏览器操作可自动重新建立控制。
|
|
126
197
|
|
|
@@ -131,8 +202,10 @@ mearl browser_release
|
|
|
131
202
|
```
|
|
132
203
|
@mearl/client (CLI / SDK)
|
|
133
204
|
├─ 本机 → @mearl/native-host → Chrome Extension / CDP
|
|
205
|
+
├─ 本机 → @mearl/provider runtime → External Provider
|
|
134
206
|
└─ WebSocket → @mearl/cloud-server → @mearl/cloud-connector
|
|
135
|
-
|
|
207
|
+
├─ @mearl/native-host → Chrome Extension / CDP
|
|
208
|
+
└─ @mearl/provider runtime → External Provider
|
|
136
209
|
```
|
|
137
210
|
|
|
138
211
|
基础设施组件必须避免递归路由:`@mearl/cloud-connector` 使用 `@mearl/client/local` 子路径直连所在机器的 native-host。普通 Agent、CLI 与 MCP 集成均使用包根入口。
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* registry directory and record shape; reading scans every *.json in the dir,
|
|
9
9
|
* so it never needs to replicate daemon-core's filename hashing.
|
|
10
10
|
*/
|
|
11
|
+
import type { BrowserCapabilities, BrowserPairingInfo, BrowserTransport } from './generated-browser-action-protocol.js';
|
|
11
12
|
export interface LiveBrowser {
|
|
12
13
|
pid: number;
|
|
13
14
|
browserId: string;
|
|
@@ -15,7 +16,15 @@ export interface LiveBrowser {
|
|
|
15
16
|
browserName: string;
|
|
16
17
|
extensionVersion?: string;
|
|
17
18
|
nativeHostVersion?: string;
|
|
18
|
-
transport?:
|
|
19
|
+
transport?: BrowserTransport;
|
|
20
|
+
providerBrowserId?: string;
|
|
21
|
+
providerId?: string;
|
|
22
|
+
providerName?: string;
|
|
23
|
+
providerVersion?: string;
|
|
24
|
+
providerProtocolVersion?: number;
|
|
25
|
+
capabilities?: BrowserCapabilities;
|
|
26
|
+
status?: 'connected' | 'pairing';
|
|
27
|
+
pairing?: BrowserPairingInfo;
|
|
19
28
|
lastFocusedAt?: number;
|
|
20
29
|
updatedAt?: number;
|
|
21
30
|
}
|
package/dist/check.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { CloudConnectorInfo } from '@mearl/cloud-types';
|
|
2
|
+
import type { BrowserCapabilities } from './generated-browser-action-protocol.js';
|
|
2
3
|
import { type UnifiedClientOptions, type UnifiedInvokeOptions } from './unified.js';
|
|
3
4
|
export interface UnifiedCheckVersions {
|
|
4
5
|
nativeHost?: string;
|
|
5
6
|
extension?: string | null;
|
|
6
7
|
browser?: string | null;
|
|
8
|
+
provider?: ProviderVersionInfo;
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderVersionInfo {
|
|
11
|
+
providerId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
protocolVersion: number;
|
|
7
15
|
}
|
|
8
16
|
export type BrowserAccessCapability = {
|
|
9
17
|
kind: 'agentbay';
|
|
@@ -25,7 +33,11 @@ interface BrowserInfo {
|
|
|
25
33
|
status: string;
|
|
26
34
|
type?: string;
|
|
27
35
|
provider?: 'local' | 'agentbay';
|
|
28
|
-
|
|
36
|
+
providerId?: string;
|
|
37
|
+
providerName?: string;
|
|
38
|
+
providerVersion?: string;
|
|
39
|
+
providerProtocolVersion?: number;
|
|
40
|
+
transport?: 'extension' | 'cdp' | 'provider';
|
|
29
41
|
}
|
|
30
42
|
interface CheckItem {
|
|
31
43
|
status: 'ok' | 'warning' | 'error';
|
|
@@ -38,6 +50,7 @@ export interface ExtensionInstallationInfo {
|
|
|
38
50
|
}
|
|
39
51
|
export interface UnifiedCheckBrowserResult extends BrowserInfo {
|
|
40
52
|
browserAccess?: BrowserAccess;
|
|
53
|
+
capabilities?: BrowserCapabilities;
|
|
41
54
|
check?: {
|
|
42
55
|
errors?: string[];
|
|
43
56
|
label: string;
|
package/dist/check.js
CHANGED
|
@@ -56,7 +56,9 @@ function formatBrowser(browser, isDefault) {
|
|
|
56
56
|
? ' [CDP]'
|
|
57
57
|
: browser.transport === 'extension'
|
|
58
58
|
? ' [Extension]'
|
|
59
|
-
: ''
|
|
59
|
+
: browser.transport === 'provider'
|
|
60
|
+
? ` [Provider: ${browser.providerName || browser.providerId || 'external'}]`
|
|
61
|
+
: '';
|
|
60
62
|
const type = browser.type === 'managed' ? ' [Managed]' : '';
|
|
61
63
|
return ` ${isDefault ? '★' : ' '} ${browser.name} [${shortBrowserId(browser.browserId)}]${type}${transport}`;
|
|
62
64
|
}
|
|
@@ -220,12 +222,20 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
220
222
|
...(versions.nativeHost ? { nativeHost: versions.nativeHost } : {}),
|
|
221
223
|
...(versions.extension !== undefined ? { extension: versions.extension } : {}),
|
|
222
224
|
...(versions.browser !== undefined ? { browser: versions.browser } : {}),
|
|
225
|
+
...(versions.provider ? { provider: versions.provider } : {}),
|
|
223
226
|
};
|
|
224
227
|
const details = [
|
|
225
228
|
versions.nativeHost ? `native-host v${versions.nativeHost}` : null,
|
|
226
229
|
versions.extension ? `extension v${versions.extension}` : null,
|
|
227
230
|
versions.browser ? `Chrome ${versions.browser}` : null,
|
|
231
|
+
versions.provider
|
|
232
|
+
? `${versions.provider.name} v${versions.provider.version} (protocol v${versions.provider.protocolVersion})`
|
|
233
|
+
: null,
|
|
228
234
|
].filter((value) => value !== null);
|
|
235
|
+
const capabilities = options.browser ? versions.capabilities : undefined;
|
|
236
|
+
if (capabilities) {
|
|
237
|
+
details.push(`capabilities (${capabilities.actions.length}): ${capabilities.actions.join(', ') || 'none'}`, `screenshot: ${capabilities.screenshot}`);
|
|
238
|
+
}
|
|
229
239
|
let extensionInstallation;
|
|
230
240
|
if (options.local &&
|
|
231
241
|
browser.transport === 'extension' &&
|
|
@@ -253,6 +263,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
253
263
|
browserChecks.push({
|
|
254
264
|
...(browserAccess ? { browserAccess } : {}),
|
|
255
265
|
browser,
|
|
266
|
+
...(capabilities ? { capabilities } : {}),
|
|
256
267
|
...(extensionInstallation ? { extensionInstallation } : {}),
|
|
257
268
|
item: {
|
|
258
269
|
status: accessUrlWarning ? 'warning' : 'ok',
|
|
@@ -302,6 +313,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
302
313
|
...browserIdentity,
|
|
303
314
|
selected: targets.some(target => target.browserId === browser.browserId),
|
|
304
315
|
...(check?.browserAccess ? { browserAccess: check.browserAccess } : {}),
|
|
316
|
+
...(check?.capabilities ? { capabilities: check.capabilities } : {}),
|
|
305
317
|
...(check?.item
|
|
306
318
|
? {
|
|
307
319
|
check: {
|
package/dist/cli.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface ParsedArgs {
|
|
|
16
16
|
serverUrlSource?: string;
|
|
17
17
|
timeoutSec: number;
|
|
18
18
|
timeoutOption?: string;
|
|
19
|
+
pluginArgs?: string[];
|
|
20
|
+
help?: boolean;
|
|
21
|
+
version?: boolean;
|
|
22
|
+
verbose?: boolean;
|
|
23
|
+
hasPayload?: boolean;
|
|
19
24
|
}
|
|
20
|
-
export declare function parseClientArgs(args: string[]): ParsedArgs;
|
|
25
|
+
export declare function parseClientArgs(args: string[], plugin?: boolean): ParsedArgs;
|
|
21
26
|
export declare function runMearlCli(args: string[], options?: MearlCliOptions): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { Command, CommanderError } from 'commander';
|
|
4
|
+
import { listInstalledProviders } from '@mearl/provider/host';
|
|
4
5
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
5
6
|
import { runUnifiedCheck, parseCheckTimeout } from './check.js';
|
|
6
7
|
import { commandInput, findCommandSuggestions, formatCommandSuggestions, } from './commandSuggestions.js';
|
|
7
8
|
import { COMMANDS, COMMAND_MAP } from './commands.js';
|
|
8
9
|
import { CLIENT_VERSION } from './socket.js';
|
|
9
10
|
import { UnifiedClient } from './unified.js';
|
|
11
|
+
import { findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, PluginError, } from './plugin.js';
|
|
10
12
|
import { getCloudServerConfigPath, loadCloudServerConfig } from './unifiedConfig.js';
|
|
13
|
+
const TOP_LEVEL_COMMAND_NAME_WIDTH = 22;
|
|
11
14
|
const CATEGORIES = [
|
|
12
15
|
{
|
|
13
16
|
title: 'API 调试',
|
|
@@ -73,24 +76,20 @@ function printUsage(commandName, version) {
|
|
|
73
76
|
' browser_list 默认聚合当前机器及已配置 cloud-server 下的浏览器。',
|
|
74
77
|
' 后续操作通过 --browser <browserId> 指定目标,客户端会自动选择本地或远端传输。',
|
|
75
78
|
'',
|
|
76
|
-
'
|
|
79
|
+
'常用选项:',
|
|
77
80
|
' --payload <json> 直接传入 JSON 参数',
|
|
78
81
|
' --payload-file <path> 从文件读取整个 JSON 参数',
|
|
79
82
|
' --timeout <seconds> 超时时间(默认按 action 推导)',
|
|
80
83
|
' --compact stdout 输出紧凑格式 JSON',
|
|
81
84
|
' --output <path> 保存输出或截图',
|
|
82
85
|
' --browser <id|名称> 指定 browser_list 返回的浏览器',
|
|
83
|
-
' --connector <id|名称> 限制到一台远端 connector',
|
|
84
|
-
' --local 只使用当前机器上的浏览器',
|
|
85
|
-
' --cdp 当前机器强制使用 CDP(隐含 --local)',
|
|
86
|
-
' --server <url> 覆盖 cloud-server WebSocket URL',
|
|
87
86
|
'',
|
|
88
87
|
'其他选项:',
|
|
89
88
|
' -h, --help 查看帮助',
|
|
90
89
|
' -v, --version 查看版本',
|
|
91
90
|
'',
|
|
92
91
|
'内置命令:',
|
|
93
|
-
'
|
|
92
|
+
` ${'check'.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}检查指定浏览器,未指定时检查全部已连接浏览器`,
|
|
94
93
|
];
|
|
95
94
|
for (const category of CATEGORIES) {
|
|
96
95
|
lines.push('', ` ${category.title}`);
|
|
@@ -100,6 +99,30 @@ function printUsage(commandName, version) {
|
|
|
100
99
|
lines.push(` ${name.padEnd(26)}${command.summary ?? command.description}`);
|
|
101
100
|
}
|
|
102
101
|
}
|
|
102
|
+
try {
|
|
103
|
+
const plugins = listPluginNames();
|
|
104
|
+
if (plugins.length) {
|
|
105
|
+
const entries = plugins.map(name => {
|
|
106
|
+
try {
|
|
107
|
+
const description = findPluginRegistration(name)?.description;
|
|
108
|
+
return description
|
|
109
|
+
? ` ${name.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}${description}`
|
|
110
|
+
: ` ${name}`;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return ` ${name}`;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
lines.push('', '已注册插件:', ...entries);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
lines.push('', error instanceof Error ? error.message : String(error));
|
|
121
|
+
}
|
|
122
|
+
const providers = listInstalledProviders();
|
|
123
|
+
if (providers.length) {
|
|
124
|
+
lines.push('', '已注册 Provider:', ...providers.map(provider => ` ${provider.providerId.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}${provider.name} v${provider.version} (${provider.packageName})`));
|
|
125
|
+
}
|
|
103
126
|
lines.push('', `运行 ${commandName} <action> --help 查看 action 参数。`);
|
|
104
127
|
console.error(lines.join('\n'));
|
|
105
128
|
}
|
|
@@ -140,7 +163,7 @@ function printCommandHelp(commandName, command) {
|
|
|
140
163
|
` ${command.description}`,
|
|
141
164
|
'',
|
|
142
165
|
'用法:',
|
|
143
|
-
` ${commandName} ${command.name}${payloadUsage} [
|
|
166
|
+
` ${commandName} ${command.name}${payloadUsage} [常用选项]`,
|
|
144
167
|
'',
|
|
145
168
|
];
|
|
146
169
|
if (command.params?.length) {
|
|
@@ -151,18 +174,24 @@ function printCommandHelp(commandName, command) {
|
|
|
151
174
|
}
|
|
152
175
|
lines.push('');
|
|
153
176
|
}
|
|
177
|
+
if (command.name === 'browser_list') {
|
|
178
|
+
lines.push('目标范围(按需):', ' --local 只列出当前机器上的浏览器', ' --connector <id|名称> 只列出指定远端 connector 的浏览器', '');
|
|
179
|
+
}
|
|
180
|
+
else if (command.name === 'browser_launch') {
|
|
181
|
+
lines.push('宿主选择(本机与远端同时可用时):', ' --local 在当前机器启动', ' --connector <id|名称> 在指定远端 connector 启动', ' --browser <id|名称> 在控制浏览器所在宿主启动,并可作为 Cookie/UA 来源', '');
|
|
182
|
+
}
|
|
154
183
|
if (command.examples?.length) {
|
|
155
184
|
lines.push('示例:');
|
|
156
185
|
for (const example of command.examples)
|
|
157
186
|
lines.push(` ${commandName} ${example}`);
|
|
158
187
|
}
|
|
159
|
-
lines.push('', `运行 ${commandName} --help
|
|
188
|
+
lines.push('', `运行 ${commandName} --help 查看常用选项。`);
|
|
160
189
|
console.error(lines.join('\n'));
|
|
161
190
|
}
|
|
162
191
|
function printUnknownCommand(commandName, args) {
|
|
163
192
|
const input = commandInput(args) || args[0] || '';
|
|
164
193
|
const suggestions = findCommandSuggestions(args, SUGGESTABLE_COMMANDS);
|
|
165
|
-
const lines = [
|
|
194
|
+
const lines = [`PLUGIN_NOT_REGISTERED: 不支持的命令: ${input}`];
|
|
166
195
|
if (suggestions.length > 0) {
|
|
167
196
|
lines.push('', '你可能想用:', ...formatCommandSuggestions(commandName, suggestions));
|
|
168
197
|
}
|
|
@@ -213,7 +242,32 @@ function resolveServer(explicit) {
|
|
|
213
242
|
? { serverUrl: config.server, serverUrlSource: getCloudServerConfigPath() }
|
|
214
243
|
: {};
|
|
215
244
|
}
|
|
216
|
-
|
|
245
|
+
function normalizeScreenshotOutput(action, payload, explicitOutput) {
|
|
246
|
+
if (action !== 'page_screenshot' || !Object.hasOwn(payload, 'output')) {
|
|
247
|
+
return { payload, ...(explicitOutput ? { outputPath: explicitOutput } : {}) };
|
|
248
|
+
}
|
|
249
|
+
const payloadOutput = payload.output;
|
|
250
|
+
if (typeof payloadOutput !== 'string' || payloadOutput.trim() === '') {
|
|
251
|
+
throw new Error('page_screenshot 的 payload.output 必须是非空路径');
|
|
252
|
+
}
|
|
253
|
+
if (explicitOutput !== undefined) {
|
|
254
|
+
throw new Error('page_screenshot 的 payload.output 不能与 --output 同时使用');
|
|
255
|
+
}
|
|
256
|
+
const { output: _output, ...normalizedPayload } = payload;
|
|
257
|
+
return { payload: normalizedPayload, outputPath: payloadOutput };
|
|
258
|
+
}
|
|
259
|
+
function actionInvocationFailed(action, result) {
|
|
260
|
+
if (result?.action?.success === false)
|
|
261
|
+
return true;
|
|
262
|
+
if (action !== 'run_actions')
|
|
263
|
+
return false;
|
|
264
|
+
if (Number.isInteger(result?.failedStep))
|
|
265
|
+
return true;
|
|
266
|
+
return Array.isArray(result?.results)
|
|
267
|
+
? result.results.some((step) => step?.success === false)
|
|
268
|
+
: false;
|
|
269
|
+
}
|
|
270
|
+
export function parseClientArgs(args, plugin = false) {
|
|
217
271
|
const program = new Command()
|
|
218
272
|
.name('mearl')
|
|
219
273
|
.helpOption(false)
|
|
@@ -232,6 +286,12 @@ export function parseClientArgs(args) {
|
|
|
232
286
|
.option('--local')
|
|
233
287
|
.option('--cdp')
|
|
234
288
|
.option('--server <url>');
|
|
289
|
+
if (plugin)
|
|
290
|
+
program
|
|
291
|
+
.argument('[pluginArgs...]')
|
|
292
|
+
.option('-h, --help')
|
|
293
|
+
.option('-v, --version')
|
|
294
|
+
.option('--verbose');
|
|
235
295
|
try {
|
|
236
296
|
program.parse(['node', 'mearl', ...args]);
|
|
237
297
|
}
|
|
@@ -243,10 +303,14 @@ export function parseClientArgs(args) {
|
|
|
243
303
|
}
|
|
244
304
|
const action = program.processedArgs[0];
|
|
245
305
|
const options = program.opts();
|
|
306
|
+
if (plugin && options.payloadFile !== undefined) {
|
|
307
|
+
throw new Error('插件使用 --payload 传入 command 和 input;正文文件可由业务字段引用');
|
|
308
|
+
}
|
|
246
309
|
const json = options.json === true;
|
|
247
310
|
if (json && action !== 'check')
|
|
248
311
|
throw new Error('--json 仅适用于 check');
|
|
249
|
-
const
|
|
312
|
+
const parsedPayload = parsePayload(options.payload, options.payloadFile);
|
|
313
|
+
const { payload, outputPath } = normalizeScreenshotOutput(action, parsedPayload, options.output);
|
|
250
314
|
const fallbackTimeout = resolveActionTimeoutSec(action, payload, 60);
|
|
251
315
|
const forceCdp = options.cdp === true;
|
|
252
316
|
const local = forceCdp || options.local === true;
|
|
@@ -262,12 +326,89 @@ export function parseClientArgs(args) {
|
|
|
262
326
|
forceCdp,
|
|
263
327
|
json,
|
|
264
328
|
local,
|
|
265
|
-
...(
|
|
329
|
+
...(outputPath ? { outputPath } : {}),
|
|
266
330
|
...(options.browser ? { browser: options.browser } : {}),
|
|
267
331
|
...(connector ? { connector } : {}),
|
|
268
332
|
...resolveServer(options.server),
|
|
333
|
+
...(plugin
|
|
334
|
+
? {
|
|
335
|
+
pluginArgs: program.processedArgs[1],
|
|
336
|
+
help: options.help === true,
|
|
337
|
+
version: options.version === true,
|
|
338
|
+
verbose: options.verbose === true,
|
|
339
|
+
hasPayload: options.payload !== undefined,
|
|
340
|
+
}
|
|
341
|
+
: {}),
|
|
269
342
|
};
|
|
270
343
|
}
|
|
344
|
+
async function runPluginCli(args, entry, commandName) {
|
|
345
|
+
let parsed;
|
|
346
|
+
try {
|
|
347
|
+
parsed = parseClientArgs(args, true);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
351
|
+
return 2;
|
|
352
|
+
}
|
|
353
|
+
let client;
|
|
354
|
+
try {
|
|
355
|
+
const invokeOptions = {
|
|
356
|
+
...(parsed.timeoutOption ? { timeoutSec: parsed.timeoutSec } : {}),
|
|
357
|
+
...(parsed.browser ? { browser: parsed.browser } : {}),
|
|
358
|
+
...(parsed.connector ? { connector: parsed.connector } : {}),
|
|
359
|
+
...(parsed.local ? { local: true } : {}),
|
|
360
|
+
...(parsed.forceCdp ? { transport: 'cdp' } : {}),
|
|
361
|
+
};
|
|
362
|
+
const plugin = await loadPlugin(entry, {
|
|
363
|
+
options: invokeOptions,
|
|
364
|
+
progress(message) {
|
|
365
|
+
if (parsed.verbose) {
|
|
366
|
+
process.stderr.write(message.endsWith('\n') ? message : `${message}\n`);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
async invoke(action, payload, options) {
|
|
370
|
+
client ??= new UnifiedClient({
|
|
371
|
+
...(parsed.serverUrl ? { serverUrl: parsed.serverUrl } : {}),
|
|
372
|
+
});
|
|
373
|
+
const result = await client.invoke(action, payload, { ...invokeOptions, ...options });
|
|
374
|
+
if (result?.action?.success === false)
|
|
375
|
+
throw new Error(result.action.error || `${action} failed`);
|
|
376
|
+
return result;
|
|
377
|
+
},
|
|
378
|
+
}, parsed.action);
|
|
379
|
+
if (parsed.version) {
|
|
380
|
+
console.log(plugin.version ?? 'unknown');
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
if (parsed.help || (!parsed.pluginArgs?.length && !parsed.hasPayload)) {
|
|
384
|
+
console.error(plugin.help(`${commandName} ${parsed.action}`, parsed.pluginArgs?.[0]));
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
const result = await plugin.run(parsed.pluginArgs ?? [], parsed.hasPayload ? parsed.payload : undefined);
|
|
388
|
+
if (result !== undefined) {
|
|
389
|
+
if (parsed.outputPath)
|
|
390
|
+
writeOutput('', result, parsed.outputPath);
|
|
391
|
+
else if (typeof result === 'string')
|
|
392
|
+
process.stdout.write(result.endsWith('\n') ? result : `${result}\n`);
|
|
393
|
+
else
|
|
394
|
+
process.stdout.write(`${JSON.stringify(result, null, parsed.compact ? undefined : 2)}\n`);
|
|
395
|
+
}
|
|
396
|
+
return result &&
|
|
397
|
+
typeof result === 'object' &&
|
|
398
|
+
!Array.isArray(result) &&
|
|
399
|
+
'success' in result &&
|
|
400
|
+
result.success === false
|
|
401
|
+
? 1
|
|
402
|
+
: 0;
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
406
|
+
return error instanceof PluginError ? 2 : 1;
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
client?.disconnect();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
271
412
|
function screenshotData(action, result) {
|
|
272
413
|
if (typeof result === 'string' && result.startsWith('data:image')) {
|
|
273
414
|
return result.split(',')[1] ?? null;
|
|
@@ -294,7 +435,23 @@ export async function runMearlCli(args, options = {}) {
|
|
|
294
435
|
const clientVersion = options.clientVersion ?? CLIENT_VERSION;
|
|
295
436
|
if (args[0] === 'capture_checkpoint')
|
|
296
437
|
args = ['tab_checkpoint', ...args.slice(1)];
|
|
297
|
-
if (args
|
|
438
|
+
if (args[0] &&
|
|
439
|
+
!args[0].startsWith('-') &&
|
|
440
|
+
!args[0].startsWith('__') &&
|
|
441
|
+
args[0] !== 'check' &&
|
|
442
|
+
!ADVANCED_BUILTIN_COMMANDS.has(args[0]) &&
|
|
443
|
+
!COMMAND_MAP.has(args[0])) {
|
|
444
|
+
try {
|
|
445
|
+
const entry = findPluginEntry(args[0]);
|
|
446
|
+
if (entry)
|
|
447
|
+
return await runPluginCli(args, entry, commandName);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
451
|
+
return 2;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
298
455
|
console.log(clientVersion);
|
|
299
456
|
return 0;
|
|
300
457
|
}
|
|
@@ -377,7 +534,7 @@ export async function runMearlCli(args, options = {}) {
|
|
|
377
534
|
else {
|
|
378
535
|
process.stdout.write(`${parsed.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2)}\n`);
|
|
379
536
|
}
|
|
380
|
-
return
|
|
537
|
+
return actionInvocationFailed(parsed.action, result) ? 1 : 0;
|
|
381
538
|
}
|
|
382
539
|
catch (error) {
|
|
383
540
|
console.error(error instanceof Error ? error.message : String(error));
|
package/dist/commands.d.ts
CHANGED
|
@@ -391,15 +391,19 @@ export declare const COMMANDS: readonly [{
|
|
|
391
391
|
readonly examples: ["browser_release", "browser_release --browser chrome-a"];
|
|
392
392
|
}, {
|
|
393
393
|
readonly name: "browser_launch";
|
|
394
|
-
readonly description: "启动本地 Chrome
|
|
394
|
+
readonly description: "启动本地 Chrome、创建/复用 AgentBay 云浏览器,或通过已注册 Provider 创建浏览器实例";
|
|
395
395
|
readonly params: [{
|
|
396
396
|
readonly name: "name";
|
|
397
397
|
readonly type: "string";
|
|
398
398
|
readonly description: "实例名称,创建浏览器时必填;按 sessionId 接入时可省略";
|
|
399
399
|
}, {
|
|
400
400
|
readonly name: "provider";
|
|
401
|
-
readonly type: "
|
|
402
|
-
readonly description: "运行环境,默认 local
|
|
401
|
+
readonly type: "string";
|
|
402
|
+
readonly description: "运行环境,默认 local;可传 agentbay 或 mearl --help 中展示的已注册 Provider ID";
|
|
403
|
+
}, {
|
|
404
|
+
readonly name: "providerOptions";
|
|
405
|
+
readonly type: "Record<string, string>";
|
|
406
|
+
readonly description: "外部 Provider 注册时声明的实例创建参数";
|
|
403
407
|
}, {
|
|
404
408
|
readonly name: "sessionId";
|
|
405
409
|
readonly type: "string";
|
|
@@ -457,7 +461,7 @@ export declare const COMMANDS: readonly [{
|
|
|
457
461
|
readonly type: "string[] | \"mearl-services\" | { presets?: [\"mearl-services\"]; domains?: string[] }";
|
|
458
462
|
readonly description: "从控制浏览器复制 Cookie:可传域名数组、Mearl 登录态预设,或组合预设与额外域名";
|
|
459
463
|
}];
|
|
460
|
-
readonly examples: ["browser_launch --payload '{\"name\":\"cloud-a\",\"provider\":\"agentbay\",\"userAgentMode\":\"desktop\",\"url\":\"https://example.com\"}'", "browser_launch --payload '{\"name\":\"cloud-public\",\"provider\":\"agentbay\",\"imageId\":\"browser_latest\",\"url\":\"https://example.com\"}'", "browser_launch --payload '{\"name\":\"cloud-login\",\"provider\":\"agentbay\",\"persistent\":true,\"copyCookieDomains\":[\"example.com\"],\"url\":\"https://example.com/account\"}'", "browser_launch --payload '{\"name\":\"cloud-login\",\"provider\":\"agentbay\",\"reuse\":\"require\",\"copyCookieDomains\":[\"example.com\"]}'", "browser_launch --browser '<本地浏览器 browserId>' --payload '{\"sessionId\":\"<AgentBay sessionId>\",\"copyCookieDomains\":[\"example.com\"]}'", "browser_launch --payload '{\"name\":\"cloud-tools\",\"provider\":\"agentbay\",\"copyCookieDomains\":\"mearl-services\"}'", "browser_launch --payload '{\"name\":\"cloud-tools\",\"provider\":\"agentbay\",\"copyCookieDomains\":{\"presets\":[\"mearl-services\"],\"domains\":[\"figma.com\"]}}'", "browser_launch --payload '{\"name\":\"account-a\",\"headless\":true,\"accountId\":12345}'", "browser_launch --payload '{\"name\":\"figma\",\"userAgentMode\":\"desktop\",\"copyCookieDomains\":[\"figma.com\"]}'", "browser_launch --payload '{\"name\":\"account-b\",\"query\":\"test_account\",\"url\":\"https://www.taobao.com\"}'", "browser_launch --payload '{\"name\":\"with-cookies\",\"copyCookieDomains\":[\"example.com\",\"sub.example.org\"]}'"];
|
|
464
|
+
readonly examples: ["browser_launch --payload '{\"provider\":\"brow\",\"providerOptions\":{}}'", "browser_launch --payload '{\"name\":\"cloud-a\",\"provider\":\"agentbay\",\"userAgentMode\":\"desktop\",\"url\":\"https://example.com\"}'", "browser_launch --payload '{\"name\":\"cloud-public\",\"provider\":\"agentbay\",\"imageId\":\"browser_latest\",\"url\":\"https://example.com\"}'", "browser_launch --payload '{\"name\":\"cloud-login\",\"provider\":\"agentbay\",\"persistent\":true,\"copyCookieDomains\":[\"example.com\"],\"url\":\"https://example.com/account\"}'", "browser_launch --payload '{\"name\":\"cloud-login\",\"provider\":\"agentbay\",\"reuse\":\"require\",\"copyCookieDomains\":[\"example.com\"]}'", "browser_launch --browser '<本地浏览器 browserId>' --payload '{\"sessionId\":\"<AgentBay sessionId>\",\"copyCookieDomains\":[\"example.com\"]}'", "browser_launch --payload '{\"name\":\"cloud-tools\",\"provider\":\"agentbay\",\"copyCookieDomains\":\"mearl-services\"}'", "browser_launch --payload '{\"name\":\"cloud-tools\",\"provider\":\"agentbay\",\"copyCookieDomains\":{\"presets\":[\"mearl-services\"],\"domains\":[\"figma.com\"]}}'", "browser_launch --payload '{\"name\":\"account-a\",\"headless\":true,\"accountId\":12345}'", "browser_launch --payload '{\"name\":\"figma\",\"userAgentMode\":\"desktop\",\"copyCookieDomains\":[\"figma.com\"]}'", "browser_launch --payload '{\"name\":\"account-b\",\"query\":\"test_account\",\"url\":\"https://www.taobao.com\"}'", "browser_launch --payload '{\"name\":\"with-cookies\",\"copyCookieDomains\":[\"example.com\",\"sub.example.org\"]}'"];
|
|
461
465
|
}, {
|
|
462
466
|
readonly name: "browser_close";
|
|
463
467
|
readonly description: "关闭指定托管浏览器并按策略保留或清理本地 Profile / AgentBay Context";
|
package/dist/commands.js
CHANGED
|
@@ -296,7 +296,7 @@ export const COMMANDS = [
|
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
name: 'browser_launch',
|
|
299
|
-
description: '启动本地 Chrome
|
|
299
|
+
description: '启动本地 Chrome、创建/复用 AgentBay 云浏览器,或通过已注册 Provider 创建浏览器实例',
|
|
300
300
|
params: [
|
|
301
301
|
{
|
|
302
302
|
name: 'name',
|
|
@@ -305,8 +305,13 @@ export const COMMANDS = [
|
|
|
305
305
|
},
|
|
306
306
|
{
|
|
307
307
|
name: 'provider',
|
|
308
|
-
type: '
|
|
309
|
-
description: '运行环境,默认 local
|
|
308
|
+
type: 'string',
|
|
309
|
+
description: '运行环境,默认 local;可传 agentbay 或 mearl --help 中展示的已注册 Provider ID',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'providerOptions',
|
|
313
|
+
type: 'Record<string, string>',
|
|
314
|
+
description: '外部 Provider 注册时声明的实例创建参数',
|
|
310
315
|
},
|
|
311
316
|
{
|
|
312
317
|
name: 'sessionId',
|
|
@@ -360,6 +365,7 @@ export const COMMANDS = [
|
|
|
360
365
|
},
|
|
361
366
|
],
|
|
362
367
|
examples: [
|
|
368
|
+
`browser_launch --payload '{"provider":"brow","providerOptions":{}}'`,
|
|
363
369
|
`browser_launch --payload '{"name":"cloud-a","provider":"agentbay","userAgentMode":"desktop","url":"https://example.com"}'`,
|
|
364
370
|
`browser_launch --payload '{"name":"cloud-public","provider":"agentbay","imageId":"browser_latest","url":"https://example.com"}'`,
|
|
365
371
|
`browser_launch --payload '{"name":"cloud-login","provider":"agentbay","persistent":true,"copyCookieDomains":["example.com"],"url":"https://example.com/account"}'`,
|
|
@@ -123,7 +123,15 @@ export interface TdbankAccountParams {
|
|
|
123
123
|
tabId?: number;
|
|
124
124
|
identityId?: string | number;
|
|
125
125
|
}
|
|
126
|
-
export type BrowserStatus = 'connected' | 'running_disconnected' | 'stopped';
|
|
126
|
+
export type BrowserStatus = 'connected' | 'pairing' | 'running_disconnected' | 'stopped';
|
|
127
|
+
export type BrowserTransport = 'extension' | 'cdp' | 'provider';
|
|
128
|
+
export interface BrowserPairingInfo {
|
|
129
|
+
kind: 'qr';
|
|
130
|
+
url: string;
|
|
131
|
+
/** Optional human-readable or provider-specific pairing code. */
|
|
132
|
+
code?: string;
|
|
133
|
+
label?: string;
|
|
134
|
+
}
|
|
127
135
|
export interface BrowserSource {
|
|
128
136
|
kind: 'local' | 'remote';
|
|
129
137
|
connectorId?: string;
|
|
@@ -139,11 +147,28 @@ export interface BrowserInfoBase {
|
|
|
139
147
|
source?: BrowserSource;
|
|
140
148
|
extensionVersion?: string;
|
|
141
149
|
nativeHostVersion?: string;
|
|
142
|
-
transport?:
|
|
150
|
+
transport?: BrowserTransport;
|
|
151
|
+
/** External control-provider identity. Present only when transport is provider. */
|
|
152
|
+
providerId?: string;
|
|
153
|
+
/** Human-readable Provider name; providerId remains the stable identity. */
|
|
154
|
+
providerName?: string;
|
|
155
|
+
providerVersion?: string;
|
|
156
|
+
providerProtocolVersion?: number;
|
|
157
|
+
/** Setup information exposed by a provider while it waits for an external device. */
|
|
158
|
+
pairing?: BrowserPairingInfo;
|
|
159
|
+
}
|
|
160
|
+
export type BrowserScreenshotKind = 'native' | 'reconstructed' | 'none';
|
|
161
|
+
/** Browser-targeted actions that an external provider may implement. */
|
|
162
|
+
export type BrowserCapabilityAction = ExtensionBrowserAction;
|
|
163
|
+
/** Capabilities advertised by an external browser-control provider. */
|
|
164
|
+
export interface BrowserCapabilities {
|
|
165
|
+
actions: BrowserCapabilityAction[];
|
|
166
|
+
/** Pixel fidelity of page_screenshot, or none when that action is unavailable. */
|
|
167
|
+
screenshot: BrowserScreenshotKind;
|
|
143
168
|
}
|
|
144
169
|
export interface RegularBrowserInfo extends BrowserInfoBase {
|
|
145
170
|
type: 'regular';
|
|
146
|
-
status: 'connected';
|
|
171
|
+
status: 'connected' | 'pairing';
|
|
147
172
|
}
|
|
148
173
|
export interface ManagedBrowserAccount {
|
|
149
174
|
accountId?: string;
|
|
@@ -152,6 +177,7 @@ export interface ManagedBrowserAccount {
|
|
|
152
177
|
}
|
|
153
178
|
export interface ManagedBrowserInfo extends BrowserInfoBase {
|
|
154
179
|
type: 'managed';
|
|
180
|
+
status: 'connected' | 'running_disconnected' | 'stopped';
|
|
155
181
|
provider?: 'local' | 'agentbay';
|
|
156
182
|
headless: boolean;
|
|
157
183
|
persistent: boolean;
|
|
@@ -199,7 +225,7 @@ export interface BrowserReleaseFailure {
|
|
|
199
225
|
export interface BrowserReleaseResult {
|
|
200
226
|
released: boolean;
|
|
201
227
|
hadActiveControl: boolean;
|
|
202
|
-
transports:
|
|
228
|
+
transports: BrowserTransport[];
|
|
203
229
|
affectedTabIds: number[];
|
|
204
230
|
/** True when browser_release also removed a sessionId-based local AgentBay attachment. */
|
|
205
231
|
detached?: boolean;
|
|
@@ -217,8 +243,10 @@ export interface CookieCopyScope {
|
|
|
217
243
|
export interface BrowserLaunchParams {
|
|
218
244
|
/** Required when creating a browser; optional when attaching by AgentBay sessionId. */
|
|
219
245
|
name?: string;
|
|
220
|
-
/**
|
|
221
|
-
provider?:
|
|
246
|
+
/** Built-in runtime or the id of a registered external Provider. */
|
|
247
|
+
provider?: string;
|
|
248
|
+
/** Creation parameters declared by a registered external Provider. */
|
|
249
|
+
providerOptions?: Record<string, string>;
|
|
222
250
|
/** Attach to an exact live AgentBay session without creating or owning it. */
|
|
223
251
|
sessionId?: string;
|
|
224
252
|
/** AgentBay running-session reuse policy. Defaults to never. */
|
|
@@ -879,5 +907,6 @@ export declare const EXTENSION_BROWSER_ACTIONS: {
|
|
|
879
907
|
};
|
|
880
908
|
export declare function isBrowserAction(value: unknown): value is BrowserAction;
|
|
881
909
|
export declare function isBrowserCommandAction(value: unknown): value is BrowserCommandAction;
|
|
910
|
+
export declare function isBrowserCapabilityAction(value: unknown): value is BrowserCapabilityAction;
|
|
882
911
|
export declare function isExtensionBrowserAction(value: unknown): value is ExtensionBrowserAction;
|
|
883
912
|
export {};
|
|
@@ -200,6 +200,9 @@ export function isBrowserAction(value) {
|
|
|
200
200
|
export function isBrowserCommandAction(value) {
|
|
201
201
|
return typeof value === 'string' && Object.hasOwn(BROWSER_COMMAND_ACTIONS, value);
|
|
202
202
|
}
|
|
203
|
+
export function isBrowserCapabilityAction(value) {
|
|
204
|
+
return isExtensionBrowserAction(value);
|
|
205
|
+
}
|
|
203
206
|
export function isExtensionBrowserAction(value) {
|
|
204
207
|
return typeof value === 'string' && Object.hasOwn(EXTENSION_BROWSER_ACTIONS, value);
|
|
205
208
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, listCloudConnectors, } from './unified.js';
|
|
8
8
|
export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
|
|
9
|
-
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
10
|
-
export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserLaunchParams, BrowserListResult, BrowserReleaseFailure, BrowserReleaseResult, BrowserSource, BrowserStatus, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetApiSchemaParams, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, GetUserInfoParams, ManagedBrowserAccount, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSelectedElementParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RecordParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, TdbankAccountParams, } from './generated-browser-action-protocol.js';
|
|
9
|
+
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
10
|
+
export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserLaunchParams, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetApiSchemaParams, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, GetUserInfoParams, ManagedBrowserAccount, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSelectedElementParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RecordParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, TdbankAccountParams, } from './generated-browser-action-protocol.js';
|
|
11
11
|
import { type UnifiedInvokeOptions } from './unified.js';
|
|
12
12
|
import type * as Protocol from './generated-browser-action-protocol.js';
|
|
13
13
|
export declare function tabCheckpoint(params?: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_checkpoint'>>;
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* then routes actions using the global browser ids returned by browser_list.
|
|
6
6
|
*/
|
|
7
7
|
export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invoke, listCloudConnectors, } from './unified.js';
|
|
8
|
-
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
8
|
+
export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_COMMAND_ACTIONS, EXTENSION_BROWSER_ACTIONS, PAGE_ACT_ACTIONS, SEND_REQUEST_MAX_FILES, SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES, SEND_REQUEST_MAX_TOTAL_FILE_BYTES, isBrowserAction, isBrowserCapabilityAction, isBrowserCommandAction, isExtensionBrowserAction, } from './generated-browser-action-protocol.js';
|
|
9
9
|
import { invoke } from './unified.js';
|
|
10
10
|
export function tabCheckpoint(params = {}, options) {
|
|
11
11
|
return invoke('tab_checkpoint', params, options);
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UnifiedInvokeOptions } from './unified.js';
|
|
2
|
+
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, } from './pluginRegistry.js';
|
|
3
|
+
export type PluginOptions = UnifiedInvokeOptions;
|
|
4
|
+
export interface PluginApp {
|
|
5
|
+
readonly options: Readonly<PluginOptions>;
|
|
6
|
+
command(signature: string, handler: PluginHandler, description?: string): void;
|
|
7
|
+
invoke(action: string, payload?: Record<string, any>, options?: UnifiedInvokeOptions): Promise<any>;
|
|
8
|
+
progress(message: string): void;
|
|
9
|
+
}
|
|
10
|
+
export type PluginHandler = (input: Record<string, any>) => unknown | Promise<unknown>;
|
|
11
|
+
/** 注册和执行共享一个 app;仅 handler 执行期间开放浏览器调用。 */
|
|
12
|
+
export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
|
|
13
|
+
name: string | undefined;
|
|
14
|
+
description: string | undefined;
|
|
15
|
+
version: string | undefined;
|
|
16
|
+
help(namespace: string, name?: string): string;
|
|
17
|
+
run(positionals: string[], payload?: unknown): Promise<unknown>;
|
|
18
|
+
}>;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { PluginError } from './pluginRegistry.js';
|
|
6
|
+
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
7
|
+
function readPluginVersion(entry) {
|
|
8
|
+
try {
|
|
9
|
+
const content = readFileSync(resolve(dirname(entry), '..', 'SKILL.md'), 'utf8');
|
|
10
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
|
11
|
+
const versionLine = frontmatter?.split(/\r?\n/).find(line => /^version:\s*/.test(line));
|
|
12
|
+
return versionLine?.match(/^version:\s*["']?([^"'#\s]+)["']?\s*$/)?.[1];
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function missingPluginEntryMessage(entry, pluginName) {
|
|
22
|
+
const name = pluginName ?? '<name>';
|
|
23
|
+
return [
|
|
24
|
+
'插件入口已失效',
|
|
25
|
+
'',
|
|
26
|
+
`插件:${name}`,
|
|
27
|
+
`入口:${entry}`,
|
|
28
|
+
'',
|
|
29
|
+
`卸载命令:npx @mearl/setup plugin uninstall ${name}`,
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
/** 注册和执行共享一个 app;仅 handler 执行期间开放浏览器调用。 */
|
|
33
|
+
export async function loadPlugin(entry, context, pluginName) {
|
|
34
|
+
try {
|
|
35
|
+
if (!statSync(entry).isFile())
|
|
36
|
+
throw new Error('not a file');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new PluginError('PLUGIN_ENTRY_MISSING', missingPluginEntryMessage(entry, pluginName));
|
|
40
|
+
}
|
|
41
|
+
const commands = new Map();
|
|
42
|
+
let executing = false;
|
|
43
|
+
let registering = true;
|
|
44
|
+
let name;
|
|
45
|
+
let description;
|
|
46
|
+
const app = Object.freeze({
|
|
47
|
+
options: Object.freeze({ ...context.options }),
|
|
48
|
+
command(signature, handler, description = '') {
|
|
49
|
+
if (!registering)
|
|
50
|
+
throw new Error('命令只能在入口注册阶段声明');
|
|
51
|
+
const [name, ...args] = signature.trim().split(/\s+/);
|
|
52
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) || commands.has(name) || typeof handler !== 'function') {
|
|
53
|
+
throw new Error(`无效或重复的命令: ${signature}`);
|
|
54
|
+
}
|
|
55
|
+
const command = new Command(name).description(description);
|
|
56
|
+
for (const argument of args) {
|
|
57
|
+
if (!/^(?:<[a-zA-Z][\w-]*(?:\.\.\.)?>|\[[a-zA-Z][\w-]*(?:\.\.\.)?\])$/.test(argument)) {
|
|
58
|
+
throw new Error(`无效的位置参数: ${argument}`);
|
|
59
|
+
}
|
|
60
|
+
command.argument(argument);
|
|
61
|
+
}
|
|
62
|
+
const names = command.registeredArguments.map(argument => argument.name());
|
|
63
|
+
if (new Set(names).size !== names.length)
|
|
64
|
+
throw new Error(`参数名称重复: ${signature}`);
|
|
65
|
+
commands.set(name, { command, signature, handler });
|
|
66
|
+
},
|
|
67
|
+
invoke(action, payload = {}, options = {}) {
|
|
68
|
+
if (!executing)
|
|
69
|
+
throw new Error('app.invoke 只能在命令执行期间调用');
|
|
70
|
+
return context.invoke(action, payload, options);
|
|
71
|
+
},
|
|
72
|
+
progress(message) {
|
|
73
|
+
if (!executing)
|
|
74
|
+
throw new Error('app.progress 只能在命令执行期间调用');
|
|
75
|
+
if (typeof message !== 'string' || !message)
|
|
76
|
+
throw new Error('app.progress 需要非空字符串');
|
|
77
|
+
context.progress(message);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
const module = await import(pathToFileURL(entry).href);
|
|
82
|
+
if (module.name !== undefined && typeof module.name !== 'string')
|
|
83
|
+
throw new Error('入口的 name 导出必须为字符串');
|
|
84
|
+
name = module.name;
|
|
85
|
+
if (module.description !== undefined && typeof module.description !== 'string')
|
|
86
|
+
throw new Error('入口的 description 导出必须为字符串');
|
|
87
|
+
description = module.description;
|
|
88
|
+
if (typeof module.default !== 'function')
|
|
89
|
+
throw new Error('入口必须默认导出 register(app) 函数');
|
|
90
|
+
await module.default(app);
|
|
91
|
+
if (commands.size === 0)
|
|
92
|
+
throw new Error('入口未注册任何命令');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new PluginError('PLUGIN_LOAD_FAILED', `${entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
registering = false;
|
|
99
|
+
}
|
|
100
|
+
const find = (name) => {
|
|
101
|
+
const command = commands.get(name);
|
|
102
|
+
if (!command)
|
|
103
|
+
throw new PluginError('PLUGIN_COMMAND_NOT_FOUND', `未知子命令: ${name}`);
|
|
104
|
+
return command;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
name,
|
|
108
|
+
description,
|
|
109
|
+
version: readPluginVersion(entry),
|
|
110
|
+
help(namespace, name) {
|
|
111
|
+
const selected = name ? [find(name)] : [...commands.values()];
|
|
112
|
+
const example = selected[0].command;
|
|
113
|
+
const input = Object.fromEntries(example.registeredArguments.map(argument => [
|
|
114
|
+
argument.name(),
|
|
115
|
+
argument.variadic ? [`<${argument.name()}>`] : `<${argument.name()}>`,
|
|
116
|
+
]));
|
|
117
|
+
return [
|
|
118
|
+
...selected.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`),
|
|
119
|
+
'',
|
|
120
|
+
` ${namespace} --payload '${JSON.stringify({ command: example.name(), input })}'`,
|
|
121
|
+
'',
|
|
122
|
+
'常用选项: --browser --timeout --compact --output',
|
|
123
|
+
'连接或传输排障运行 mearl check --help。',
|
|
124
|
+
'插件选项: --verbose; -h, --help; -v, --version',
|
|
125
|
+
'JSON input 直接传给 handler;业务字段与校验规则见对应 skill。',
|
|
126
|
+
].join('\n');
|
|
127
|
+
},
|
|
128
|
+
async run(positionals, payload) {
|
|
129
|
+
if (payload !== undefined && positionals.length > 0) {
|
|
130
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数调用与 --payload 调用不能混用');
|
|
131
|
+
}
|
|
132
|
+
if (payload !== undefined &&
|
|
133
|
+
(!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
|
|
134
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
|
|
135
|
+
}
|
|
136
|
+
const envelope = payload;
|
|
137
|
+
const { command, handler } = find(envelope?.command ?? positionals[0] ?? '');
|
|
138
|
+
const input = envelope
|
|
139
|
+
? { ...envelope.input }
|
|
140
|
+
: Object.fromEntries(command.registeredArguments
|
|
141
|
+
.map((argument, index) => [
|
|
142
|
+
argument.name(),
|
|
143
|
+
argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
|
|
144
|
+
])
|
|
145
|
+
.filter(([, value]) => value !== undefined));
|
|
146
|
+
if (!envelope &&
|
|
147
|
+
!command.registeredArguments.some(argument => argument.variadic) &&
|
|
148
|
+
positionals.length - 1 > command.registeredArguments.length) {
|
|
149
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
|
|
150
|
+
}
|
|
151
|
+
for (const argument of command.registeredArguments) {
|
|
152
|
+
const value = Object.hasOwn(input, argument.name()) ? input[argument.name()] : undefined;
|
|
153
|
+
if (argument.required &&
|
|
154
|
+
(value === undefined ||
|
|
155
|
+
value === null ||
|
|
156
|
+
value === '' ||
|
|
157
|
+
(Array.isArray(value) && value.length === 0))) {
|
|
158
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `缺少必填参数: ${argument.name()}`);
|
|
159
|
+
}
|
|
160
|
+
if (argument.variadic && value !== undefined && !Array.isArray(value)) {
|
|
161
|
+
throw new PluginError('PLUGIN_INVALID_INPUT', `${argument.name()} 必须为数组`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
executing = true;
|
|
165
|
+
try {
|
|
166
|
+
return await handler(input);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
executing = false;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class PluginError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface PluginRegistration {
|
|
6
|
+
entry: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function pluginDirectory(): string;
|
|
10
|
+
export declare function assertPluginName(name: string): void;
|
|
11
|
+
export declare function resolvePluginEntry(target: string): string;
|
|
12
|
+
export declare function registerPlugin(name: string, target: string, description?: string): string;
|
|
13
|
+
export declare function unregisterPlugin(name: string): boolean;
|
|
14
|
+
export declare function findPluginRegistration(name: string): PluginRegistration | undefined;
|
|
15
|
+
export declare function findPluginEntry(name: string): string | undefined;
|
|
16
|
+
export declare function listPluginNames(): string[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { COMMAND_MAP } from './commands.js';
|
|
6
|
+
const RESERVED_NAMES = new Set([
|
|
7
|
+
'check',
|
|
8
|
+
'help',
|
|
9
|
+
'connector_list',
|
|
10
|
+
'capture_checkpoint',
|
|
11
|
+
'plugin',
|
|
12
|
+
'plugins',
|
|
13
|
+
]);
|
|
14
|
+
export class PluginError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(`${code}: ${message}`);
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function pluginDirectory() {
|
|
22
|
+
return resolve(process.env.MEARL_PLUGIN_DIR || join(homedir(), '.mearl', 'plugins'));
|
|
23
|
+
}
|
|
24
|
+
export function assertPluginName(name) {
|
|
25
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name) || RESERVED_NAMES.has(name) || COMMAND_MAP.has(name)) {
|
|
26
|
+
throw new PluginError('PLUGIN_INVALID_NAME', `不可用的插件名称: ${name}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function resolvePluginEntry(target) {
|
|
30
|
+
const absolute = resolve(target);
|
|
31
|
+
try {
|
|
32
|
+
const entry = statSync(absolute).isDirectory()
|
|
33
|
+
? join(absolute, 'scripts', 'main.mjs')
|
|
34
|
+
: absolute;
|
|
35
|
+
if (!statSync(entry).isFile())
|
|
36
|
+
throw new Error('not a file');
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new PluginError('PLUGIN_ENTRY_MISSING', `入口不存在: ${absolute}(目录默认使用 scripts/main.mjs)`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function registerPlugin(name, target, description) {
|
|
44
|
+
assertPluginName(name);
|
|
45
|
+
const entry = resolvePluginEntry(target);
|
|
46
|
+
const directory = pluginDirectory();
|
|
47
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
48
|
+
const file = join(directory, `${name}.json`);
|
|
49
|
+
// 每个插件独立保存,多个名称并发注册不会覆盖彼此;同名注册原子替换绑定。
|
|
50
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
51
|
+
const summary = description?.trim();
|
|
52
|
+
const record = { entry, ...(summary ? { description: summary } : {}) };
|
|
53
|
+
try {
|
|
54
|
+
writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
55
|
+
renameSync(temporary, file);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
rmSync(temporary, { force: true });
|
|
59
|
+
}
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
export function unregisterPlugin(name) {
|
|
63
|
+
assertPluginName(name);
|
|
64
|
+
const file = join(pluginDirectory(), `${name}.json`);
|
|
65
|
+
try {
|
|
66
|
+
rmSync(file);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error.code === 'ENOENT')
|
|
71
|
+
return false;
|
|
72
|
+
throw new PluginError('PLUGIN_UNINSTALL_FAILED', `无法移除插件注册: ${file}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function findPluginRegistration(name) {
|
|
76
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name))
|
|
77
|
+
return undefined;
|
|
78
|
+
const file = join(pluginDirectory(), `${name}.json`);
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = readFileSync(file, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === 'ENOENT')
|
|
85
|
+
return undefined;
|
|
86
|
+
throw new PluginError('PLUGIN_REGISTRY_INVALID', `无法读取插件记录: ${file}`);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const record = JSON.parse(content);
|
|
90
|
+
if (typeof record?.entry !== 'string' || !isAbsolute(record.entry))
|
|
91
|
+
throw new Error('invalid entry');
|
|
92
|
+
const description = typeof record.description === 'string' && record.description.trim()
|
|
93
|
+
? record.description.trim()
|
|
94
|
+
: undefined;
|
|
95
|
+
return { entry: record.entry, ...(description ? { description } : {}) };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function findPluginEntry(name) {
|
|
102
|
+
return findPluginRegistration(name)?.entry;
|
|
103
|
+
}
|
|
104
|
+
export function listPluginNames() {
|
|
105
|
+
try {
|
|
106
|
+
return readdirSync(pluginDirectory())
|
|
107
|
+
.filter(file => /^[a-z][a-z0-9-]*\.json$/.test(file))
|
|
108
|
+
.map(file => file.slice(0, -5))
|
|
109
|
+
.sort();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (error.code === 'ENOENT')
|
|
113
|
+
return [];
|
|
114
|
+
throw new PluginError('PLUGIN_REGISTRY_INVALID', '无法读取插件目录');
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/socket.js
CHANGED
|
@@ -12,8 +12,9 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import { StringDecoder } from 'node:string_decoder';
|
|
15
|
+
import { ensureInstalledProvidersRunning } from '@mearl/provider/runtime';
|
|
15
16
|
import { resolveFileRefs } from './fileRefs.js';
|
|
16
|
-
import { listLiveBrowsers, findBrowser, describeBrowser } from './browserRegistry.js';
|
|
17
|
+
import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
|
|
17
18
|
import { resolveActionTimeoutSec } from './actionTimeouts.js';
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
19
20
|
export const CLIENT_VERSION = require('../package.json').version;
|
|
@@ -70,6 +71,42 @@ export function decorateBrowserListResult(result) {
|
|
|
70
71
|
defaultBrowserId: selected?.browserId ?? null,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
74
|
+
function browserListFromProviderRecords(browsers) {
|
|
75
|
+
const providerBrowsers = browsers
|
|
76
|
+
.filter(browser => browser.transport === 'provider')
|
|
77
|
+
.map(browser => ({
|
|
78
|
+
browserId: browser.browserId,
|
|
79
|
+
name: browser.browserName,
|
|
80
|
+
type: 'regular',
|
|
81
|
+
status: browser.status === 'pairing' ? 'pairing' : 'connected',
|
|
82
|
+
transport: 'provider',
|
|
83
|
+
providerId: browser.providerId,
|
|
84
|
+
providerName: browser.providerName,
|
|
85
|
+
providerVersion: browser.providerVersion,
|
|
86
|
+
providerProtocolVersion: browser.providerProtocolVersion,
|
|
87
|
+
pairing: browser.pairing,
|
|
88
|
+
}));
|
|
89
|
+
return {
|
|
90
|
+
count: providerBrowsers.length,
|
|
91
|
+
connectedCount: providerBrowsers.filter(browser => browser.status === 'connected').length,
|
|
92
|
+
browsers: providerBrowsers,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function mergeWarnings(result, warnings) {
|
|
96
|
+
const resultWarnings = Array.isArray(result?.warnings)
|
|
97
|
+
? result.warnings.filter((warning) => typeof warning === 'string')
|
|
98
|
+
: [];
|
|
99
|
+
const merged = [...new Set([...resultWarnings, ...warnings])];
|
|
100
|
+
return { ...result, ...(merged.length > 0 ? { warnings: merged } : {}) };
|
|
101
|
+
}
|
|
102
|
+
function supportsAction(browser, action) {
|
|
103
|
+
if (browser.transport === 'provider' &&
|
|
104
|
+
(action === 'get_versions' || action === 'browser_close')) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return (browser.transport !== 'provider' ||
|
|
108
|
+
browser.capabilities?.actions.some(capability => capability === action) === true);
|
|
109
|
+
}
|
|
73
110
|
function selectBrowserResult(data) {
|
|
74
111
|
const selector = typeof data.browser === 'string' ? data.browser.trim() : '';
|
|
75
112
|
if (!selector) {
|
|
@@ -80,7 +117,8 @@ function selectBrowserResult(data) {
|
|
|
80
117
|
};
|
|
81
118
|
}
|
|
82
119
|
const browsers = listLiveBrowsers();
|
|
83
|
-
const
|
|
120
|
+
const controllableBrowsers = browsers.filter(browser => browser.status !== 'pairing');
|
|
121
|
+
const match = findBrowser(controllableBrowsers, selector);
|
|
84
122
|
if (!match)
|
|
85
123
|
throw new Error(formatNoMatchError(selector, browsers));
|
|
86
124
|
setDefaultBrowser(match.browserId);
|
|
@@ -148,7 +186,11 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
148
186
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
149
187
|
const resolvedData = resolveFileRefs(data);
|
|
150
188
|
if (action === 'browser_list') {
|
|
151
|
-
|
|
189
|
+
const hasNativeAggregator = listLiveBrowsers().some(liveBrowser => liveBrowser.transport !== 'provider');
|
|
190
|
+
const providerStartup = hasNativeAggregator
|
|
191
|
+
? Promise.resolve([])
|
|
192
|
+
: ensureInstalledProvidersRunning();
|
|
193
|
+
return providerStartup.then(providerWarnings => dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(mergeWarnings(result, providerWarnings))));
|
|
152
194
|
}
|
|
153
195
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
154
196
|
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
@@ -176,14 +218,24 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
176
218
|
}
|
|
177
219
|
}
|
|
178
220
|
const browsers = listLiveBrowsers();
|
|
221
|
+
const controllableBrowsers = browsers.filter(browser => browser.status !== 'pairing');
|
|
179
222
|
// Explicit target: must match a live browser; do not silently fall back.
|
|
180
223
|
if (selector) {
|
|
181
224
|
const match = findBrowser(browsers, selector);
|
|
182
225
|
if (!match) {
|
|
183
226
|
throw new Error(formatNoMatchError(selector, browsers));
|
|
184
227
|
}
|
|
228
|
+
if (match.status === 'pairing' && action !== 'get_versions' && action !== 'browser_close') {
|
|
229
|
+
throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
|
|
230
|
+
}
|
|
231
|
+
if (transport === 'cdp' && match.transport === 'provider') {
|
|
232
|
+
throw new Error(`Browser ${describeBrowser(match)} does not support the CDP transport`);
|
|
233
|
+
}
|
|
234
|
+
if (!supportsAction(match, action)) {
|
|
235
|
+
throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
|
|
236
|
+
}
|
|
185
237
|
try {
|
|
186
|
-
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource);
|
|
238
|
+
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.providerBrowserId);
|
|
187
239
|
}
|
|
188
240
|
catch (error) {
|
|
189
241
|
if (isConnError(error) || isSocketSandboxError(error)) {
|
|
@@ -196,18 +248,25 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
196
248
|
}
|
|
197
249
|
// No explicit target: prefer the most-recently-focused live browser; on a
|
|
198
250
|
// stale socket, fall through to the legacy path + auto-spawn.
|
|
199
|
-
|
|
251
|
+
const routableBrowsers = controllableBrowsers.filter(browser => !(transport === 'cdp' && browser.transport === 'provider') && supportsAction(browser, action));
|
|
252
|
+
if (routableBrowsers.length > 0) {
|
|
253
|
+
const target = routableBrowsers[0];
|
|
200
254
|
try {
|
|
201
|
-
return await invokeOnce(action, data, timeoutSec, transport,
|
|
255
|
+
return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
|
|
202
256
|
}
|
|
203
257
|
catch (error) {
|
|
204
258
|
if (isSocketSandboxError(error)) {
|
|
205
|
-
throw new Error(formatConnectError(error,
|
|
259
|
+
throw new Error(formatConnectError(error, target.socketPath), {
|
|
260
|
+
cause: error,
|
|
261
|
+
});
|
|
206
262
|
}
|
|
207
263
|
if (!isConnError(error))
|
|
208
264
|
throw error;
|
|
209
265
|
}
|
|
210
266
|
}
|
|
267
|
+
if (action === 'browser_list' && browsers.some(browser => browser.transport === 'provider')) {
|
|
268
|
+
return browserListFromProviderRecords(browsers);
|
|
269
|
+
}
|
|
211
270
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
|
|
212
271
|
}
|
|
213
272
|
/** Legacy well-known socket, auto-spawning a native-host on ENOENT and retrying once. */
|
|
@@ -229,8 +288,11 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
229
288
|
/* isRetry */ true);
|
|
230
289
|
}
|
|
231
290
|
const browsers = listLiveBrowsers();
|
|
232
|
-
|
|
233
|
-
|
|
291
|
+
const target = browsers.find(browser => browser.status !== 'pairing' &&
|
|
292
|
+
!(transport === 'cdp' && browser.transport === 'provider') &&
|
|
293
|
+
supportsAction(browser, action));
|
|
294
|
+
if (target) {
|
|
295
|
+
return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
|
|
234
296
|
}
|
|
235
297
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
|
|
236
298
|
/* isRetry */ true);
|
|
@@ -241,7 +303,7 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
241
303
|
throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
|
|
242
304
|
}
|
|
243
305
|
}
|
|
244
|
-
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource) {
|
|
306
|
+
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerBrowserId) {
|
|
245
307
|
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
246
308
|
return new Promise((resolve, reject) => {
|
|
247
309
|
if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
|
|
@@ -282,6 +344,8 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSour
|
|
|
282
344
|
};
|
|
283
345
|
if (transport)
|
|
284
346
|
message.transport = transport;
|
|
347
|
+
if (providerBrowserId)
|
|
348
|
+
message.browserId = providerBrowserId;
|
|
285
349
|
socket.write(JSON.stringify(message) + '\n');
|
|
286
350
|
});
|
|
287
351
|
socket.on('data', chunk => {
|
package/dist/unified.js
CHANGED
|
@@ -302,6 +302,9 @@ export class UnifiedClient {
|
|
|
302
302
|
? result.browsers
|
|
303
303
|
: [];
|
|
304
304
|
const browsers = rawBrowsers.map(browser => decorateBrowser(browser, source));
|
|
305
|
+
const resultWarnings = Array.isArray(result?.warnings)
|
|
306
|
+
? result.warnings.filter((warning) => typeof warning === 'string')
|
|
307
|
+
: [];
|
|
305
308
|
return {
|
|
306
309
|
count: browsers.length,
|
|
307
310
|
connectedCount: browsers.filter(browser => browser.status === 'connected').length,
|
|
@@ -312,6 +315,7 @@ export class UnifiedClient {
|
|
|
312
315
|
})
|
|
313
316
|
: null,
|
|
314
317
|
browsers,
|
|
318
|
+
...(resultWarnings.length > 0 ? { warnings: resultWarnings } : {}),
|
|
315
319
|
};
|
|
316
320
|
}
|
|
317
321
|
applyConfiguredDefault(listing) {
|
|
@@ -374,6 +378,8 @@ export class UnifiedClient {
|
|
|
374
378
|
connectorName: connector.name,
|
|
375
379
|
}));
|
|
376
380
|
}
|
|
381
|
+
for (const listing of listings)
|
|
382
|
+
warnings.push(...(listing.warnings ?? []));
|
|
377
383
|
const browsers = listings.flatMap(listing => listing.browsers);
|
|
378
384
|
const defaults = listings
|
|
379
385
|
.map(listing => listing.defaultBrowserId)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Unified Mearl SDK & CLI for local and remote browsers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"./cli": {
|
|
30
30
|
"types": "./dist/cli.d.ts",
|
|
31
31
|
"import": "./dist/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"./plugin": {
|
|
34
|
+
"types": "./dist/plugin.d.ts",
|
|
35
|
+
"import": "./dist/plugin.js"
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
38
|
"bin": {
|
|
@@ -55,7 +59,8 @@
|
|
|
55
59
|
"commander": "^15.0.0",
|
|
56
60
|
"fastest-levenshtein": "^1.0.16",
|
|
57
61
|
"ws": "^8.18.0",
|
|
58
|
-
"@mearl/cloud-types": "2.
|
|
62
|
+
"@mearl/cloud-types": "2.13.0",
|
|
63
|
+
"@mearl/provider": "2.13.0"
|
|
59
64
|
},
|
|
60
65
|
"devDependencies": {
|
|
61
66
|
"@types/node": "^24.9.1",
|