@mearl/client 2.12.0 → 2.14.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 +12 -1
- package/dist/check.d.ts +17 -2
- package/dist/check.js +21 -5
- 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 +128 -31
- 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 +89 -10
- package/dist/unified.js +7 -11
- 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, BrowserDetails, BrowserPairingInfo, BrowserProviderIdentity, BrowserProviderOperation, BrowserReference, BrowserTransport } from './generated-browser-action-protocol.js';
|
|
11
12
|
export interface LiveBrowser {
|
|
12
13
|
pid: number;
|
|
13
14
|
browserId: string;
|
|
@@ -15,7 +16,17 @@ export interface LiveBrowser {
|
|
|
15
16
|
browserName: string;
|
|
16
17
|
extensionVersion?: string;
|
|
17
18
|
nativeHostVersion?: string;
|
|
18
|
-
transport?:
|
|
19
|
+
transport?: BrowserTransport;
|
|
20
|
+
providerBrowserId?: string;
|
|
21
|
+
provider?: BrowserProviderIdentity;
|
|
22
|
+
capabilities?: BrowserCapabilities;
|
|
23
|
+
status?: 'connected' | 'pairing' | 'running_disconnected' | 'stopped';
|
|
24
|
+
ownership?: 'owned' | 'attached';
|
|
25
|
+
persistence?: 'ephemeral' | 'persistent';
|
|
26
|
+
operations?: BrowserProviderOperation[];
|
|
27
|
+
pairing?: BrowserPairingInfo;
|
|
28
|
+
references?: BrowserReference[];
|
|
29
|
+
details?: BrowserDetails;
|
|
19
30
|
lastFocusedAt?: number;
|
|
20
31
|
updatedAt?: number;
|
|
21
32
|
}
|
package/dist/check.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { CloudConnectorInfo } from '@mearl/cloud-types';
|
|
2
|
+
import type { BrowserCapabilities, BrowserProviderIdentity, BrowserProviderOperation } 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
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
builtin: boolean;
|
|
14
|
+
version: string;
|
|
15
|
+
protocolVersion: number;
|
|
7
16
|
}
|
|
8
17
|
export type BrowserAccessCapability = {
|
|
9
18
|
kind: 'agentbay';
|
|
@@ -11,6 +20,8 @@ export type BrowserAccessCapability = {
|
|
|
11
20
|
kind: 'sandbox-novnc';
|
|
12
21
|
environment: 'opensandbox' | 'aone';
|
|
13
22
|
port: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: string;
|
|
14
25
|
};
|
|
15
26
|
export type BrowserAccess = BrowserAccessCapability & {
|
|
16
27
|
url: string;
|
|
@@ -24,8 +35,11 @@ interface BrowserInfo {
|
|
|
24
35
|
originalName?: string;
|
|
25
36
|
status: string;
|
|
26
37
|
type?: string;
|
|
27
|
-
provider?:
|
|
28
|
-
|
|
38
|
+
provider?: BrowserProviderIdentity;
|
|
39
|
+
ownership?: 'owned' | 'attached';
|
|
40
|
+
persistence?: 'ephemeral' | 'persistent';
|
|
41
|
+
operations?: BrowserProviderOperation[];
|
|
42
|
+
transport?: 'extension' | 'cdp' | 'provider';
|
|
29
43
|
}
|
|
30
44
|
interface CheckItem {
|
|
31
45
|
status: 'ok' | 'warning' | 'error';
|
|
@@ -38,6 +52,7 @@ export interface ExtensionInstallationInfo {
|
|
|
38
52
|
}
|
|
39
53
|
export interface UnifiedCheckBrowserResult extends BrowserInfo {
|
|
40
54
|
browserAccess?: BrowserAccess;
|
|
55
|
+
capabilities?: BrowserCapabilities;
|
|
41
56
|
check?: {
|
|
42
57
|
errors?: string[];
|
|
43
58
|
label: string;
|
package/dist/check.js
CHANGED
|
@@ -5,13 +5,16 @@ function formatError(error) {
|
|
|
5
5
|
function browserAccessLabel(access) {
|
|
6
6
|
if (access.kind === 'agentbay')
|
|
7
7
|
return 'AgentBay, temporary';
|
|
8
|
-
|
|
8
|
+
if (access.kind === 'sandbox-novnc' && 'environment' in access) {
|
|
9
|
+
return access.environment === 'opensandbox' ? 'OpenSandbox noVNC' : 'AONE Sandbox noVNC';
|
|
10
|
+
}
|
|
11
|
+
return access.kind;
|
|
9
12
|
}
|
|
10
13
|
async function getBrowserAccess(client, browser, capability, timeoutSec) {
|
|
11
14
|
const result = (await client.invoke('get_browser_access_url', {}, { timeoutSec, browser: browser.browserId }));
|
|
12
15
|
if (!result.browserAccess?.url)
|
|
13
16
|
throw new Error('Browser host did not return an access URL');
|
|
14
|
-
if (result.browserAccess.kind !== capability.kind) {
|
|
17
|
+
if (capability && result.browserAccess.kind !== capability.kind) {
|
|
15
18
|
throw new Error('Browser host returned an unexpected access URL type');
|
|
16
19
|
}
|
|
17
20
|
return result.browserAccess;
|
|
@@ -56,7 +59,9 @@ function formatBrowser(browser, isDefault) {
|
|
|
56
59
|
? ' [CDP]'
|
|
57
60
|
: browser.transport === 'extension'
|
|
58
61
|
? ' [Extension]'
|
|
59
|
-
: ''
|
|
62
|
+
: browser.transport === 'provider'
|
|
63
|
+
? ` [Provider: ${browser.provider?.name || browser.provider?.id || 'external'}]`
|
|
64
|
+
: '';
|
|
60
65
|
const type = browser.type === 'managed' ? ' [Managed]' : '';
|
|
61
66
|
return ` ${isDefault ? '★' : ' '} ${browser.name} [${shortBrowserId(browser.browserId)}]${type}${transport}`;
|
|
62
67
|
}
|
|
@@ -220,12 +225,20 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
220
225
|
...(versions.nativeHost ? { nativeHost: versions.nativeHost } : {}),
|
|
221
226
|
...(versions.extension !== undefined ? { extension: versions.extension } : {}),
|
|
222
227
|
...(versions.browser !== undefined ? { browser: versions.browser } : {}),
|
|
228
|
+
...(versions.provider ? { provider: versions.provider } : {}),
|
|
223
229
|
};
|
|
224
230
|
const details = [
|
|
225
231
|
versions.nativeHost ? `native-host v${versions.nativeHost}` : null,
|
|
226
232
|
versions.extension ? `extension v${versions.extension}` : null,
|
|
227
233
|
versions.browser ? `Chrome ${versions.browser}` : null,
|
|
234
|
+
versions.provider
|
|
235
|
+
? `${versions.provider.name} v${versions.provider.version} (protocol v${versions.provider.protocolVersion})`
|
|
236
|
+
: null,
|
|
228
237
|
].filter((value) => value !== null);
|
|
238
|
+
const capabilities = options.browser ? versions.capabilities : undefined;
|
|
239
|
+
if (capabilities) {
|
|
240
|
+
details.push(`capabilities (${capabilities.actions.length}): ${capabilities.actions.join(', ') || 'none'}`, `screenshot: ${capabilities.screenshot}`);
|
|
241
|
+
}
|
|
229
242
|
let extensionInstallation;
|
|
230
243
|
if (options.local &&
|
|
231
244
|
browser.transport === 'extension' &&
|
|
@@ -240,9 +253,10 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
240
253
|
let browserAccess;
|
|
241
254
|
let accessUrlWarning;
|
|
242
255
|
const browserAccessCapability = versions.browserAccess;
|
|
243
|
-
|
|
256
|
+
const providerAccess = browser.operations?.includes('access') === true;
|
|
257
|
+
if ((providerAccess || browserAccessCapability) && options.browser) {
|
|
244
258
|
try {
|
|
245
|
-
browserAccess = await getBrowserAccess(client, browser, browserAccessCapability, timeoutSec);
|
|
259
|
+
browserAccess = await getBrowserAccess(client, browser, providerAccess ? undefined : browserAccessCapability, timeoutSec);
|
|
246
260
|
details.push(`Browser access URL (${browserAccessLabel(browserAccess)}; sensitive): ${browserAccess.url}`);
|
|
247
261
|
}
|
|
248
262
|
catch (error) {
|
|
@@ -253,6 +267,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
253
267
|
browserChecks.push({
|
|
254
268
|
...(browserAccess ? { browserAccess } : {}),
|
|
255
269
|
browser,
|
|
270
|
+
...(capabilities ? { capabilities } : {}),
|
|
256
271
|
...(extensionInstallation ? { extensionInstallation } : {}),
|
|
257
272
|
item: {
|
|
258
273
|
status: accessUrlWarning ? 'warning' : 'ok',
|
|
@@ -302,6 +317,7 @@ export async function runUnifiedCheck(options, dependencies = {}) {
|
|
|
302
317
|
...browserIdentity,
|
|
303
318
|
selected: targets.some(target => target.browserId === browser.browserId),
|
|
304
319
|
...(check?.browserAccess ? { browserAccess: check.browserAccess } : {}),
|
|
320
|
+
...(check?.capabilities ? { capabilities: check.capabilities } : {}),
|
|
305
321
|
...(check?.item
|
|
306
322
|
? {
|
|
307
323
|
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, JSON value>";
|
|
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, JSON value>',
|
|
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,12 +123,52 @@ 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;
|
|
130
138
|
connectorName?: string;
|
|
131
139
|
}
|
|
140
|
+
export type BrowserJsonPrimitive = string | number | boolean | null;
|
|
141
|
+
export type BrowserJsonValue = BrowserJsonPrimitive | BrowserJsonValue[] | {
|
|
142
|
+
[key: string]: BrowserJsonValue;
|
|
143
|
+
};
|
|
144
|
+
export type BrowserProviderOperation = 'start' | 'close' | 'detach' | 'deleteData' | 'access' | 'syncCookies';
|
|
145
|
+
export interface BrowserProviderIdentity {
|
|
146
|
+
id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
builtin: boolean;
|
|
149
|
+
version?: string;
|
|
150
|
+
protocolVersion?: number;
|
|
151
|
+
}
|
|
152
|
+
/** Common safe metadata; additional Provider-specific keys must also be non-secret JSON. */
|
|
153
|
+
export type BrowserDetails = Record<string, BrowserJsonValue> & {
|
|
154
|
+
headless?: boolean;
|
|
155
|
+
createdAt?: string;
|
|
156
|
+
cdpPort?: number;
|
|
157
|
+
account?: {
|
|
158
|
+
nickname: string;
|
|
159
|
+
accountId?: string;
|
|
160
|
+
loginId?: string;
|
|
161
|
+
};
|
|
162
|
+
copiedCookies?: {
|
|
163
|
+
domains: string[];
|
|
164
|
+
count: number;
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
export interface BrowserReference {
|
|
168
|
+
kind: string;
|
|
169
|
+
id: string;
|
|
170
|
+
label?: string;
|
|
171
|
+
}
|
|
132
172
|
export interface BrowserInfoBase {
|
|
133
173
|
browserId: string;
|
|
134
174
|
name: string;
|
|
@@ -139,38 +179,37 @@ export interface BrowserInfoBase {
|
|
|
139
179
|
source?: BrowserSource;
|
|
140
180
|
extensionVersion?: string;
|
|
141
181
|
nativeHostVersion?: string;
|
|
142
|
-
transport?:
|
|
182
|
+
transport?: BrowserTransport;
|
|
183
|
+
/** Provider identity, independent from the browser control transport. */
|
|
184
|
+
provider?: BrowserProviderIdentity;
|
|
185
|
+
ownership?: 'owned' | 'attached';
|
|
186
|
+
persistence?: 'ephemeral' | 'persistent';
|
|
187
|
+
operations?: BrowserProviderOperation[];
|
|
188
|
+
references?: BrowserReference[];
|
|
189
|
+
/** Safe, non-secret metadata suitable for display and remote connectors. */
|
|
190
|
+
details?: BrowserDetails;
|
|
191
|
+
/** Setup information exposed by a provider while it waits for an external device. */
|
|
192
|
+
pairing?: BrowserPairingInfo;
|
|
193
|
+
}
|
|
194
|
+
export type BrowserScreenshotKind = 'native' | 'reconstructed' | 'none';
|
|
195
|
+
/** Browser-targeted actions that an external provider may implement. */
|
|
196
|
+
export type BrowserCapabilityAction = ExtensionBrowserAction;
|
|
197
|
+
/** Capabilities advertised by an external browser-control provider. */
|
|
198
|
+
export interface BrowserCapabilities {
|
|
199
|
+
actions: BrowserCapabilityAction[];
|
|
200
|
+
/** Pixel fidelity of page_screenshot, or none when that action is unavailable. */
|
|
201
|
+
screenshot: BrowserScreenshotKind;
|
|
143
202
|
}
|
|
144
203
|
export interface RegularBrowserInfo extends BrowserInfoBase {
|
|
145
204
|
type: 'regular';
|
|
146
|
-
status: 'connected';
|
|
147
|
-
}
|
|
148
|
-
export interface ManagedBrowserAccount {
|
|
149
|
-
accountId?: string;
|
|
150
|
-
loginId?: string;
|
|
151
|
-
nickname: string;
|
|
205
|
+
status: 'connected' | 'pairing';
|
|
152
206
|
}
|
|
153
207
|
export interface ManagedBrowserInfo extends BrowserInfoBase {
|
|
154
208
|
type: 'managed';
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
agentbaySessionId?: string;
|
|
160
|
-
/** Whether this host owns the AgentBay session lifecycle or only attaches to it. */
|
|
161
|
-
agentbaySessionMode?: 'owned' | 'attached';
|
|
162
|
-
/** AgentBay image used to create the session. */
|
|
163
|
-
agentbayImageId?: string;
|
|
164
|
-
/** Persistent AgentBay browser context ID when enabled for this instance. */
|
|
165
|
-
agentbayContextId?: string;
|
|
166
|
-
/** Stable AgentBay browser context name derived from the managed browser name. */
|
|
167
|
-
agentbayContextName?: string;
|
|
168
|
-
createdAt: string;
|
|
169
|
-
account?: ManagedBrowserAccount;
|
|
170
|
-
copiedCookies?: {
|
|
171
|
-
domains: string[];
|
|
172
|
-
count: number;
|
|
173
|
-
};
|
|
209
|
+
status: 'connected' | 'pairing' | 'running_disconnected' | 'stopped';
|
|
210
|
+
provider: BrowserProviderIdentity;
|
|
211
|
+
persistence: 'ephemeral' | 'persistent';
|
|
212
|
+
operations: BrowserProviderOperation[];
|
|
174
213
|
}
|
|
175
214
|
export type BrowserInfo = RegularBrowserInfo | ManagedBrowserInfo;
|
|
176
215
|
export interface BrowserListResult {
|
|
@@ -199,7 +238,7 @@ export interface BrowserReleaseFailure {
|
|
|
199
238
|
export interface BrowserReleaseResult {
|
|
200
239
|
released: boolean;
|
|
201
240
|
hadActiveControl: boolean;
|
|
202
|
-
transports:
|
|
241
|
+
transports: BrowserTransport[];
|
|
203
242
|
affectedTabIds: number[];
|
|
204
243
|
/** True when browser_release also removed a sessionId-based local AgentBay attachment. */
|
|
205
244
|
detached?: boolean;
|
|
@@ -217,8 +256,10 @@ export interface CookieCopyScope {
|
|
|
217
256
|
export interface BrowserLaunchParams {
|
|
218
257
|
/** Required when creating a browser; optional when attaching by AgentBay sessionId. */
|
|
219
258
|
name?: string;
|
|
220
|
-
/**
|
|
221
|
-
provider?:
|
|
259
|
+
/** Built-in runtime or the id of a registered external Provider. */
|
|
260
|
+
provider?: string;
|
|
261
|
+
/** Structured launch parameters declared by the selected Provider. Cannot be mixed with flat options. */
|
|
262
|
+
providerOptions?: Record<string, BrowserJsonValue>;
|
|
222
263
|
/** Attach to an exact live AgentBay session without creating or owning it. */
|
|
223
264
|
sessionId?: string;
|
|
224
265
|
/** AgentBay running-session reuse policy. Defaults to never. */
|
|
@@ -244,6 +285,61 @@ export interface BrowserCloseParams {
|
|
|
244
285
|
browser: string;
|
|
245
286
|
deleteProfile?: boolean;
|
|
246
287
|
}
|
|
288
|
+
interface BrowserProviderSchemaBase {
|
|
289
|
+
title?: string;
|
|
290
|
+
description?: string;
|
|
291
|
+
default?: BrowserJsonValue;
|
|
292
|
+
}
|
|
293
|
+
export type BrowserProviderInputSchema = (BrowserProviderSchemaBase & {
|
|
294
|
+
type: 'string';
|
|
295
|
+
secret?: boolean;
|
|
296
|
+
enum?: readonly string[];
|
|
297
|
+
minLength?: number;
|
|
298
|
+
maxLength?: number;
|
|
299
|
+
pattern?: string;
|
|
300
|
+
placeholder?: string;
|
|
301
|
+
}) | (BrowserProviderSchemaBase & {
|
|
302
|
+
type: 'number';
|
|
303
|
+
integer?: boolean;
|
|
304
|
+
minimum?: number;
|
|
305
|
+
maximum?: number;
|
|
306
|
+
}) | (BrowserProviderSchemaBase & {
|
|
307
|
+
type: 'boolean';
|
|
308
|
+
}) | (BrowserProviderSchemaBase & {
|
|
309
|
+
type: 'array';
|
|
310
|
+
items: BrowserProviderInputSchema;
|
|
311
|
+
minItems?: number;
|
|
312
|
+
maxItems?: number;
|
|
313
|
+
}) | BrowserProviderObjectSchema;
|
|
314
|
+
export interface BrowserProviderObjectSchema extends BrowserProviderSchemaBase {
|
|
315
|
+
type: 'object';
|
|
316
|
+
properties: Record<string, BrowserProviderInputSchema>;
|
|
317
|
+
required?: readonly string[];
|
|
318
|
+
oneOf?: readonly BrowserProviderObjectSchema[];
|
|
319
|
+
not?: BrowserProviderObjectSchema;
|
|
320
|
+
additionalProperties?: boolean;
|
|
321
|
+
}
|
|
322
|
+
export interface BrowserProviderDefinition {
|
|
323
|
+
provider: BrowserProviderIdentity;
|
|
324
|
+
browserType: {
|
|
325
|
+
name: string;
|
|
326
|
+
documentationUrl?: string;
|
|
327
|
+
inputSchema: BrowserProviderObjectSchema;
|
|
328
|
+
configuration?: {
|
|
329
|
+
inputSchema: BrowserProviderObjectSchema;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export interface BrowserProviderListResult {
|
|
334
|
+
providers: BrowserProviderDefinition[];
|
|
335
|
+
}
|
|
336
|
+
export interface BrowserAccessResult {
|
|
337
|
+
browserAccess: {
|
|
338
|
+
kind: string;
|
|
339
|
+
url: string;
|
|
340
|
+
sensitive: true;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
247
343
|
export interface SetRuleParams {
|
|
248
344
|
actionType: 'redirect' | 'block' | 'allow' | 'upgradeScheme' | 'modifyHeaders';
|
|
249
345
|
filter: string;
|
|
@@ -619,7 +715,7 @@ export interface BrowserActionMap {
|
|
|
619
715
|
};
|
|
620
716
|
browser_launch: {
|
|
621
717
|
request: BrowserLaunchParams;
|
|
622
|
-
response:
|
|
718
|
+
response: ManagedBrowserInfo;
|
|
623
719
|
};
|
|
624
720
|
browser_close: {
|
|
625
721
|
request: BrowserCloseParams;
|
|
@@ -879,5 +975,6 @@ export declare const EXTENSION_BROWSER_ACTIONS: {
|
|
|
879
975
|
};
|
|
880
976
|
export declare function isBrowserAction(value: unknown): value is BrowserAction;
|
|
881
977
|
export declare function isBrowserCommandAction(value: unknown): value is BrowserCommandAction;
|
|
978
|
+
export declare function isBrowserCapabilityAction(value: unknown): value is BrowserCapabilityAction;
|
|
882
979
|
export declare function isExtensionBrowserAction(value: unknown): value is ExtensionBrowserAction;
|
|
883
980
|
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,
|
|
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, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetApiSchemaParams, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, GetUserInfoParams, 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,56 @@ 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: 'managed',
|
|
81
|
+
status: browser.status ?? 'running_disconnected',
|
|
82
|
+
transport: 'provider',
|
|
83
|
+
provider: browser.provider,
|
|
84
|
+
ownership: browser.ownership,
|
|
85
|
+
persistence: browser.persistence,
|
|
86
|
+
operations: browser.operations,
|
|
87
|
+
pairing: browser.pairing,
|
|
88
|
+
references: browser.references,
|
|
89
|
+
details: browser.details,
|
|
90
|
+
}));
|
|
91
|
+
return {
|
|
92
|
+
count: providerBrowsers.length,
|
|
93
|
+
connectedCount: providerBrowsers.filter(browser => browser.status === 'connected').length,
|
|
94
|
+
browsers: providerBrowsers,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function mergeWarnings(result, warnings) {
|
|
98
|
+
const resultWarnings = Array.isArray(result?.warnings)
|
|
99
|
+
? result.warnings.filter((warning) => typeof warning === 'string')
|
|
100
|
+
: [];
|
|
101
|
+
const merged = [...new Set([...resultWarnings, ...warnings])];
|
|
102
|
+
return { ...result, ...(merged.length > 0 ? { warnings: merged } : {}) };
|
|
103
|
+
}
|
|
104
|
+
function isConnectedBrowser(browser) {
|
|
105
|
+
return browser.status === undefined || browser.status === 'connected';
|
|
106
|
+
}
|
|
107
|
+
function supportsAction(browser, action, data) {
|
|
108
|
+
if (action === 'get_versions')
|
|
109
|
+
return true;
|
|
110
|
+
if (action === 'get_browser_access_url') {
|
|
111
|
+
return browser.transport !== 'provider' || browser.operations?.includes('access') === true;
|
|
112
|
+
}
|
|
113
|
+
if (action === 'browser_close') {
|
|
114
|
+
if (browser.transport !== 'provider')
|
|
115
|
+
return true;
|
|
116
|
+
const required = data.deleteProfile === true ? 'deleteData' : 'close';
|
|
117
|
+
return browser.operations?.includes(required) === true;
|
|
118
|
+
}
|
|
119
|
+
if (!isConnectedBrowser(browser))
|
|
120
|
+
return false;
|
|
121
|
+
return (browser.transport !== 'provider' ||
|
|
122
|
+
browser.capabilities?.actions.some(capability => capability === action) === true);
|
|
123
|
+
}
|
|
73
124
|
function selectBrowserResult(data) {
|
|
74
125
|
const selector = typeof data.browser === 'string' ? data.browser.trim() : '';
|
|
75
126
|
if (!selector) {
|
|
@@ -80,7 +131,8 @@ function selectBrowserResult(data) {
|
|
|
80
131
|
};
|
|
81
132
|
}
|
|
82
133
|
const browsers = listLiveBrowsers();
|
|
83
|
-
const
|
|
134
|
+
const controllableBrowsers = browsers.filter(isConnectedBrowser);
|
|
135
|
+
const match = findBrowser(controllableBrowsers, selector);
|
|
84
136
|
if (!match)
|
|
85
137
|
throw new Error(formatNoMatchError(selector, browsers));
|
|
86
138
|
setDefaultBrowser(match.browserId);
|
|
@@ -148,7 +200,11 @@ export function invoke(action, data = {}, options = {}) {
|
|
|
148
200
|
// 统一解析 payload 顶层字段中的 `@<path>` 文件引用(所有 action 通用)
|
|
149
201
|
const resolvedData = resolveFileRefs(data);
|
|
150
202
|
if (action === 'browser_list') {
|
|
151
|
-
|
|
203
|
+
const hasNativeAggregator = listLiveBrowsers().some(liveBrowser => liveBrowser.transport !== 'provider');
|
|
204
|
+
const providerStartup = hasNativeAggregator
|
|
205
|
+
? Promise.resolve([])
|
|
206
|
+
: ensureInstalledProvidersRunning();
|
|
207
|
+
return providerStartup.then(providerWarnings => dispatchInvoke(action, resolvedData, timeoutSec, undefined, undefined, controlSource).then(result => decorateBrowserListResult(mergeWarnings(result, providerWarnings))));
|
|
152
208
|
}
|
|
153
209
|
const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
|
|
154
210
|
return dispatchInvoke(action, resolvedData, timeoutSec, transport, selector, controlSource);
|
|
@@ -176,14 +232,24 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
176
232
|
}
|
|
177
233
|
}
|
|
178
234
|
const browsers = listLiveBrowsers();
|
|
235
|
+
const controllableBrowsers = browsers.filter(isConnectedBrowser);
|
|
179
236
|
// Explicit target: must match a live browser; do not silently fall back.
|
|
180
237
|
if (selector) {
|
|
181
238
|
const match = findBrowser(browsers, selector);
|
|
182
239
|
if (!match) {
|
|
183
240
|
throw new Error(formatNoMatchError(selector, browsers));
|
|
184
241
|
}
|
|
242
|
+
if (transport === 'cdp' && match.transport === 'provider') {
|
|
243
|
+
throw new Error(`Browser ${describeBrowser(match)} does not support the CDP transport`);
|
|
244
|
+
}
|
|
245
|
+
if (match.status === 'pairing' && action !== 'get_versions' && action !== 'browser_close') {
|
|
246
|
+
throw new Error(`Browser ${describeBrowser(match)} is waiting for device pairing`);
|
|
247
|
+
}
|
|
248
|
+
if (!supportsAction(match, action, data)) {
|
|
249
|
+
throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
|
|
250
|
+
}
|
|
185
251
|
try {
|
|
186
|
-
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource);
|
|
252
|
+
return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.providerBrowserId);
|
|
187
253
|
}
|
|
188
254
|
catch (error) {
|
|
189
255
|
if (isConnError(error) || isSocketSandboxError(error)) {
|
|
@@ -196,18 +262,26 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
|
|
|
196
262
|
}
|
|
197
263
|
// No explicit target: prefer the most-recently-focused live browser; on a
|
|
198
264
|
// stale socket, fall through to the legacy path + auto-spawn.
|
|
199
|
-
|
|
265
|
+
const routableBrowsers = controllableBrowsers.filter(browser => !(transport === 'cdp' && browser.transport === 'provider') &&
|
|
266
|
+
supportsAction(browser, action, data));
|
|
267
|
+
if (routableBrowsers.length > 0) {
|
|
268
|
+
const target = routableBrowsers[0];
|
|
200
269
|
try {
|
|
201
|
-
return await invokeOnce(action, data, timeoutSec, transport,
|
|
270
|
+
return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
|
|
202
271
|
}
|
|
203
272
|
catch (error) {
|
|
204
273
|
if (isSocketSandboxError(error)) {
|
|
205
|
-
throw new Error(formatConnectError(error,
|
|
274
|
+
throw new Error(formatConnectError(error, target.socketPath), {
|
|
275
|
+
cause: error,
|
|
276
|
+
});
|
|
206
277
|
}
|
|
207
278
|
if (!isConnError(error))
|
|
208
279
|
throw error;
|
|
209
280
|
}
|
|
210
281
|
}
|
|
282
|
+
if (action === 'browser_list' && browsers.some(browser => browser.transport === 'provider')) {
|
|
283
|
+
return browserListFromProviderRecords(browsers);
|
|
284
|
+
}
|
|
211
285
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource, /* isRetry */ false);
|
|
212
286
|
}
|
|
213
287
|
/** Legacy well-known socket, auto-spawning a native-host on ENOENT and retrying once. */
|
|
@@ -229,8 +303,11 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
229
303
|
/* isRetry */ true);
|
|
230
304
|
}
|
|
231
305
|
const browsers = listLiveBrowsers();
|
|
232
|
-
|
|
233
|
-
|
|
306
|
+
const target = browsers.find(browser => isConnectedBrowser(browser) &&
|
|
307
|
+
!(transport === 'cdp' && browser.transport === 'provider') &&
|
|
308
|
+
supportsAction(browser, action, data));
|
|
309
|
+
if (target) {
|
|
310
|
+
return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.providerBrowserId);
|
|
234
311
|
}
|
|
235
312
|
return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
|
|
236
313
|
/* isRetry */ true);
|
|
@@ -241,7 +318,7 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
|
|
|
241
318
|
throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
|
|
242
319
|
}
|
|
243
320
|
}
|
|
244
|
-
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource) {
|
|
321
|
+
function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerBrowserId) {
|
|
245
322
|
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
246
323
|
return new Promise((resolve, reject) => {
|
|
247
324
|
if (process.platform !== 'win32' && fs.existsSync(socketPath) && process.getuid) {
|
|
@@ -282,6 +359,8 @@ function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSour
|
|
|
282
359
|
};
|
|
283
360
|
if (transport)
|
|
284
361
|
message.transport = transport;
|
|
362
|
+
if (providerBrowserId)
|
|
363
|
+
message.browserId = providerBrowserId;
|
|
285
364
|
socket.write(JSON.stringify(message) + '\n');
|
|
286
365
|
});
|
|
287
366
|
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)
|
|
@@ -414,17 +420,7 @@ export class UnifiedClient {
|
|
|
414
420
|
decorateActionResult(action, result, source) {
|
|
415
421
|
if (action !== 'browser_launch' || typeof result?.browserId !== 'string')
|
|
416
422
|
return result;
|
|
417
|
-
|
|
418
|
-
const originalBrowserId = result.browserId;
|
|
419
|
-
return {
|
|
420
|
-
...safeResult,
|
|
421
|
-
browserId: encodeBrowserTarget({
|
|
422
|
-
browser: originalBrowserId,
|
|
423
|
-
...(source.connectorId ? { connector: source.connectorId } : {}),
|
|
424
|
-
}),
|
|
425
|
-
originalBrowserId,
|
|
426
|
-
source,
|
|
427
|
-
};
|
|
423
|
+
return decorateBrowser(result, source);
|
|
428
424
|
}
|
|
429
425
|
async ensureConnected() {
|
|
430
426
|
if (this.ws?.readyState === WebSocket.OPEN)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mearl/client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.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.14.0",
|
|
63
|
+
"@mearl/provider": "2.14.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@types/node": "^24.9.1",
|