@mearl/client 2.0.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 +119 -0
- package/dist/actionTimeouts.d.ts +2 -0
- package/dist/actionTimeouts.js +49 -0
- package/dist/browserRegistry.d.ts +38 -0
- package/dist/browserRegistry.js +80 -0
- package/dist/check.d.ts +1 -0
- package/dist/check.js +146 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +324 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +666 -0
- package/dist/fileRefs.d.ts +20 -0
- package/dist/fileRefs.js +52 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +125 -0
- package/dist/socket.d.ts +35 -0
- package/dist/socket.js +306 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @mearl/client
|
|
2
|
+
|
|
3
|
+
本地客户端 SDK 与 CLI,通过 **Unix Domain Socket** 与 Chrome 扩展的 native host 通信,调用浏览器调试与操作能力。
|
|
4
|
+
|
|
5
|
+
提供两种用法:类型安全的具名方法(`getRequests`、`sendRequest` 等),以及灵活的底层 `invoke(action, payload)`。
|
|
6
|
+
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @mearl/client
|
|
11
|
+
# 或
|
|
12
|
+
pnpm add @mearl/client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## SDK 使用
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import * as client from '@mearl/client';
|
|
19
|
+
|
|
20
|
+
// 具名方法(类型安全)
|
|
21
|
+
const requests = await client.getRequests({ count: 5 });
|
|
22
|
+
const result = await client.sendRequest({
|
|
23
|
+
url: 'https://api.example.com',
|
|
24
|
+
method: 'GET',
|
|
25
|
+
withCookies: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 底层通用方法(灵活扩展)
|
|
29
|
+
const data = await client.invoke('send_request', { url: '...' });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CLI 使用
|
|
33
|
+
|
|
34
|
+
安装后提供 `mearl` 命令:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 获取最近 5 个请求
|
|
38
|
+
mearl get_requests --payload '{"count":5}'
|
|
39
|
+
|
|
40
|
+
# 代理一个 HTTP 请求
|
|
41
|
+
mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
|
|
42
|
+
|
|
43
|
+
# 截图并保存
|
|
44
|
+
mearl page_screenshot --output ./screenshot.png
|
|
45
|
+
|
|
46
|
+
# 从文件读取大体积参数(如 mock 数据)
|
|
47
|
+
mearl set_mock --payload-file ./mock-data.json
|
|
48
|
+
|
|
49
|
+
# 环境自检
|
|
50
|
+
mearl check
|
|
51
|
+
|
|
52
|
+
# 查看某个 action 的帮助
|
|
53
|
+
mearl <action> --help
|
|
54
|
+
|
|
55
|
+
# 启动两个隔离的 headless Chrome,并分别登录不同 TDBank 账号
|
|
56
|
+
mearl browser_launch --payload '{"name":"account-a","accountId":12345}'
|
|
57
|
+
mearl browser_launch --payload '{"name":"account-b","query":"test_account"}'
|
|
58
|
+
mearl browser_launch --payload '{"name":"cookie-copy","copyCookieDomains":["example.com"]}'
|
|
59
|
+
mearl browser_launch --payload '{"name":"figma","userAgentMode":"desktop","copyCookieDomains":["figma.com"]}'
|
|
60
|
+
mearl browser_list
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**通用选项:**
|
|
64
|
+
|
|
65
|
+
| 选项 | 说明 |
|
|
66
|
+
| ----------------------- | ------------------------ |
|
|
67
|
+
| `--payload <json>` | 直接传入 JSON 参数 |
|
|
68
|
+
| `--payload-file <path>` | 从文件读取 JSON 参数 |
|
|
69
|
+
| `--timeout <seconds>` | 请求超时时间 |
|
|
70
|
+
| `--compact` | 紧凑输出 |
|
|
71
|
+
| `--output <path>` | 将结果写入文件(如截图) |
|
|
72
|
+
|
|
73
|
+
## 支持的操作
|
|
74
|
+
|
|
75
|
+
| 分类 | Actions |
|
|
76
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| API 调试 | `get_requests` `get_logs` `get_events` `get_api_schema` |
|
|
78
|
+
| Mock & 规则 | `set_mock` `get_mocks` `set_rule` `get_rules` |
|
|
79
|
+
| 网络代理 | `send_request` `send_mtop_request` |
|
|
80
|
+
| 标签页 | `tab_open` `tab_close` `tab_list` |
|
|
81
|
+
| 页面操作 | `page_click` `page_type` `page_scroll` `page_eval` `page_press` `page_wait` `page_navigate` `page_upload` |
|
|
82
|
+
| 页面感知 | `page_snapshot` `page_screenshot` `page_selected_element` `page_frames` |
|
|
83
|
+
| 环境模拟 | `set_device_emulation` `set_timezone` |
|
|
84
|
+
| 用户信息 | `get_user_info` |
|
|
85
|
+
| 录制 | `record_start` `record_stop` |
|
|
86
|
+
| TDBank | `tdbank_account` |
|
|
87
|
+
| 浏览器 | `browser_list` `browser_launch` `browser_close` |
|
|
88
|
+
|
|
89
|
+
页面交互动作(`page_click` / `page_type` / `page_hover` / `page_scroll` / `page_press` / `page_upload`)默认内置观察:同一次调用内执行动作并等待异步稳定,返回 `{ action, observation }`;传 `observe: false` 可关闭观察仅执行裸动作。观察结果不替代完整页面理解:`mode: "delta"` 只返回主文档中的 `effects.notifications`、`effects.interactives` 和 `effects.focus` 等高置信度信号,并明确携带 `scope: "main-document"`;`mode: "navigation"` 且 `ready: true` 时,页面已通过网络静默、骨架状态或保守的内容稳定判定,可在新页面重建快照。仅在 `fullSnapshotRecommended` 为 true 时根据 `snapshotReasons` 回退到完整快照或截图。
|
|
90
|
+
|
|
91
|
+
`page_snapshot` 默认返回完整 AX Tree;长列表可传 `mode: "viewport"`,只需要控件时传 `mode: "interactive"`,已知区域时传 `rootSelector`。页面缺少 AX 控件语义时,`interactive` 会自动回退到 viewport,并返回 `fallbackMode: "viewport"`。`maxNodes` / `maxChars` 截断会同时保留首尾内容。
|
|
92
|
+
|
|
93
|
+
`browser_list` 统一列出普通浏览器和托管浏览器。`type` 区分 `regular` / `managed`,`status` 区分 `connected` / `running_disconnected` / `stopped`;只有 `connected` 的浏览器可作为操作目标。
|
|
94
|
+
|
|
95
|
+
托管浏览器使用独立 Profile 和动态 CDP 端口。控制浏览器需先登录 TDBank;生成的 SSO 地址在本地内部传递,新实例可以使用 `headless: true`(默认)完成测试账号登录。`copyCookieDomains` 可把控制浏览器指定域的 Cookie 复制到新实例,Cookie 值不会出现在命令结果或日志中;`userAgentMode: "desktop"` 可让实例使用匹配本机 Chrome 版本的桌面 UA。完整设计见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
|
|
96
|
+
|
|
97
|
+
## 架构
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
@mearl/client (CLI / SDK)
|
|
101
|
+
↓ (Unix Socket)
|
|
102
|
+
@mearl/native-host
|
|
103
|
+
↓ (Native Messaging)
|
|
104
|
+
Chrome Extension / CDP
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> 云端远程调用场景请使用 [@mearl/cloud-client](../cloud/cloud-client),其操作集与本包完全一致。
|
|
108
|
+
|
|
109
|
+
## 构建
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm build # tsc 编译到 dist/,并赋予 cli.js 执行权限
|
|
113
|
+
pnpm dev # tsc --watch
|
|
114
|
+
pnpm typecheck # 仅类型检查
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
ISC
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function boundedNumber(value, fallback, minimum, maximum) {
|
|
2
|
+
const numeric = Number(value);
|
|
3
|
+
if (!Number.isFinite(numeric))
|
|
4
|
+
return fallback;
|
|
5
|
+
return Math.min(Math.max(numeric, minimum), maximum);
|
|
6
|
+
}
|
|
7
|
+
// 默认内置观察的页面交互动作。client 作为零依赖包本地维护,
|
|
8
|
+
// 需与 browser-core/pageAct.ts 的 PAGE_ACT_ACTIONS 保持一致。
|
|
9
|
+
const OBSERVED_PAGE_ACTIONS = new Set([
|
|
10
|
+
'page_click',
|
|
11
|
+
'page_type',
|
|
12
|
+
'page_hover',
|
|
13
|
+
'page_scroll',
|
|
14
|
+
'page_press',
|
|
15
|
+
'page_upload',
|
|
16
|
+
]);
|
|
17
|
+
export function resolveObservedActionTimeoutSec(data) {
|
|
18
|
+
const observe = data.observe ?? {};
|
|
19
|
+
const observationMs = boundedNumber(observe.timeoutMs, 3000, 500, 30000);
|
|
20
|
+
const navigationGraceMs = boundedNumber(observe.navigationGraceMs, 1000, 0, 5000);
|
|
21
|
+
const navigationTimeoutMs = boundedNumber(observe.navigationTimeoutMs, 15000, 1000, 30000);
|
|
22
|
+
const totalMs = observationMs + navigationGraceMs + 4000 + navigationTimeoutMs + 5000;
|
|
23
|
+
return Math.max(20, Math.ceil(totalMs / 1000));
|
|
24
|
+
}
|
|
25
|
+
export function resolveActionTimeoutSec(action, data, fallback = 15) {
|
|
26
|
+
if (action === 'send_request') {
|
|
27
|
+
return Math.max(fallback, Math.ceil(((data.timeout ?? 30000) + 5000) / 1000));
|
|
28
|
+
}
|
|
29
|
+
if (action === 'send_mtop_request') {
|
|
30
|
+
return Math.max(fallback, Math.ceil(((data.timeout ?? 10000) + 5000) / 1000));
|
|
31
|
+
}
|
|
32
|
+
if (action === 'page_wait') {
|
|
33
|
+
const waitMs = data.time ?? data.timeout ?? 10000;
|
|
34
|
+
return Math.max(fallback, Math.ceil((Number(waitMs) + 5000) / 1000));
|
|
35
|
+
}
|
|
36
|
+
if (action === 'browser_launch')
|
|
37
|
+
return Math.max(fallback, 90);
|
|
38
|
+
if (action === 'browser_close' || action === 'tdbank_account')
|
|
39
|
+
return Math.max(fallback, 20);
|
|
40
|
+
// 页面交互动作默认内置观察,超时需覆盖观察与导航窗口;observe:false 时是裸动作,用默认超时
|
|
41
|
+
if (OBSERVED_PAGE_ACTIONS.has(action) && data.observe !== false) {
|
|
42
|
+
return Math.max(fallback, resolveObservedActionTimeoutSec(data));
|
|
43
|
+
}
|
|
44
|
+
if (action === 'run_actions') {
|
|
45
|
+
const steps = Array.isArray(data.actions) ? data.actions.length : 1;
|
|
46
|
+
return Math.max(fallback, steps * 15);
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the shared browser registry that native-host processes publish, so
|
|
3
|
+
* the client can discover every browser that currently has the extension
|
|
4
|
+
* connected and address a specific one.
|
|
5
|
+
*
|
|
6
|
+
* Kept dependency-free (no @mearl/daemon-core) because the client ships
|
|
7
|
+
* as a standalone npm package. It only has to agree with daemon-core on the
|
|
8
|
+
* registry directory and record shape; reading scans every *.json in the dir,
|
|
9
|
+
* so it never needs to replicate daemon-core's filename hashing.
|
|
10
|
+
*/
|
|
11
|
+
export interface LiveBrowser {
|
|
12
|
+
pid: number;
|
|
13
|
+
browserId: string;
|
|
14
|
+
socketPath: string;
|
|
15
|
+
browserName: string;
|
|
16
|
+
extensionVersion?: string;
|
|
17
|
+
nativeHostVersion?: string;
|
|
18
|
+
transport?: 'extension' | 'cdp';
|
|
19
|
+
lastFocusedAt?: number;
|
|
20
|
+
updatedAt?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every live browser record, most-recently-focused first.
|
|
24
|
+
*
|
|
25
|
+
* Read-only: dead records are filtered out but never deleted here. Deleting is
|
|
26
|
+
* left to the native-hosts (they overwrite their own record on reconnect and
|
|
27
|
+
* reconcile when serving browser_list). Otherwise many concurrent clients pruning at once
|
|
28
|
+
* could, in a narrow window, delete a record a browser just re-published on
|
|
29
|
+
* reconnect — making a live browser briefly vanish from the registry.
|
|
30
|
+
*/
|
|
31
|
+
export declare function listLiveBrowsers(): LiveBrowser[];
|
|
32
|
+
/**
|
|
33
|
+
* Find the browser matching a user selector: exact browserId first, then an
|
|
34
|
+
* exact (case-insensitive) name, then a substring of id/name.
|
|
35
|
+
*/
|
|
36
|
+
export declare function findBrowser(browsers: LiveBrowser[], selector: string): LiveBrowser | null;
|
|
37
|
+
/** Human-readable one-line label for a browser, used in listings and errors. */
|
|
38
|
+
export declare function describeBrowser(b: LiveBrowser): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the shared browser registry that native-host processes publish, so
|
|
3
|
+
* the client can discover every browser that currently has the extension
|
|
4
|
+
* connected and address a specific one.
|
|
5
|
+
*
|
|
6
|
+
* Kept dependency-free (no @mearl/daemon-core) because the client ships
|
|
7
|
+
* as a standalone npm package. It only has to agree with daemon-core on the
|
|
8
|
+
* registry directory and record shape; reading scans every *.json in the dir,
|
|
9
|
+
* so it never needs to replicate daemon-core's filename hashing.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
/** Must match @mearl/daemon-core's DAEMON_HOME + the native-host 'browsers' subdir. */
|
|
15
|
+
const BROWSERS_DIR = path.join(os.homedir(), '.mearl', 'browsers');
|
|
16
|
+
function isAlive(pid) {
|
|
17
|
+
if (!pid || pid <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return error.code === 'EPERM';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every live browser record, most-recently-focused first.
|
|
29
|
+
*
|
|
30
|
+
* Read-only: dead records are filtered out but never deleted here. Deleting is
|
|
31
|
+
* left to the native-hosts (they overwrite their own record on reconnect and
|
|
32
|
+
* reconcile when serving browser_list). Otherwise many concurrent clients pruning at once
|
|
33
|
+
* could, in a narrow window, delete a record a browser just re-published on
|
|
34
|
+
* reconnect — making a live browser briefly vanish from the registry.
|
|
35
|
+
*/
|
|
36
|
+
export function listLiveBrowsers() {
|
|
37
|
+
let files;
|
|
38
|
+
try {
|
|
39
|
+
files = fs.readdirSync(BROWSERS_DIR).filter(name => name.endsWith('.json'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const name of files) {
|
|
46
|
+
let rec;
|
|
47
|
+
try {
|
|
48
|
+
rec = JSON.parse(fs.readFileSync(path.join(BROWSERS_DIR, name), 'utf-8'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Corrupt or mid-write (a writer may be renaming it in); skip.
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (rec && typeof rec.pid === 'number' && rec.socketPath && isAlive(rec.pid)) {
|
|
55
|
+
out.push(rec);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out.sort((a, b) => (b.lastFocusedAt ?? 0) - (a.lastFocusedAt ?? 0));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Find the browser matching a user selector: exact browserId first, then an
|
|
62
|
+
* exact (case-insensitive) name, then a substring of id/name.
|
|
63
|
+
*/
|
|
64
|
+
export function findBrowser(browsers, selector) {
|
|
65
|
+
const sel = selector.trim().toLowerCase();
|
|
66
|
+
if (!sel)
|
|
67
|
+
return null;
|
|
68
|
+
const byId = browsers.find(b => b.browserId.toLowerCase() === sel);
|
|
69
|
+
if (byId)
|
|
70
|
+
return byId;
|
|
71
|
+
const byExactName = browsers.find(b => b.browserName.toLowerCase() === sel);
|
|
72
|
+
if (byExactName)
|
|
73
|
+
return byExactName;
|
|
74
|
+
return (browsers.find(b => b.browserId.toLowerCase().includes(sel) || b.browserName.toLowerCase().includes(sel)) ?? null);
|
|
75
|
+
}
|
|
76
|
+
/** Human-readable one-line label for a browser, used in listings and errors. */
|
|
77
|
+
export function describeBrowser(b) {
|
|
78
|
+
const idShort = b.browserId.length > 12 ? `${b.browserId.slice(0, 8)}…` : b.browserId;
|
|
79
|
+
return `${b.browserName} [${idShort}]`;
|
|
80
|
+
}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCheck(): Promise<void>;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { invoke, CLIENT_VERSION, SOCKET_PATH } from './socket.js';
|
|
6
|
+
import { listLiveBrowsers, describeBrowser } from './browserRegistry.js';
|
|
7
|
+
const EXTENSION_INSTALL_URL = 'https://chromewebstore.google.com/detail/mearl/aoehhjnofngknnjefamjbplchbolghkm';
|
|
8
|
+
function resolveManifestDir() {
|
|
9
|
+
switch (process.platform) {
|
|
10
|
+
case 'darwin':
|
|
11
|
+
return path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
|
|
12
|
+
case 'linux':
|
|
13
|
+
return path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts');
|
|
14
|
+
case 'win32':
|
|
15
|
+
return path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Google\\Chrome\\NativeMessagingHosts');
|
|
16
|
+
default:
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function checkCommandExists(command) {
|
|
21
|
+
try {
|
|
22
|
+
return execSync(`${command} --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function runCheck() {
|
|
29
|
+
const env = [];
|
|
30
|
+
// 1. native-host CLI on PATH
|
|
31
|
+
const cliVersion = checkCommandExists('mearl-native-host');
|
|
32
|
+
env.push(cliVersion
|
|
33
|
+
? { ok: true, label: 'native-host CLI' }
|
|
34
|
+
: {
|
|
35
|
+
ok: false,
|
|
36
|
+
label: 'native-host CLI not found',
|
|
37
|
+
fix: ['npm install -g @mearl/native-host'],
|
|
38
|
+
});
|
|
39
|
+
// 2. Native Messaging manifest
|
|
40
|
+
const manifestDir = resolveManifestDir();
|
|
41
|
+
const manifestName = 'com.alibaba.mearl.skills.json';
|
|
42
|
+
const manifestPath = manifestDir ? path.join(manifestDir, manifestName) : '';
|
|
43
|
+
env.push(manifestPath && fs.existsSync(manifestPath)
|
|
44
|
+
? { ok: true, label: 'Native Messaging manifest' }
|
|
45
|
+
: {
|
|
46
|
+
ok: false,
|
|
47
|
+
label: 'Native Messaging manifest not found',
|
|
48
|
+
fix: ['mearl-native-host --init'],
|
|
49
|
+
});
|
|
50
|
+
// 3. Socket file (Windows uses Named Pipes — assume present, probe via invoke)
|
|
51
|
+
const isWindows = process.platform === 'win32';
|
|
52
|
+
const socketExists = isWindows || fs.existsSync(SOCKET_PATH);
|
|
53
|
+
if (isWindows) {
|
|
54
|
+
env.push({ ok: true, label: `Socket (Named Pipe)` });
|
|
55
|
+
}
|
|
56
|
+
else if (socketExists) {
|
|
57
|
+
env.push({ ok: true, label: 'Socket file' });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
env.push({
|
|
61
|
+
ok: false,
|
|
62
|
+
label: `Socket file not found at ${SOCKET_PATH}`,
|
|
63
|
+
fix: [
|
|
64
|
+
'native host is not running',
|
|
65
|
+
'Extension mode: ensure Chrome is running and the extension is enabled',
|
|
66
|
+
'CDP mode: chrome://inspect/#remote-debugging → "Discover network targets"',
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// 4. Connection + version fetch
|
|
71
|
+
let runtime = null;
|
|
72
|
+
let outdatedDaemon = false;
|
|
73
|
+
if (socketExists) {
|
|
74
|
+
try {
|
|
75
|
+
runtime = (await invoke('get_versions', {}, { timeoutSec: 5 }));
|
|
76
|
+
env.push({ ok: true, label: 'Connection' });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
80
|
+
if (msg.includes('Unknown action')) {
|
|
81
|
+
// Old native-host responded but doesn't know get_versions — connection itself is fine.
|
|
82
|
+
outdatedDaemon = true;
|
|
83
|
+
env.push({ ok: true, label: 'Connection' });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
env.push({ ok: false, label: 'Connection failed', fix: [msg] });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Build versions list
|
|
91
|
+
const versions = [['client', `v${CLIENT_VERSION}`]];
|
|
92
|
+
const nhVersion = runtime?.nativeHost ?? cliVersion;
|
|
93
|
+
if (nhVersion)
|
|
94
|
+
versions.push(['native-host', `v${nhVersion}`]);
|
|
95
|
+
if (runtime?.extension) {
|
|
96
|
+
versions.push(['extension', `v${runtime.extension}`]);
|
|
97
|
+
}
|
|
98
|
+
if (runtime?.browser) {
|
|
99
|
+
versions.push(['browser', `Chrome ${runtime.browser}`]);
|
|
100
|
+
}
|
|
101
|
+
// Print Versions section
|
|
102
|
+
const nameWidth = Math.max(...versions.map(([n]) => n.length));
|
|
103
|
+
console.error('\nVersions');
|
|
104
|
+
for (const [name, value] of versions) {
|
|
105
|
+
console.error(` ${name.padEnd(nameWidth)} ${value}`);
|
|
106
|
+
}
|
|
107
|
+
console.error('');
|
|
108
|
+
// Print Environment section
|
|
109
|
+
console.error('Environment');
|
|
110
|
+
let hasError = false;
|
|
111
|
+
for (const e of env) {
|
|
112
|
+
console.error(` ${e.ok ? '✅' : '❌'} ${e.label}`);
|
|
113
|
+
if (!e.ok) {
|
|
114
|
+
hasError = true;
|
|
115
|
+
for (const fix of e.fix ?? [])
|
|
116
|
+
console.error(` ${fix}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (outdatedDaemon) {
|
|
120
|
+
console.error(' ⚠️ native-host daemon is outdated (missing get_versions). Restart Chrome or re-init: mearl-native-host --init');
|
|
121
|
+
}
|
|
122
|
+
console.error('');
|
|
123
|
+
// Connected browsers (multi-browser addressing)
|
|
124
|
+
const browsers = listLiveBrowsers();
|
|
125
|
+
if (browsers.length > 0) {
|
|
126
|
+
console.error('Connected browsers');
|
|
127
|
+
browsers.forEach((b, i) => {
|
|
128
|
+
const marker = i === 0 ? '★' : ' ';
|
|
129
|
+
const via = b.transport === 'cdp' ? ' [CDP]' : '';
|
|
130
|
+
console.error(` ${marker} ${describeBrowser(b)}${via}`);
|
|
131
|
+
});
|
|
132
|
+
if (browsers.length > 1) {
|
|
133
|
+
console.error(' ★ = default target (most-recently focused). Use --browser to pick another.');
|
|
134
|
+
}
|
|
135
|
+
console.error('');
|
|
136
|
+
}
|
|
137
|
+
// Summary
|
|
138
|
+
if (hasError) {
|
|
139
|
+
console.error('⚠️ Some checks failed. See above for fix instructions.');
|
|
140
|
+
console.error(` Install extension: ${EXTENSION_INSTALL_URL}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.error('🎉 All checks passed!');
|
|
145
|
+
}
|
|
146
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mearl CLI — 通过命令行调用 Mearl 浏览器插件能力
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* mearl <action> [--payload <json> | --payload-file <path>] [--timeout <seconds>] [--compact] [--output <path>]
|
|
7
|
+
*
|
|
8
|
+
* 示例:
|
|
9
|
+
* mearl get_requests --payload '{"count":5}'
|
|
10
|
+
* mearl send_request --payload '{"url":"https://api.example.com","method":"GET"}'
|
|
11
|
+
* mearl page_screenshot --output ./screenshot.png
|
|
12
|
+
* mearl set_mock --payload-file ./mock-data.json
|
|
13
|
+
*/
|
|
14
|
+
export {};
|