@mearl/client 3.0.0-beta.5 → 3.0.0-beta.8

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.
Files changed (42) hide show
  1. package/README.md +18 -14
  2. package/dist/actionTimeouts.js +0 -3
  3. package/dist/autoUpdate.js +2 -1
  4. package/dist/bin.js +12 -5
  5. package/dist/capabilityHelp.d.ts +4 -0
  6. package/dist/capabilityHelp.js +68 -0
  7. package/dist/check.js +1 -1
  8. package/dist/cli.js +17 -16
  9. package/dist/commandHelp.js +3 -3
  10. package/dist/commandSuggestions.d.ts +3 -0
  11. package/dist/commandSuggestions.js +16 -5
  12. package/dist/commands.d.ts +50 -55
  13. package/dist/commands.js +41 -41
  14. package/dist/guide.d.ts +46 -0
  15. package/dist/guide.js +71 -0
  16. package/dist/guides/api-browser-setup.md +93 -0
  17. package/dist/guides/api-page-automation.md +140 -0
  18. package/dist/guides/api-page-debugging.md +39 -0
  19. package/dist/guides/api-page-environment.md +37 -0
  20. package/dist/guides/api-request-debugging.md +54 -0
  21. package/dist/guides/browser-tool-cooperation.md +23 -0
  22. package/dist/guides/core.md +60 -0
  23. package/dist/guides/related-skills.md +32 -0
  24. package/dist/guides/scripting.md +21 -0
  25. package/dist/guides/troubleshooting.md +170 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +4 -5
  28. package/dist/plugins/officialPluginInstaller.js +2 -1
  29. package/dist/plugins/pluginRegistry.js +9 -3
  30. package/dist/plugins/runtime/pluginDiagnostics.js +1 -1
  31. package/dist/plugins/runtime/pluginHostServer.js +1 -1
  32. package/dist/plugins/runtime/pluginPageManager.js +2 -2
  33. package/dist/plugins/runtime/pluginStateManager.js +1 -1
  34. package/dist/plugins/runtime/pluginTaskManager.js +2 -2
  35. package/dist/plugins/sdk/plugin.d.ts +2 -0
  36. package/dist/plugins/sdk/plugin.js +17 -9
  37. package/dist/script.js +2 -1
  38. package/dist/socket.js +32 -8
  39. package/dist/unified.js +11 -1
  40. package/dist/unifiedConfig.d.ts +4 -0
  41. package/dist/unifiedConfig.js +11 -0
  42. package/package.json +10 -10
