@mearl/client 2.12.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 +48 -10
- package/dist/browserRegistry.d.ts +10 -1
- package/dist/check.d.ts +14 -1
- package/dist/check.js +13 -1
- package/dist/cli.js +60 -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 +2 -1
- package/dist/plugin.js +8 -2
- package/dist/pluginRegistry.d.ts +6 -1
- package/dist/pluginRegistry.js +12 -4
- package/dist/socket.js +74 -10
- package/dist/unified.js +6 -0
- package/package.json +3 -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,19 @@ 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` 仅用于输出机器可读的检查结果。
|
|
99
132
|
|
|
100
133
|
## Skill CLI 插件
|
|
101
134
|
|
|
@@ -111,7 +144,8 @@ mearl yuque --help
|
|
|
111
144
|
mearl yuque --version
|
|
112
145
|
```
|
|
113
146
|
|
|
114
|
-
目录默认使用 `scripts/main.mjs`。入口默认导出 `register(app)
|
|
147
|
+
目录默认使用 `scripts/main.mjs`。入口默认导出 `register(app)`,可导出简短的 `description`
|
|
148
|
+
显示在 `mearl` 插件列表中,并通过 `app.command(signature, handler, description?)`
|
|
115
149
|
注册动作、`app.invoke(action, payload, options?)` 复用当前浏览器上下文,并用 `app.progress(message)` 报告可选进度。CLI 默认只输出最终结果,传入
|
|
116
150
|
`--verbose` 时才把进度写入 stderr。handler 返回文本或 JSON,宿主统一处理 `--output`、连接和错误。写命令在用户调用后直接执行,业务模块负责目标、状态、幂等和结果校验。
|
|
117
151
|
自动注册默认以 skill 安装目录名作为命令名,也可在入口导出 `name` 指定短名称。
|
|
@@ -124,7 +158,7 @@ setup 会补齐缺失或低于自身版本的 client、native-host;安装参
|
|
|
124
158
|
插件接受位置参数或包含 command/input 的 `--payload`,两种形式不能混用。JSON 保留业务字段类型;正文等文件输入
|
|
125
159
|
按具体 skill 的字段约定处理。插件不使用上表中内置 action 的 `--payload-file` 入口。
|
|
126
160
|
|
|
127
|
-
注册记录在 `~/.mearl/plugins/<name>.json
|
|
161
|
+
注册记录在 `~/.mearl/plugins/<name>.json`,保存入口路径和可选的简短描述;skill 业务代码原地更新直接生效,换路径或更新描述后重新注册。
|
|
128
162
|
`plugin uninstall <name>` 会对 `~/.agents/skills` 中的全局 Skill 调用 `ali-skills remove --global --yes`,
|
|
129
163
|
成功后清除相关注册;即使入口目录已被手动删除,也会完成 Ali Skills 跟踪记录的清理。
|
|
130
164
|
手工路径只解除注册并保留源码。
|
|
@@ -155,7 +189,9 @@ setup 会补齐缺失或低于自身版本的 client、native-host;安装参
|
|
|
155
189
|
|
|
156
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"` 自动解析最近可滚动祖先。
|
|
157
191
|
|
|
158
|
-
`browser_list`
|
|
192
|
+
`browser_list` 统一列出普通浏览器、Provider 浏览器和托管浏览器。`type` 区分
|
|
193
|
+
`regular` / `managed`,`status` 区分 `connected` / `pairing` / `running_disconnected` /
|
|
194
|
+
`stopped`;只有 `connected` 的浏览器可作为常规操作目标。
|
|
159
195
|
|
|
160
196
|
按 `sessionId` 接入的 AgentBay 浏览器用 `browser_release` 收尾:它会移除本机 attachment 并退出对应宿主,但不会关闭云端 Session。其他浏览器仅在用户明确要求结束调试、退出接管或清理会话资源时调用;它会释放 debugger/CDP 控制和会话级临时状态(包括设备/时区模拟),保留浏览器及全部标签页,后续浏览器操作可自动重新建立控制。
|
|
161
197
|
|
|
@@ -166,8 +202,10 @@ setup 会补齐缺失或低于自身版本的 client、native-host;安装参
|
|
|
166
202
|
```
|
|
167
203
|
@mearl/client (CLI / SDK)
|
|
168
204
|
├─ 本机 → @mearl/native-host → Chrome Extension / CDP
|
|
205
|
+
├─ 本机 → @mearl/provider runtime → External Provider
|
|
169
206
|
└─ WebSocket → @mearl/cloud-server → @mearl/cloud-connector
|
|
170
|
-
|
|
207
|
+
├─ @mearl/native-host → Chrome Extension / CDP
|
|
208
|
+
└─ @mearl/provider runtime → External Provider
|
|
171
209
|
```
|
|
172
210
|
|
|
173
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.js
CHANGED
|
@@ -1,14 +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';
|
|
10
|
-
import { findPluginEntry, listPluginNames, loadPlugin, PluginError } from './plugin.js';
|
|
11
|
+
import { findPluginEntry, findPluginRegistration, listPluginNames, loadPlugin, PluginError, } from './plugin.js';
|
|
11
12
|
import { getCloudServerConfigPath, loadCloudServerConfig } from './unifiedConfig.js';
|
|
13
|
+
const TOP_LEVEL_COMMAND_NAME_WIDTH = 22;
|
|
12
14
|
const CATEGORIES = [
|
|
13
15
|
{
|
|
14
16
|
title: 'API 调试',
|
|
@@ -74,24 +76,20 @@ function printUsage(commandName, version) {
|
|
|
74
76
|
' browser_list 默认聚合当前机器及已配置 cloud-server 下的浏览器。',
|
|
75
77
|
' 后续操作通过 --browser <browserId> 指定目标,客户端会自动选择本地或远端传输。',
|
|
76
78
|
'',
|
|
77
|
-
'
|
|
79
|
+
'常用选项:',
|
|
78
80
|
' --payload <json> 直接传入 JSON 参数',
|
|
79
81
|
' --payload-file <path> 从文件读取整个 JSON 参数',
|
|
80
82
|
' --timeout <seconds> 超时时间(默认按 action 推导)',
|
|
81
83
|
' --compact stdout 输出紧凑格式 JSON',
|
|
82
84
|
' --output <path> 保存输出或截图',
|
|
83
85
|
' --browser <id|名称> 指定 browser_list 返回的浏览器',
|
|
84
|
-
' --connector <id|名称> 限制到一台远端 connector',
|
|
85
|
-
' --local 只使用当前机器上的浏览器',
|
|
86
|
-
' --cdp 当前机器强制使用 CDP(隐含 --local)',
|
|
87
|
-
' --server <url> 覆盖 cloud-server WebSocket URL',
|
|
88
86
|
'',
|
|
89
87
|
'其他选项:',
|
|
90
88
|
' -h, --help 查看帮助',
|
|
91
89
|
' -v, --version 查看版本',
|
|
92
90
|
'',
|
|
93
91
|
'内置命令:',
|
|
94
|
-
'
|
|
92
|
+
` ${'check'.padEnd(TOP_LEVEL_COMMAND_NAME_WIDTH)}检查指定浏览器,未指定时检查全部已连接浏览器`,
|
|
95
93
|
];
|
|
96
94
|
for (const category of CATEGORIES) {
|
|
97
95
|
lines.push('', ` ${category.title}`);
|
|
@@ -103,12 +101,28 @@ function printUsage(commandName, version) {
|
|
|
103
101
|
}
|
|
104
102
|
try {
|
|
105
103
|
const plugins = listPluginNames();
|
|
106
|
-
if (plugins.length)
|
|
107
|
-
|
|
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
|
+
}
|
|
108
118
|
}
|
|
109
119
|
catch (error) {
|
|
110
120
|
lines.push('', error instanceof Error ? error.message : String(error));
|
|
111
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
|
+
}
|
|
112
126
|
lines.push('', `运行 ${commandName} <action> --help 查看 action 参数。`);
|
|
113
127
|
console.error(lines.join('\n'));
|
|
114
128
|
}
|
|
@@ -149,7 +163,7 @@ function printCommandHelp(commandName, command) {
|
|
|
149
163
|
` ${command.description}`,
|
|
150
164
|
'',
|
|
151
165
|
'用法:',
|
|
152
|
-
` ${commandName} ${command.name}${payloadUsage} [
|
|
166
|
+
` ${commandName} ${command.name}${payloadUsage} [常用选项]`,
|
|
153
167
|
'',
|
|
154
168
|
];
|
|
155
169
|
if (command.params?.length) {
|
|
@@ -160,12 +174,18 @@ function printCommandHelp(commandName, command) {
|
|
|
160
174
|
}
|
|
161
175
|
lines.push('');
|
|
162
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
|
+
}
|
|
163
183
|
if (command.examples?.length) {
|
|
164
184
|
lines.push('示例:');
|
|
165
185
|
for (const example of command.examples)
|
|
166
186
|
lines.push(` ${commandName} ${example}`);
|
|
167
187
|
}
|
|
168
|
-
lines.push('', `运行 ${commandName} --help
|
|
188
|
+
lines.push('', `运行 ${commandName} --help 查看常用选项。`);
|
|
169
189
|
console.error(lines.join('\n'));
|
|
170
190
|
}
|
|
171
191
|
function printUnknownCommand(commandName, args) {
|
|
@@ -222,6 +242,31 @@ function resolveServer(explicit) {
|
|
|
222
242
|
? { serverUrl: config.server, serverUrlSource: getCloudServerConfigPath() }
|
|
223
243
|
: {};
|
|
224
244
|
}
|
|
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
|
+
}
|
|
225
270
|
export function parseClientArgs(args, plugin = false) {
|
|
226
271
|
const program = new Command()
|
|
227
272
|
.name('mearl')
|
|
@@ -264,7 +309,8 @@ export function parseClientArgs(args, plugin = false) {
|
|
|
264
309
|
const json = options.json === true;
|
|
265
310
|
if (json && action !== 'check')
|
|
266
311
|
throw new Error('--json 仅适用于 check');
|
|
267
|
-
const
|
|
312
|
+
const parsedPayload = parsePayload(options.payload, options.payloadFile);
|
|
313
|
+
const { payload, outputPath } = normalizeScreenshotOutput(action, parsedPayload, options.output);
|
|
268
314
|
const fallbackTimeout = resolveActionTimeoutSec(action, payload, 60);
|
|
269
315
|
const forceCdp = options.cdp === true;
|
|
270
316
|
const local = forceCdp || options.local === true;
|
|
@@ -280,7 +326,7 @@ export function parseClientArgs(args, plugin = false) {
|
|
|
280
326
|
forceCdp,
|
|
281
327
|
json,
|
|
282
328
|
local,
|
|
283
|
-
...(
|
|
329
|
+
...(outputPath ? { outputPath } : {}),
|
|
284
330
|
...(options.browser ? { browser: options.browser } : {}),
|
|
285
331
|
...(connector ? { connector } : {}),
|
|
286
332
|
...resolveServer(options.server),
|
|
@@ -488,7 +534,7 @@ export async function runMearlCli(args, options = {}) {
|
|
|
488
534
|
else {
|
|
489
535
|
process.stdout.write(`${parsed.compact ? JSON.stringify(result) : JSON.stringify(result, null, 2)}\n`);
|
|
490
536
|
}
|
|
491
|
-
return
|
|
537
|
+
return actionInvocationFailed(parsed.action, result) ? 1 : 0;
|
|
492
538
|
}
|
|
493
539
|
catch (error) {
|
|
494
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UnifiedInvokeOptions } from './unified.js';
|
|
2
|
-
export { assertPluginName, findPluginEntry, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
2
|
+
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, type PluginRegistration, } from './pluginRegistry.js';
|
|
3
3
|
export type PluginOptions = UnifiedInvokeOptions;
|
|
4
4
|
export interface PluginApp {
|
|
5
5
|
readonly options: Readonly<PluginOptions>;
|
|
@@ -11,6 +11,7 @@ export type PluginHandler = (input: Record<string, any>) => unknown | Promise<un
|
|
|
11
11
|
/** 注册和执行共享一个 app;仅 handler 执行期间开放浏览器调用。 */
|
|
12
12
|
export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'invoke' | 'options' | 'progress'>, pluginName?: string): Promise<{
|
|
13
13
|
name: string | undefined;
|
|
14
|
+
description: string | undefined;
|
|
14
15
|
version: string | undefined;
|
|
15
16
|
help(namespace: string, name?: string): string;
|
|
16
17
|
run(positionals: string[], payload?: unknown): Promise<unknown>;
|
package/dist/plugin.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { PluginError } from './pluginRegistry.js';
|
|
6
|
-
export { assertPluginName, findPluginEntry, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
6
|
+
export { assertPluginName, findPluginEntry, findPluginRegistration, listPluginNames, registerPlugin, resolvePluginEntry, unregisterPlugin, PluginError, } from './pluginRegistry.js';
|
|
7
7
|
function readPluginVersion(entry) {
|
|
8
8
|
try {
|
|
9
9
|
const content = readFileSync(resolve(dirname(entry), '..', 'SKILL.md'), 'utf8');
|
|
@@ -42,6 +42,7 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
42
42
|
let executing = false;
|
|
43
43
|
let registering = true;
|
|
44
44
|
let name;
|
|
45
|
+
let description;
|
|
45
46
|
const app = Object.freeze({
|
|
46
47
|
options: Object.freeze({ ...context.options }),
|
|
47
48
|
command(signature, handler, description = '') {
|
|
@@ -81,6 +82,9 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
81
82
|
if (module.name !== undefined && typeof module.name !== 'string')
|
|
82
83
|
throw new Error('入口的 name 导出必须为字符串');
|
|
83
84
|
name = module.name;
|
|
85
|
+
if (module.description !== undefined && typeof module.description !== 'string')
|
|
86
|
+
throw new Error('入口的 description 导出必须为字符串');
|
|
87
|
+
description = module.description;
|
|
84
88
|
if (typeof module.default !== 'function')
|
|
85
89
|
throw new Error('入口必须默认导出 register(app) 函数');
|
|
86
90
|
await module.default(app);
|
|
@@ -101,6 +105,7 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
101
105
|
};
|
|
102
106
|
return {
|
|
103
107
|
name,
|
|
108
|
+
description,
|
|
104
109
|
version: readPluginVersion(entry),
|
|
105
110
|
help(namespace, name) {
|
|
106
111
|
const selected = name ? [find(name)] : [...commands.values()];
|
|
@@ -114,7 +119,8 @@ export async function loadPlugin(entry, context, pluginName) {
|
|
|
114
119
|
'',
|
|
115
120
|
` ${namespace} --payload '${JSON.stringify({ command: example.name(), input })}'`,
|
|
116
121
|
'',
|
|
117
|
-
'
|
|
122
|
+
'常用选项: --browser --timeout --compact --output',
|
|
123
|
+
'连接或传输排障运行 mearl check --help。',
|
|
118
124
|
'插件选项: --verbose; -h, --help; -v, --version',
|
|
119
125
|
'JSON input 直接传给 handler;业务字段与校验规则见对应 skill。',
|
|
120
126
|
].join('\n');
|
package/dist/pluginRegistry.d.ts
CHANGED
|
@@ -2,10 +2,15 @@ export declare class PluginError extends Error {
|
|
|
2
2
|
readonly code: string;
|
|
3
3
|
constructor(code: string, message: string);
|
|
4
4
|
}
|
|
5
|
+
export interface PluginRegistration {
|
|
6
|
+
entry: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
5
9
|
export declare function pluginDirectory(): string;
|
|
6
10
|
export declare function assertPluginName(name: string): void;
|
|
7
11
|
export declare function resolvePluginEntry(target: string): string;
|
|
8
|
-
export declare function registerPlugin(name: string, target: string): string;
|
|
12
|
+
export declare function registerPlugin(name: string, target: string, description?: string): string;
|
|
9
13
|
export declare function unregisterPlugin(name: string): boolean;
|
|
14
|
+
export declare function findPluginRegistration(name: string): PluginRegistration | undefined;
|
|
10
15
|
export declare function findPluginEntry(name: string): string | undefined;
|
|
11
16
|
export declare function listPluginNames(): string[];
|
package/dist/pluginRegistry.js
CHANGED
|
@@ -40,7 +40,7 @@ export function resolvePluginEntry(target) {
|
|
|
40
40
|
throw new PluginError('PLUGIN_ENTRY_MISSING', `入口不存在: ${absolute}(目录默认使用 scripts/main.mjs)`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
export function registerPlugin(name, target) {
|
|
43
|
+
export function registerPlugin(name, target, description) {
|
|
44
44
|
assertPluginName(name);
|
|
45
45
|
const entry = resolvePluginEntry(target);
|
|
46
46
|
const directory = pluginDirectory();
|
|
@@ -48,8 +48,10 @@ export function registerPlugin(name, target) {
|
|
|
48
48
|
const file = join(directory, `${name}.json`);
|
|
49
49
|
// 每个插件独立保存,多个名称并发注册不会覆盖彼此;同名注册原子替换绑定。
|
|
50
50
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
51
|
+
const summary = description?.trim();
|
|
52
|
+
const record = { entry, ...(summary ? { description: summary } : {}) };
|
|
51
53
|
try {
|
|
52
|
-
writeFileSync(temporary, `${JSON.stringify(
|
|
54
|
+
writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
|
|
53
55
|
renameSync(temporary, file);
|
|
54
56
|
}
|
|
55
57
|
finally {
|
|
@@ -70,7 +72,7 @@ export function unregisterPlugin(name) {
|
|
|
70
72
|
throw new PluginError('PLUGIN_UNINSTALL_FAILED', `无法移除插件注册: ${file}`);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
|
-
export function
|
|
75
|
+
export function findPluginRegistration(name) {
|
|
74
76
|
if (!/^[a-z][a-z0-9-]*$/.test(name))
|
|
75
77
|
return undefined;
|
|
76
78
|
const file = join(pluginDirectory(), `${name}.json`);
|
|
@@ -87,12 +89,18 @@ export function findPluginEntry(name) {
|
|
|
87
89
|
const record = JSON.parse(content);
|
|
88
90
|
if (typeof record?.entry !== 'string' || !isAbsolute(record.entry))
|
|
89
91
|
throw new Error('invalid entry');
|
|
90
|
-
|
|
92
|
+
const description = typeof record.description === 'string' && record.description.trim()
|
|
93
|
+
? record.description.trim()
|
|
94
|
+
: undefined;
|
|
95
|
+
return { entry: record.entry, ...(description ? { description } : {}) };
|
|
91
96
|
}
|
|
92
97
|
catch {
|
|
93
98
|
throw new PluginError('PLUGIN_REGISTRY_INVALID', `无效的插件记录: ${file}`);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
101
|
+
export function findPluginEntry(name) {
|
|
102
|
+
return findPluginRegistration(name)?.entry;
|
|
103
|
+
}
|
|
96
104
|
export function listPluginNames() {
|
|
97
105
|
try {
|
|
98
106
|
return readdirSync(pluginDirectory())
|
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",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"commander": "^15.0.0",
|
|
60
60
|
"fastest-levenshtein": "^1.0.16",
|
|
61
61
|
"ws": "^8.18.0",
|
|
62
|
-
"@mearl/cloud-types": "2.
|
|
62
|
+
"@mearl/cloud-types": "2.13.0",
|
|
63
|
+
"@mearl/provider": "2.13.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/node": "^24.9.1",
|