@@ -0,0 +1,170 @@
1
+ # Mearl 环境安装与连接排查
2
+
3
+ ## 目录
4
+
5
+ - [环境安装、更新与卸载](#environment-management)
6
+ - [本机控制](#local)
7
+ - [浏览器扩展安装](#local-extension)
8
+ - [命令不可用或连接失败](#local-check)
9
+ - [EACCES](#eacces)
10
+ - [ENOENT](#enoent)
11
+ - [ECONNREFUSED / 连接关闭](#econnrefused)
12
+ - [远程控制](#cloud)
13
+ - [Agent(控制端)](#cloud-agent)
14
+ - [浏览器端](#cloud-browser)
15
+ - [远程控制常见问题](#cloud-issues)
16
+ - [通用问题](#common)
17
+ - [请求超时](#timeout)
18
+
19
+ 按 Agent 与浏览器所在机器选择排障流程;命令参数读取对应 CLI 的 `--help`,失败时优先按错误或 `check` 输出的提示处理。
20
+
21
+ - **本机控制**:Agent 与浏览器在同一台机器,`@mearl/client` 通过 native host socket 通信。
22
+ - **远程控制**:Agent 与浏览器不在同一台机器,`@mearl/client` 通过 cloud-server + cloud-connector 中转。
23
+
24
+ <a id="environment-management"></a>
25
+
26
+ ## 环境安装、更新与卸载
27
+
28
+ CLI、Native Host、MCP 和云端核心运行包要求 Node.js >= 20.18.0。安装前用 `node --version` 确认版本;
29
+ 不满足时先升级 Node。3.0 beta 使用 `npx @mearl/setup@beta --yes`,保留显式的 `@beta` 标签。
30
+ 出现 `Unknown operation: plugin` 时,用 `npx @mearl/setup@beta --version` 核对安装器版本;
31
+ 未指定版本时,npm 可能因 Node 不兼容而选择历史版本。
32
+
33
+ 环境管理使用 `npx @mearl/setup@beta <install|update|uninstall>`,具体选项按子命令 `--help` 查询。Agent 执行 install/uninstall 时附加 `--yes` 或 `--non-interactive`,避免等待交互;人工终端可保留交互选择。
34
+
35
+ 主动安装或更新本机环境时,setup 优先使用显式 `--preset <清单 URL 或路径>`,否则保留本机已选 preset;本机未选择时,在运行时初始化后、安装任何 Skill 前检查已连接扩展的受信 preset 并补齐本机组件。首次连接最多等待约一分钟。多个浏览器选择不同、来源不受信或状态不可读时会停止并提示显式选择,不猜测。浏览器未连接时仅安装基础环境,连接后重跑 setup 即可;普通连接和后台自动更新不触发首次本机 preset 安装。
36
+
37
+ 卸载只在用户明确要求一并移除全部 CLI 插件和浏览器 Provider 时传 `--full`;该选项不删除 Chrome 扩展。正常浏览器任务不主动安装或重新初始化,下方排障只在连接失败后使用。
38
+
39
+ ---
40
+
41
+ <a id="local"></a>
42
+
43
+ ## 本机控制
44
+
45
+ <a id="local-extension"></a>
46
+
47
+ ### 浏览器扩展安装
48
+
49
+ 优先安装 [Chrome 商店版](https://chromewebstore.google.com/detail/mearl/aoehhjnofngknnjefamjbplchbolghkm),由 Chrome 管理更新。商店不可用或用户明确要求 setup 托管时,执行:
50
+
51
+ ```bash
52
+ npx @mearl/setup@beta extension --full --yes
53
+ ```
54
+
55
+ 该命令同时准备本地环境。人工在终端执行时可省略 `--yes`。
56
+
57
+ <a id="local-check"></a>
58
+
59
+ ### 命令不可用或连接失败
60
+
61
+ `mearl` 或 `mearl-native-host` 命令不存在时,在浏览器所在机器安装:
62
+
63
+ ```bash
64
+ npx @mearl/setup@beta --local --yes
65
+ ```
66
+
67
+ 命令存在但连接失败时,先检查当前链路:
68
+
69
+ ```bash
70
+ mearl check
71
+ ```
72
+
73
+ 只需隔离检查当前机器时使用 `mearl check --local`;路由参数与配置优先级见 `mearl check --help`。只有检查结果表明本地组件缺失、过期或初始化不完整时,才更新并重新初始化:
74
+
75
+ ```bash
76
+ npx @mearl/setup@beta update --local --yes
77
+ ```
78
+
79
+ <a id="eacces"></a>
80
+
81
+ ### EACCES(权限不足)
82
+
83
+ 仅当 native host 初始化写入系统目录时权限不足,才在浏览器所在机器执行:
84
+
85
+ ```bash
86
+ sudo mearl-native-host --init
87
+ ```
88
+
89
+ Socket 访问被 Agent 沙箱拒绝时,按错误提示配置访问权限,不通过 sudo 或改写 Socket 权限绕过。
90
+
91
+ <a id="enoent"></a>
92
+
93
+ ### ENOENT(找不到 Socket)
94
+
95
+ client 会自动拉起 native host 并重试。自动恢复失败时按错误提示检查本机链路;手工启动或开发版扩展初始化的参数见 `mearl-native-host --help`,不要用 `nohup` 手工保活 daemon。
96
+
97
+ 如果重试后仍然失败,可按以下步骤排查:
98
+
99
+ **使用浏览器插件模式:**
100
+
101
+ 确认 Chrome 已打开并等待扩展建联;仍失败时打开 DevTools 的 Mearl 面板触发连接。开发版扩展使用其实际扩展 ID 初始化 native host,再重新执行命令。
102
+
103
+ **使用 CDP 模式:**
104
+
105
+ 按 `mearl-native-host --help` 或连接错误提示在 Chrome 开启调试并允许授权,确认有普通网页已打开后重试。默认在 Extension 不可用时回退 CDP;需要单独验证 CDP 链路时使用 `mearl check --cdp`。
106
+
107
+ <a id="econnrefused"></a>
108
+
109
+ ### ECONNREFUSED / 连接关闭
110
+
111
+ client 会处理当前用户的 stale socket 并尝试恢复 native host;恢复失败时运行 `mearl check --local`,按诊断处理,避免盲目删除 Socket 或重复初始化。
112
+
113
+ ---
114
+
115
+ <a id="cloud"></a>
116
+
117
+ ## 远程控制
118
+
119
+ 远程控制需要在 **Agent(控制端)** 和 **浏览器端** 两端分别准备。
120
+
121
+ <a id="cloud-agent"></a>
122
+
123
+ ### Agent(控制端)
124
+
125
+ 1. 安装 cloud-server、统一 client 与当前 Agent 的全局 mearl skill:
126
+
127
+ ```bash
128
+ npx @mearl/setup@beta --cloud --yes
129
+ ```
130
+
131
+ setup 会启动 cloud-server 并输出连接命令。把输出的完整命令交给用户在浏览器所在机器执行,不自行拼接地址或 token。
132
+
133
+ 2. 再次获取连接命令使用 `mearl-cloud-server status`;管理与日志命令见 `mearl-cloud-server --help`。
134
+
135
+ 3. client 自动读取连接配置。用户完成本地连接后,在 Agent 侧验证:
136
+
137
+ ```bash
138
+ mearl check
139
+ ```
140
+
141
+ 按诊断定位故障所在机器,再在对应机器修复。默认不逐次传 Server 地址;覆盖参数、配置优先级和检查范围见 `mearl check --help`。连接 URL 可能包含 token,不写入日志或转发给无关方。
142
+
143
+ <a id="cloud-browser"></a>
144
+
145
+ ### 浏览器端
146
+
147
+ 1. 执行 Agent 侧输出的完整连接命令,把浏览器接入远程控制链路。
148
+
149
+ 2. 浏览器端仍需满足 [本机控制](#local) 的前置条件(native host 已初始化、浏览器插件或 CDP 已建联),否则 connector 无法连到本地 socket。相关报错(EACCES / ENOENT / ECONNREFUSED)参考上文本机排查步骤。
150
+
151
+ <a id="cloud-issues"></a>
152
+
153
+ ### 远程控制常见问题
154
+
155
+ - **Agent 侧无法连接 Server**:运行 `mearl check`,按失败项的提示处理。
156
+ - **需要确认动作会落到哪台机器**:运行 `mearl browser_list`,首次操作传返回的全局 `--browser <browserId>`;控制会话建立后由 `--session <sessionId>` 固定目标,无会话时继续传选定的 `--browser`。需要限制到某台远端机器时,从 `source` 获取 ID 或名称并传 `--connector <id|name>`。
157
+ - **connector 连不上或一直重连**:在浏览器端运行 `mearl-cloud-connector status`,按状态和错误提示处理。
158
+ - **connector 已接入但浏览器不可用或请求无响应**:在浏览器端运行 `mearl check --local`;需要检查 connector 版本与心跳时,Agent 端使用 `mearl connector_list`,字段见该命令 `--help`。
159
+
160
+ ### 可选云连接协议
161
+
162
+ Qoder Cloud Agent 需要在浏览器机器先安装 `@mearl/plugin-qoder` 能力,再按该包 README 和 `docs/cloud-pairing.md` 生成配对信息、配置自定义工具。普通远端 Agent 使用上述 WebSocket 模式。核心未安装对应协议时会报告能力缺失,不会自动安装或读取该协议的凭据。
163
+
164
+ ### 请求超时
165
+
166
+ 超时可能发生在连接、动作执行或结果回传阶段,不能据此判断动作未执行。
167
+
168
+ - 连接超时:使用 `mearl check` 定位链路故障。
169
+ - 点击、提交、上传或写入超时:先读取当前页面或业务状态确认结果,再决定是否重试,避免重复副作用。
170
+ - 只读查询超时:按 CLI 提示缩小查询范围、裁剪结果或增加 `--timeout`;默认时间按 action 推导,不统一设为 30 秒。
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { UnifiedClient, UnifiedClient as MearlClient, decodeBrowserTarget, invok
13
13
  export type { UnifiedClientOptions, UnifiedInvokeOptions, UnifiedInvokeOptions as InvokeOptions, } from './unified.js';
14
14
  export type { PluginInvocation, PluginInvokeOptions, PluginProgress, StructuredPluginProgress, } from './plugins/sdk/plugin.js';
15
15
  export { APP_PROFILE_NAMES, BROWSER_DEVICE_PRESET_NAMES, BROWSER_ACTIONS, BROWSER_CAPABILITY_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 '@mearl/browser-core';
16
- 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, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestDomainPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestBinaryBody, SendRequestFormValue, SendMtopRequestParams, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from '@mearl/browser-core';
16
+ export type { ActionStep, AppProfileName, BrowserAction, BrowserActionMap, BrowserActionRequest, BrowserActionResponse, BrowserCapabilities, BrowserCapabilityAction, BrowserCloseParams, BrowserCommandAction, BrowserInfo, BrowserInfoBase, BrowserJsonValue, BrowserLaunchParams, BrowserProviderOperation, BrowserListResult, BrowserListParams, BrowserPairingInfo, BrowserReleaseFailure, BrowserReleaseResult, BrowserProviderDefinition, BrowserProviderIdentity, BrowserProviderInputSchema, BrowserProviderListResult, BrowserReference, BrowserDetails, BrowserSource, BrowserStatus, BrowserScreenshotKind, BrowserTransport, CaptureCheckpointParams, CaptureCheckpointResult, BrowserDevicePresetName, BrowserEmulationOptions, ExtensionBrowserAction, GetCookieParams, GetCookieResult, GetEventsParams, GetLogsParams, GetMocksParams, GetRequestsParams, ManagedBrowserInfo, PageClickParams, PageDragEndpoint, PageDragParams, PageDragPoint, PageActAction, PageDiagnosticsOptions, PageDiagnosticsSetting, PageEvalParams, PageFramesParams, PageHoverParams, PageInspectFilter, PageInspectParams, PageNavigateParams, PageObservationWaitOptions, PagePressParams, PageScreenshotParams, PageScrollParams, PageSnapshotParams, PageSnapshotQuery, PageTypeParams, PageUploadParams, PageWaitParams, RegularBrowserInfo, RequestPermissionParams, RulesResult, RunActionName, RunActionsParams, SelectBrowserParams, SelectBrowserResult, SendRequestFile, SendRequestBinaryBody, SendRequestFormValue, SendRequestParams, SetAppProfileParams, SetCookieParams, SetCookieResult, SetDeviceEmulationParams, SetMockParams, SetRuleParams, SetTimezoneParams, SetTimezoneResult, TabCloseParams, TabOpenParams, } from '@mearl/browser-core';
17
17
  import { type UnifiedInvokeOptions } from './unified.js';
18
18
  import type * as Protocol from '@mearl/browser-core';
19
19
  export declare function tabCheckpoint(params: Protocol.CaptureCheckpointParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'tab_checkpoint'>>;
@@ -25,8 +25,8 @@ export declare function getEvents(params: Protocol.GetEventsParams, options?: Un
25
25
  export declare function setMock(params: Protocol.SetMockParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'set_mock'>>;
26
26
  export declare function getMocks(params: Protocol.GetMocksParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'get_mocks'>>;
27
27
  export declare function sendRequest(params: Protocol.SendRequestParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'send_request'>>;
28
- export declare function sendMtopRequest(params: Protocol.SendMtopRequestParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'send_mtop_request'>>;
29
28
  export declare function browserList(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
29
+ export declare function browserList(params: Protocol.BrowserListParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_list'>>;
30
30
  export declare function selectBrowser(params?: Protocol.SelectBrowserParams, options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_select_browser'>>;
31
31
  export declare function browserRelease(options?: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
32
32
  export declare function browserRelease(params: Protocol.BrowserReleaseParams, options: UnifiedInvokeOptions): Promise<Protocol.BrowserActionResponse<'browser_release'>>;
package/dist/index.js CHANGED
@@ -36,11 +36,10 @@ export function getMocks(params, options) {
36
36
  export function sendRequest(params, options) {
37
37
  return invoke('send_request', params, options);
38
38
  }
39
- export function sendMtopRequest(params, options) {
40
- return invoke('send_mtop_request', params, options);
41
- }
42
- export function browserList(options) {
43
- return invoke('browser_list', {}, options);
39
+ export function browserList(paramsOrOptions = {}, options) {
40
+ return 'provider' in paramsOrOptions || 'launchForm' in paramsOrOptions
41
+ ? invoke('browser_list', paramsOrOptions, options)
42
+ : invoke('browser_list', {}, paramsOrOptions);
44
43
  }
45
44
  export function selectBrowser(params = {}, options) {
46
45
  return invoke('browser_select_browser', params, options);
@@ -2,6 +2,7 @@ import { closeSync, mkdirSync, openSync, rmSync, statSync, writeFileSync } from
2
2
  import { CLIENT_VERSION } from '../runtimePaths.js';
3
3
  import { join } from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
+ import { npmExecEnvironment } from '@mearl/daemon-core';
5
6
  import { findOfficialPlugin, readOfficialPluginCatalog } from './officialPluginCatalog.js';
6
7
  import { findPluginRegistration, pluginDirectory } from './pluginRegistry.js';
7
8
  const INSTALL_TIMEOUT_MS = 180_000;
@@ -60,7 +61,7 @@ function appendOutput(current, chunk) {
60
61
  function installWithSetup(name) {
61
62
  return new Promise((resolve, reject) => {
62
63
  const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
63
- const child = spawn(command, ['-y', '--@mearl:registry=https://registry.npmjs.org', `@mearl/setup@${CLIENT_VERSION}`, 'plugin', 'ensure', name], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
64
+ const child = spawn(command, ['-y', '--@mearl:registry=https://registry.npmjs.org', `@mearl/setup@${CLIENT_VERSION}`, 'plugin', 'ensure', name], { env: npmExecEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
64
65
  let stdout = '';
65
66
  let stderr = '';
66
67
  let settled = false;
@@ -8,6 +8,7 @@ import { COMMAND_MAP } from '../commands.js';
8
8
  import { buildPluginPageBundle, PLUGIN_PAGE_CHUNK_BYTES, PLUGIN_PAGE_RUNTIME_HASH, } from './pluginPageBundle.js';
9
9
  import { pluginFileVersion } from './pluginSource.js';
10
10
  const RESERVED_NAMES = new Set([
11
+ 'guide',
11
12
  'check',
12
13
  'run',
13
14
  'help',
@@ -15,6 +16,11 @@ const RESERVED_NAMES = new Set([
15
16
  'capture_checkpoint',
16
17
  'plugin',
17
18
  'plugins',
19
+ 'plugin_state',
20
+ 'plugin_model',
21
+ 'plugin_page',
22
+ 'plugin_invoke_child',
23
+ 'plugin_task_checkpoint',
18
24
  ]);
19
25
  export class PluginError extends Error {
20
26
  code;
@@ -111,7 +117,7 @@ function persistPluginPageBundle(name, bundle) {
111
117
  }
112
118
  }
113
119
  export function assertPluginName(name) {
114
- if (!/^[a-z][a-z0-9-]*$/.test(name) || RESERVED_NAMES.has(name) || COMMAND_MAP.has(name)) {
120
+ if (!/^[a-z][a-z0-9_-]*$/.test(name) || RESERVED_NAMES.has(name) || COMMAND_MAP.has(name)) {
115
121
  throw new PluginError('PLUGIN_INVALID_NAME', `不可用的插件名称: ${name}`);
116
122
  }
117
123
  }
@@ -346,7 +352,7 @@ export function unregisterPlugin(name) {
346
352
  }
347
353
  }
348
354
  export function findPluginRegistration(name) {
349
- if (!/^[a-z][a-z0-9-]*$/.test(name))
355
+ if (!/^[a-z][a-z0-9_-]*$/.test(name))
350
356
  return undefined;
351
357
  const file = join(pluginDirectory(), `${name}.json`);
352
358
  let content;
@@ -470,7 +476,7 @@ export function findPluginEntry(name) {
470
476
  export function listPluginNames() {
471
477
  try {
472
478
  return readdirSync(pluginDirectory())
473
- .filter(file => /^[a-z][a-z0-9-]*\.json$/.test(file))
479
+ .filter(file => /^[a-z][a-z0-9_-]*\.json$/.test(file))
474
480
  .map(file => file.slice(0, -5))
475
481
  .sort();
476
482
  }
@@ -26,7 +26,7 @@ export function normalizeDiagnosticOptions(value) {
26
26
  throw new Error('Invalid diagnostic instance ID');
27
27
  if (input.plugin !== undefined &&
28
28
  (typeof input.plugin !== 'string' ||
29
- !/^[a-z][a-z0-9-]*$/.test(input.plugin) ||
29
+ !/^[a-z][a-z0-9_-]*$/.test(input.plugin) ||
30
30
  input.plugin.length > 128))
31
31
  throw new Error('Invalid diagnostic plugin');
32
32
  for (const key of ['includeErrorMessages', 'includeLocalPaths'])
@@ -91,7 +91,7 @@ export async function startPluginHostServer(configuration = {}) {
91
91
  !['service', 'caller'].includes(String(input.bridge)) ||
92
92
  !['cli', 'sdk', 'mcp', 'toolbox', 'runtime'].includes(String(input.source)) ||
93
93
  (input.plugin !== undefined &&
94
- (typeof input.plugin !== 'string' || !/^[a-z][a-z0-9-]*$/.test(input.plugin))) ||
94
+ (typeof input.plugin !== 'string' || !/^[a-z][a-z0-9_-]*$/.test(input.plugin))) ||
95
95
  (input.scopeId !== undefined &&
96
96
  (typeof input.scopeId !== 'string' || input.scopeId.length > 256)) ||
97
97
  (input.access !== 'public' && (!input.plugin || input.bridge !== 'caller')))
@@ -272,7 +272,7 @@ export class PluginPageManager {
272
272
  execution.signal?.throwIfAborted();
273
273
  if (!input || typeof input !== 'object' || Array.isArray(input))
274
274
  throw new Error('Invalid plugin page request');
275
- if (!/^[a-z][a-z0-9-]*$/.test(plugin) ||
275
+ if (!/^[a-z][a-z0-9_-]*$/.test(plugin) ||
276
276
  (bridge.scope.plugin && bridge.scope.plugin !== plugin))
277
277
  throw new Error('Invalid plugin page scope');
278
278
  const operation = input.operation;
@@ -656,7 +656,7 @@ export class PluginPageManager {
656
656
  async receive(plugin, input, bridge, options = {}) {
657
657
  if (input.kind === 'sync') {
658
658
  if (this.closed ||
659
- !/^[a-z][a-z0-9-]*$/.test(plugin) ||
659
+ !/^[a-z][a-z0-9_-]*$/.test(plugin) ||
660
660
  (bridge.scope.plugin && bridge.scope.plugin !== plugin) ||
661
661
  !Array.isArray(input.instances) ||
662
662
  input.instances.length > 32)
@@ -29,7 +29,7 @@ export class PluginStateManager {
29
29
  async call(plugin, input, bridge, options = {}, binding) {
30
30
  this.controller.signal.throwIfAborted();
31
31
  binding?.signal?.throwIfAborted();
32
- if (!/^[a-z][a-z0-9-]*$/.test(plugin) ||
32
+ if (!/^[a-z][a-z0-9_-]*$/.test(plugin) ||
33
33
  (bridge.scope.plugin && bridge.scope.plugin !== plugin) ||
34
34
  !input ||
35
35
  typeof input !== 'object' ||
@@ -205,7 +205,7 @@ export class PluginTaskManager {
205
205
  executionTimeoutMs: request.executionTimeoutMs ?? 120000,
206
206
  };
207
207
  }
208
- if (!/^[a-z][a-z0-9-]*$/.test(request.plugin) || !/^[a-z][a-z0-9-]*$/.test(request.command))
208
+ if (!/^[a-z][a-z0-9_-]*$/.test(request.plugin) || !/^[a-z][a-z0-9-]*$/.test(request.command))
209
209
  throw new Error('Invalid plugin task command');
210
210
  if (bridge.scope.plugin && request.plugin !== bridge.scope.plugin)
211
211
  throw error('PLUGIN_COMMAND_NOT_ALLOWED', 'Plugin task is outside the caller scope');
@@ -884,7 +884,7 @@ export class PluginTaskManager {
884
884
  });
885
885
  }
886
886
  if (restrictedTarget &&
887
- !['send_request', 'send_mtop_request', 'browser_list', 'connector_list'].includes(action))
887
+ !['send_request', 'browser_list', 'connector_list'].includes(action))
888
888
  throw error('PLUGIN_PAGE_TARGET_CHANGED', 'Runtime tasks must access the document through context.page');
889
889
  this.assertRouting(options, request.options);
890
890
  if (client) {
@@ -72,6 +72,7 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'opti
72
72
  }, pluginName?: string): Promise<{
73
73
  name: string | undefined;
74
74
  description: string | undefined;
75
+ defaultCommand: string | undefined;
75
76
  toolbox: PluginToolboxContribution | undefined;
76
77
  pageRuntime: PluginPageRuntime | undefined;
77
78
  stateStorage: PluginStateDeclaration | undefined;
@@ -90,6 +91,7 @@ export declare function loadPlugin(entry: string, context: Pick<PluginApp, 'opti
90
91
  export declare function loadRegisteredPlugin(name: string, context: PluginExecutionContext, options?: PluginInvokeOptions): Promise<{
91
92
  name: string | undefined;
92
93
  description: string | undefined;
94
+ defaultCommand: string | undefined;
93
95
  toolbox: PluginToolboxContribution | undefined;
94
96
  pageRuntime: PluginPageRuntime | undefined;
95
97
  stateStorage: PluginStateDeclaration | undefined;
@@ -146,6 +146,7 @@ export async function loadPlugin(entry, context, pluginName) {
146
146
  let registering = true;
147
147
  let name;
148
148
  let description;
149
+ let defaultCommand;
149
150
  let toolbox;
150
151
  let pageRuntime;
151
152
  let stateStorage;
@@ -312,6 +313,9 @@ export async function loadPlugin(entry, context, pluginName) {
312
313
  if (module.description !== undefined && typeof module.description !== 'string')
313
314
  throw new Error('入口的 description 导出必须为字符串');
314
315
  description = module.description;
316
+ if (module.defaultCommand !== undefined && typeof module.defaultCommand !== 'string')
317
+ throw new Error('入口的 defaultCommand 导出必须为字符串');
318
+ defaultCommand = module.defaultCommand;
315
319
  toolbox = normalizePluginToolboxContribution(module.toolbox);
316
320
  pageRuntime = normalizePluginPageRuntime(module.pageRuntime);
317
321
  stateStorage = normalizePluginState(module.stateStorage);
@@ -326,6 +330,8 @@ export async function loadPlugin(entry, context, pluginName) {
326
330
  }
327
331
  if (commands.size === 0 && !appProfiles)
328
332
  throw new Error('入口未注册任何命令');
333
+ if (defaultCommand !== undefined && (!commands.has(defaultCommand) || commands.get(defaultCommand)?.toolboxOnly || commands.get(defaultCommand)?.runtimeOnly))
334
+ throw new Error('defaultCommand 必须指向公开命令');
329
335
  for (const command of Object.keys(taskRecovery?.commands ?? {})) {
330
336
  if (!commands.has(command) ||
331
337
  commands.get(command)?.runtimeOnly ||
@@ -401,12 +407,13 @@ export async function loadPlugin(entry, context, pluginName) {
401
407
  if (payload !== undefined && Object.keys(namedInput).length > 0) {
402
408
  throw new PluginError('PLUGIN_INVALID_INPUT', '插件业务选项与 --payload 调用不能混用');
403
409
  }
404
- if (payload !== undefined &&
405
- (!isObject(payload) || typeof payload.command !== 'string' || !isObject(payload.input))) {
410
+ const defaultPayload = defaultCommand && toolboxPage === false && isObject(payload) && !Object.hasOwn(payload, 'command');
411
+ if (payload !== undefined && !defaultPayload && !isPluginInvocation(payload)) {
406
412
  throw new PluginError('PLUGIN_INVALID_INPUT', '--payload 必须为 {"command":"...","input":{...}}');
407
413
  }
408
- const envelope = payload;
409
- const commandName = envelope?.command ?? positionals[0] ?? '';
414
+ const envelope = (defaultPayload ? { command: defaultCommand, input: payload } : payload);
415
+ const positionalOffset = defaultCommand && !commands.has(positionals[0]) && toolboxPage === false ? 0 : 1;
416
+ const commandName = envelope?.command ?? (positionalOffset === 0 ? defaultCommand : positionals[0] ?? '');
410
417
  const hidden = toolboxPage === 'recovery' || toolboxPage === 'migration';
411
418
  const selected = find(commandName, hidden ? false : toolboxPage);
412
419
  const command = selected.command;
@@ -426,7 +433,7 @@ export async function loadPlugin(entry, context, pluginName) {
426
433
  : Object.fromEntries(command.registeredArguments
427
434
  .map((argument, index) => [
428
435
  argument.name(),
429
- argument.variadic ? positionals.slice(index + 1) : positionals[index + 1],
436
+ argument.variadic ? positionals.slice(index + positionalOffset) : positionals[index + positionalOffset],
430
437
  ])
431
438
  .filter(([, value]) => value !== undefined));
432
439
  for (const name of Object.keys(namedInput)) {
@@ -437,7 +444,7 @@ export async function loadPlugin(entry, context, pluginName) {
437
444
  const input = { ...positionalInput, ...namedInput };
438
445
  if (!envelope &&
439
446
  !command.registeredArguments.some(argument => argument.variadic) &&
440
- positionals.length - 1 > command.registeredArguments.length) {
447
+ positionals.length - positionalOffset > command.registeredArguments.length) {
441
448
  throw new PluginError('PLUGIN_INVALID_INPUT', '位置参数过多');
442
449
  }
443
450
  for (const argument of hidden ? [] : command.registeredArguments) {
@@ -529,6 +536,7 @@ export async function loadPlugin(entry, context, pluginName) {
529
536
  return {
530
537
  name,
531
538
  description,
539
+ defaultCommand,
532
540
  toolbox,
533
541
  pageRuntime,
534
542
  stateStorage,
@@ -538,11 +546,11 @@ export async function loadPlugin(entry, context, pluginName) {
538
546
  version: readPluginVersion(entry),
539
547
  commandHelp(namespace) {
540
548
  return publicCommands()
541
- .map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`)
549
+ .map(({ command, signature }) => ` ${namespace} ${command.name() === defaultCommand ? signature.slice(defaultCommand.length).trim() : signature} ${command.description()}`)
542
550
  .join('\n');
543
551
  },
544
552
  help(namespace, name) {
545
- const selected = name ? [find(name)] : publicCommands();
553
+ const selected = name ? [find(defaultCommand && !commands.has(name) ? defaultCommand : name)] : publicCommands();
546
554
  if (!selected.length) {
547
555
  const location = toolbox
548
556
  ? `请在 Mearl ${toolbox.displayMode === 'popup' ? 'Popup' : '工具箱'} 中打开「${toolbox.title}」使用。`
@@ -552,7 +560,7 @@ export async function loadPlugin(entry, context, pluginName) {
552
560
  const examples = selected.flatMap(({ examples }) => examples);
553
561
  return [
554
562
  '用法:',
555
- ...selected.map(({ command, signature }) => ` ${namespace} ${signature} ${command.description()}`),
563
+ ...selected.map(({ command, signature }) => ` ${namespace} ${command.name() === defaultCommand ? signature.slice(defaultCommand.length).trim() : signature} ${command.description()}`),
556
564
  ...(examples.length
557
565
  ? ['', '示例:', ...examples.map(example => ` ${namespace} ${example}`)]
558
566
  : []),
package/dist/script.js CHANGED
@@ -208,7 +208,8 @@ export async function executeScript(code, options = {}) {
208
208
  session: sessionId,
209
209
  ...args[2],
210
210
  });
211
- const optionsOnly = OPTIONS_ONLY.has(method) || (method === 'browserRelease' && args.length < 2);
211
+ const optionsOnly = (OPTIONS_ONLY.has(method) && !(method === 'browserList' && args[0] &&
212
+ ('provider' in args[0] || 'launchForm' in args[0]))) || (method === 'browserRelease' && args.length < 2);
212
213
  return invoke(METHOD_ACTIONS.get(method), optionsOnly ? {} : (args[0] ?? {}), optionsOnly ? args[0] : args[1]);
213
214
  })
214
215
  .then(result => send({ type: 'response', id, result, session }), error => send({ type: 'response', id, error: serializeError(error), session }))
package/dist/socket.js CHANGED
@@ -13,13 +13,13 @@ import os from 'node:os';
13
13
  import path from 'node:path';
14
14
  import { CLIENT_VERSION } from './runtimePaths.js';
15
15
  import { StringDecoder } from 'node:string_decoder';
16
- import { ensureProviderControlHost, listInstalledProviders } from '@mearl/provider/host';
17
- import { MEARL_PROVIDER_PROTOCOL_VERSION } from '@mearl/provider';
18
- import { readLiveProvider } from '@mearl/provider/runtime';
16
+ import { assertProviderControlSessionMode, ensureProviderControlHost, listInstalledProviders } from '@mearl/provider/host';
17
+ import { MEARL_PROVIDER_PROTOCOL_VERSION, assertProviderProtocolVersion, validateProviderLaunchForm } from '@mearl/provider';
18
+ import { readLiveProvider, ensureInstalledProvidersRunning } from '@mearl/provider/runtime';
19
19
  import { resolveFileRefs } from './fileRefs.js';
20
20
  import { listLiveBrowsers, findBrowser, describeBrowser, } from './browserRegistry.js';
21
21
  import { resolveActionTimeoutSec } from './actionTimeouts.js';
22
- import { isBrowserCapabilityAction } from '@mearl/browser-core';
22
+ import { isBrowserCapabilityAction, parseBrowserListParams } from '@mearl/browser-core';
23
23
  export { CLIENT_VERSION };
24
24
  const MAX_SOCKET_BUFFER_SIZE = 64 * 1024 * 1024;
25
25
  /**
@@ -376,6 +376,29 @@ export function invoke(action, data = {}, options = {}) {
376
376
  const resolvedData = resolveFileRefs(data);
377
377
  Object.defineProperty(resolvedData, SESSION_OPTION, { value: options.controlSessionId });
378
378
  if (action === 'browser_list') {
379
+ const discovery = parseBrowserListParams(resolvedData);
380
+ if (discovery) {
381
+ return (async () => {
382
+ if (transport || options.socketPath || controlSource !== 'local' || options.controlSessionId)
383
+ throw new Error('Provider discovery requires the local source without a transport, socket or session override');
384
+ const provider = listInstalledProviders().find(provider => provider.providerId === discovery.provider);
385
+ if (!provider?.browserType.dynamicLaunchForm)
386
+ throw new Error(`Provider does not support dynamic launch forms: ${discovery.provider}`);
387
+ const warnings = await ensureInstalledProvidersRunning([discovery.provider]);
388
+ if (warnings.length)
389
+ throw new Error(warnings.join('; '));
390
+ const live = readLiveProvider(discovery.provider);
391
+ if (!live)
392
+ throw new Error(`Provider did not start: ${discovery.provider}`);
393
+ assertProviderProtocolVersion(live.providerProtocolVersion, discovery.provider);
394
+ const form = await invokeOnce('browser_providers', { operation: 'launch_form', ...discovery.launchForm }, timeoutSec, undefined, live.socketPath, 'local');
395
+ validateProviderLaunchForm(form);
396
+ return decorateBrowserListResult({
397
+ ...browserListFromProviderRecords(listLiveBrowsers().filter(browser => browser.provider?.id === discovery.provider)),
398
+ discovery: { provider: discovery.provider, launchForm: form },
399
+ });
400
+ })();
401
+ }
379
402
  return dispatchInvoke(action, resolvedData, timeoutSec, transport, undefined, controlSource, options.socketPath).then(result => decorateBrowserListResult(transport === 'cdp' ? cdpOnlyBrowserList(result) : result));
380
403
  }
381
404
  const selector = browser ?? defaultBrowser ?? process.env.MEARL_BROWSER ?? undefined;
@@ -510,7 +533,7 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
510
533
  throw new Error(`Browser ${describeBrowser(match)} does not support ${action}`);
511
534
  }
512
535
  try {
513
- return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.transport === 'provider' ? match.browserId : undefined);
536
+ return await invokeOnce(action, data, timeoutSec, transport, match.socketPath, controlSource, match.transport === 'provider' ? match.browserId : undefined, match.capabilities?.controlSessionMode);
514
537
  }
515
538
  catch (error) {
516
539
  if (isConnError(error) || isSocketSandboxError(error)) {
@@ -527,7 +550,7 @@ async function dispatchInvoke(action, data, timeoutSec, transport, selector, con
527
550
  if (candidates.length > 0) {
528
551
  const target = candidates[0];
529
552
  try {
530
- return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
553
+ return await invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined, target.capabilities?.controlSessionMode);
531
554
  }
532
555
  catch (error) {
533
556
  if (isSocketSandboxError(error)) {
@@ -575,7 +598,7 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
575
598
  !(transport === 'cdp' && browser.transport === 'provider') &&
576
599
  supportsAction(browser, action, data));
577
600
  if (target) {
578
- return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined);
601
+ return invokeOnce(action, data, timeoutSec, transport, target.socketPath, controlSource, target.transport === 'provider' ? target.browserId : undefined, target.capabilities?.controlSessionMode);
579
602
  }
580
603
  return invokeViaLegacy(action, data, timeoutSec, transport, controlSource,
581
604
  /* isRetry */ true, localOnly);
@@ -586,9 +609,10 @@ async function invokeViaLegacy(action, data, timeoutSec, transport, controlSourc
586
609
  throw new Error(formatConnectError(err, SOCKET_PATH), { cause: error });
587
610
  }
588
611
  }
589
- async function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerTarget) {
612
+ async function invokeOnce(action, data, timeoutSec, transport, socketPath, controlSource, providerTarget, controlSessionMode) {
590
613
  if (providerTarget) {
591
614
  const host = await ensureProviderControlHost(Math.min(timeoutSec * 1000, 10_000));
615
+ assertProviderControlSessionMode(host, controlSessionMode);
592
616
  socketPath = host.socketPath;
593
617
  }
594
618
  const id = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
package/dist/unified.js CHANGED
@@ -3,7 +3,7 @@ import { stageFile, exportArtifact } from './artifacts.js';
3
3
  import WebSocket from 'ws';
4
4
  import { DEFAULT_CONNECT_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_REQUEST_TIMEOUT, MAX_BUFFER_SIZE, isHeartbeatMessage, resolveCloudConnectorSelector, } from '@mearl/cloud-types';
5
5
  import { resolveActionTimeoutSec } from './actionTimeouts.js';
6
- import { validateRequiredActionTabId, } from '@mearl/browser-core';
6
+ import { validateRequiredActionTabId, parseBrowserListParams, } from '@mearl/browser-core';
7
7
  import { invoke as invokeLocalBrowser, resolveLocalActionTarget } from './socket.js';
8
8
  import { loadCloudServerConfig, loadLocalConnectorId } from './unifiedConfig.js';
9
9
  const RESPONSE_GRACE_MS = 1_000;
@@ -146,6 +146,9 @@ export class UnifiedClient {
146
146
  }, options);
147
147
  }
148
148
  async invoke(action, data = {}, options = {}) {
149
+ const discovery = action === 'browser_list' ? parseBrowserListParams(data) : undefined;
150
+ if (discovery && (!options.local || options.connector || options.transport || options.session || options.controlSessionId))
151
+ throw new Error('Provider discovery requires explicit --local without connector, transport, socket or session overrides');
149
152
  if (action === 'plugin_state') {
150
153
  const { PluginStateClient } = await import('./plugins/clients/pluginState.js');
151
154
  const state = new PluginStateClient();
@@ -350,6 +353,12 @@ export class UnifiedClient {
350
353
  if (action === 'connector_list')
351
354
  return this.listConnectors(timeoutSec);
352
355
  if (action === 'browser_list') {
356
+ if (parseBrowserListParams(data)) {
357
+ const result = await invokeLocalBrowser('browser_list', data, { timeoutSec, controlSource: 'local' });
358
+ if (!result?.discovery)
359
+ throw new Error('Provider launch-form discovery is not supported by this host');
360
+ return this.applyConfiguredDefault(this.decorateListing(result, { kind: 'local' }));
361
+ }
353
362
  if (options.local || options.transport === 'cdp') {
354
363
  return this.listLocalBrowsers(timeoutSec, options.transport);
355
364
  }
@@ -543,6 +552,7 @@ export class UnifiedClient {
543
552
  })
544
553
  : null,
545
554
  browsers,
555
+ ...(result?.discovery ? { discovery: result.discovery } : {}),
546
556
  ...(resultWarnings.length > 0 ? { warnings: resultWarnings } : {}),
547
557
  };
548
558
  }
@@ -6,3 +6,7 @@ export declare function loadCliAutoUpdateIntervalDays(configPath?: string): numb
6
6
  export declare function getCloudServerConfigPath(): string;
7
7
  export declare function loadCloudServerConfig(): CloudServerConfig | null;
8
8
  export declare function loadLocalConnectorId(): string | null;
9
+ export declare function resolveServer(explicit?: string): {
10
+ serverUrl?: string;
11
+ serverUrlSource?: string;
12
+ };
@@ -56,3 +56,14 @@ export function loadLocalConnectorId() {
56
56
  return null;
57
57
  }
58
58
  }
59
+ export function resolveServer(explicit) {
60
+ if (explicit)
61
+ return { serverUrl: explicit, serverUrlSource: '--server' };
62
+ if (process.env.MEARL_SERVER_URL) {
63
+ return { serverUrl: process.env.MEARL_SERVER_URL, serverUrlSource: 'MEARL_SERVER_URL' };
64
+ }
65
+ const config = loadCloudServerConfig();
66
+ return config?.server
67
+ ? { serverUrl: config.server, serverUrlSource: getCloudServerConfigPath() }
68
+ : {};
69
+ }