@szc-ft/mcp-szcd-client 0.29.0 → 0.31.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/opencode-extension/skills/local-browser-test/SKILL.md +253 -8
- package/opencode-extension/skills/local-browser-test/decrypt-creds.mjs +210 -0
- package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +701 -17
- package/opencode-extension/skills/local-browser-test/lib/cdp-browser-config.js +115 -0
- package/opencode-extension/skills/local-browser-test/lib/decrypt-passwords.js +210 -0
- package/opencode-extension/skills/local-browser-test/lib/shared-deps.js +2 -2
- package/opencode-extension/skills/local-browser-test/local-browser-executor.js +50 -3
- package/package.json +1 -1
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +253 -8
- package/qwen-extension/skills/local-browser-test/decrypt-creds.mjs +210 -0
- package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +701 -17
- package/qwen-extension/skills/local-browser-test/lib/cdp-browser-config.js +115 -0
- package/qwen-extension/skills/local-browser-test/lib/decrypt-passwords.js +210 -0
- package/qwen-extension/skills/local-browser-test/lib/shared-deps.js +2 -2
- package/qwen-extension/skills/local-browser-test/local-browser-executor.js +50 -3
- package/standard-skill/local-browser-test/SKILL.md +253 -8
- package/standard-skill/local-browser-test/decrypt-creds.mjs +210 -0
- package/standard-skill/local-browser-test/lib/browser-engine.js +701 -17
- package/standard-skill/local-browser-test/lib/cdp-browser-config.js +115 -0
- package/standard-skill/local-browser-test/lib/decrypt-passwords.js +210 -0
- package/standard-skill/local-browser-test/lib/shared-deps.js +2 -2
- package/standard-skill/local-browser-test/local-browser-executor.js +50 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP 方式获取浏览器运行配置
|
|
3
|
+
* 通过 Chrome DevTools Protocol 获取浏览器命令行参数,解析 user-data-dir 等配置
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 通过 wmic 获取 msedge.exe 的完整命令行,解析关键参数
|
|
10
|
+
* @param {"msedge"|"chrome"} browserName
|
|
11
|
+
* @returns {{ userDataDir: string, remoteDebuggingPort: string, args: string[] }}
|
|
12
|
+
*/
|
|
13
|
+
export function getBrowserConfigFromProcess(browserName = 'msedge') {
|
|
14
|
+
const exeName = browserName === 'chrome' ? 'chrome.exe' : 'msedge.exe';
|
|
15
|
+
// 方案1:wmic /format:list 获取命令行
|
|
16
|
+
let output = '';
|
|
17
|
+
try {
|
|
18
|
+
const raw = execSync(
|
|
19
|
+
`wmic process where "name='${exeName}'" get commandline /format:list`,
|
|
20
|
+
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
|
21
|
+
);
|
|
22
|
+
// 解析 key=value 格式(每行 CommandLine=...,空格行分隔进程)
|
|
23
|
+
const blocks = raw.split(/\r\n\r\n/);
|
|
24
|
+
output = blocks
|
|
25
|
+
.map(b => {
|
|
26
|
+
const m = b.match(/CommandLine=(.+)/s);
|
|
27
|
+
return m ? m[1].trim() : '';
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join('\n');
|
|
31
|
+
} catch {
|
|
32
|
+
// 方案2:PowerShell Get-CimInstance
|
|
33
|
+
try {
|
|
34
|
+
const psScript = `Get-CimInstance Win32_Process -Filter 'Name="${exeName}"' | Where-Object { $_.CommandLine -like '*--remote-debugging-port*' } | ForEach-Object { $_.CommandLine }`;
|
|
35
|
+
output = execSync(
|
|
36
|
+
`powershell -NoProfile -Command "${psScript.replace(/"/g, '\\"')}"`,
|
|
37
|
+
{ encoding: 'utf8', timeout: 8000, windowsHide: true }
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 解析命令行参数
|
|
45
|
+
const lines = output.split('\n').filter(l => l.includes('--remote-debugging-port') && l.trim());
|
|
46
|
+
if (lines.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const cmdLine = lines[0].trim();
|
|
49
|
+
|
|
50
|
+
// 提取关键参数
|
|
51
|
+
const config = {
|
|
52
|
+
userDataDir: null,
|
|
53
|
+
remoteDebuggingPort: null,
|
|
54
|
+
args: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// 解析 --user-data-dir="path" 或 --user-data-dir=path 或 --user-data-dir path
|
|
58
|
+
const uddMatch = cmdLine.match(/--user-data-dir[= ]"?([^ "]+)"?/i);
|
|
59
|
+
if (uddMatch) {
|
|
60
|
+
config.userDataDir = uddMatch[1].replace(/\\/g, '/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 解析 --remote-debugging-port
|
|
64
|
+
const rdpMatch = cmdLine.match(/--remote-debugging-port[= ](\d+)/i);
|
|
65
|
+
if (rdpMatch) {
|
|
66
|
+
config.remoteDebuggingPort = rdpMatch[1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 返回所有参数
|
|
70
|
+
const argMatches = cmdLine.match(/--[\w-]+(?:[= ][^ "]+)?/g);
|
|
71
|
+
if (argMatches) {
|
|
72
|
+
config.args = argMatches.map(a => a.trim());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 通过 CDP 连接获取浏览器版本信息
|
|
80
|
+
* @param {string} cdpUrl - CDP 地址,默认 http://localhost:9222
|
|
81
|
+
*/
|
|
82
|
+
export async function getBrowserInfoFromCDP(cdpUrl = 'http://localhost:9222') {
|
|
83
|
+
try {
|
|
84
|
+
const resp = await fetch(`${cdpUrl}/json/version`);
|
|
85
|
+
const data = await resp.json();
|
|
86
|
+
return {
|
|
87
|
+
browser: data.Browser || data.browser,
|
|
88
|
+
userAgent: data['User-Agent'],
|
|
89
|
+
protocolVersion: data['Protocol-Version'],
|
|
90
|
+
webSocketDebuggerUrl: data.webSocketDebuggerUrl,
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 综合获取浏览器完整配置
|
|
99
|
+
* 先通过进程命令行获取 user-data-dir,再通过 CDP 获取版本信息
|
|
100
|
+
*/
|
|
101
|
+
export async function getFullBrowserConfig(cdpUrl = 'http://localhost:9222') {
|
|
102
|
+
const procConfig = getBrowserConfigFromProcess();
|
|
103
|
+
const browserInfo = await getBrowserInfoFromCDP(cdpUrl);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...procConfig,
|
|
107
|
+
browserInfo,
|
|
108
|
+
loginDataPath: procConfig?.userDataDir
|
|
109
|
+
? join(procConfig.userDataDir, 'Default', 'Login Data')
|
|
110
|
+
: null,
|
|
111
|
+
localStatePath: procConfig?.userDataDir
|
|
112
|
+
? join(procConfig.userDataDir, 'Local State')
|
|
113
|
+
: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 Edge/Chrome 的 Login Data 数据库中解密保存的密码
|
|
3
|
+
*
|
|
4
|
+
* 支持 v10 格式(使用 AES-256-GCM + DPAPI 加密密钥)
|
|
5
|
+
* 不支持 v20 格式(App-Bound Encryption,需在浏览器进程内解密)
|
|
6
|
+
*
|
|
7
|
+
* 依赖:
|
|
8
|
+
* - sql.js(纯 JS SQLite,npm install sql.js)
|
|
9
|
+
* - Node.js 内置 crypto
|
|
10
|
+
* - Windows DPAPI(通过 PowerShell 子进程)
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
13
|
+
import { createDecipheriv } from 'crypto';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import { join, dirname } from 'path';
|
|
16
|
+
import { createRequire } from 'module';
|
|
17
|
+
|
|
18
|
+
// sql.js 在共享缓存 ~/.szcd-mcp/deps/node_modules/ 中
|
|
19
|
+
// ESM 的 NODE_PATH 不支持,用 createRequire 兼容
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const initSqlJs = require('sql.js');
|
|
22
|
+
|
|
23
|
+
// ============ DPAPI 解密(通过 PowerShell) ============
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 使用 Windows DPAPI 解密数据
|
|
27
|
+
* 通过 PowerShell 调用 System.Security.Cryptography.ProtectedData
|
|
28
|
+
*/
|
|
29
|
+
export function dpapiDecrypt(encryptedBuffer) {
|
|
30
|
+
const tmpDir = process.env.TEMP || process.env.TMP || '/tmp';
|
|
31
|
+
const tmpIn = join(tmpDir, '_dpapi_in.bin');
|
|
32
|
+
const tmpOut = join(tmpDir, '_dpapi_out.bin');
|
|
33
|
+
|
|
34
|
+
writeFileSync(tmpIn, encryptedBuffer);
|
|
35
|
+
|
|
36
|
+
// PowerShell 脚本:读文件 → DPAPI 解密 → 写文件
|
|
37
|
+
const psScript = [
|
|
38
|
+
'Add-Type -AssemblyName System.Security',
|
|
39
|
+
`$in=[System.IO.File]::ReadAllBytes('${tmpIn.replace(/\\/g, '\\\\')}')`,
|
|
40
|
+
`$out=[System.Security.Cryptography.ProtectedData]::Unprotect($in,$null,'CurrentUser')`,
|
|
41
|
+
`[System.IO.File]::WriteAllBytes('${tmpOut.replace(/\\/g, '\\\\')}',$out)`,
|
|
42
|
+
].join(';');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
execSync(`powershell -NoProfile -Command "${psScript}"`, {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
timeout: 10000,
|
|
49
|
+
});
|
|
50
|
+
return readFileSync(tmpOut);
|
|
51
|
+
} finally {
|
|
52
|
+
try { unlinkSync(tmpIn); } catch {}
|
|
53
|
+
try { unlinkSync(tmpOut); } catch {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// ============ 浏览器配置检测 ============
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取 Edge/Chrome 的 user-data-dir 配置
|
|
62
|
+
* 通过解析进程命令行参数
|
|
63
|
+
*/
|
|
64
|
+
export function detectUserDataDir(browserName = 'msedge') {
|
|
65
|
+
const exeName = browserName === 'chrome' ? 'chrome.exe' : 'msedge.exe';
|
|
66
|
+
|
|
67
|
+
let output = '';
|
|
68
|
+
try {
|
|
69
|
+
output = execSync(
|
|
70
|
+
`wmic process where "name='${exeName}'" get commandline /format:csv`,
|
|
71
|
+
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
|
72
|
+
);
|
|
73
|
+
} catch {
|
|
74
|
+
try {
|
|
75
|
+
output = execSync(
|
|
76
|
+
`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='${exeName}'\\" | Where-Object {\\$_.CommandLine -like '*--remote-debugging-port*'} | Select-Object -ExpandProperty CommandLine"`,
|
|
77
|
+
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = output.split('\n').filter(l => l.includes('--remote-debugging-port'));
|
|
85
|
+
if (lines.length === 0) return null;
|
|
86
|
+
|
|
87
|
+
const match = lines[0].match(/--user-data-dir[= ]"?([^ "]+)"?/i);
|
|
88
|
+
return match ? match[1].replace(/\\/g, '/') : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// ============ AES-256-GCM 解密(v10 格式) ============
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 解密 v10 格式的密码
|
|
96
|
+
* 格式: "v10" (3 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes)
|
|
97
|
+
*/
|
|
98
|
+
function decryptV10Password(aesKey, encryptedBlob) {
|
|
99
|
+
const raw = Buffer.from(encryptedBlob);
|
|
100
|
+
if (raw.length < 31) throw new Error('v10 blob too short');
|
|
101
|
+
|
|
102
|
+
const nonce = raw.subarray(3, 15); // 12 bytes after v10
|
|
103
|
+
const tag = raw.subarray(raw.length - 16); // last 16 bytes
|
|
104
|
+
const ciphertext = raw.subarray(15, raw.length - 16);
|
|
105
|
+
|
|
106
|
+
const decipher = createDecipheriv('aes-256-gcm', aesKey, nonce);
|
|
107
|
+
decipher.setAuthTag(tag);
|
|
108
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// ============ 主入口 ============
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 从浏览器配置目录解密所有保存的密码
|
|
116
|
+
* @param {object} options
|
|
117
|
+
* @param {string} [options.userDataDir] - Edge/Chrome 的 user-data-dir 路径,不传则自动检测
|
|
118
|
+
* @param {string} [options.filter] - 过滤 URL 的关键词,默认 '%'(全部)
|
|
119
|
+
* @param {boolean} [options.verbose] - 是否输出详细信息
|
|
120
|
+
* @returns {Array<{url: string, username: string, password: string}>}
|
|
121
|
+
*/
|
|
122
|
+
export async function decryptAllPasswords(options = {}) {
|
|
123
|
+
const {
|
|
124
|
+
userDataDir = detectUserDataDir(),
|
|
125
|
+
filter = '%',
|
|
126
|
+
verbose = false,
|
|
127
|
+
} = options;
|
|
128
|
+
|
|
129
|
+
if (!userDataDir) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
'未检测到浏览器 user-data-dir。请确保 Edge/Chrome 以 --remote-debugging-port 和 --user-data-dir 参数启动'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const localStatePath = join(userDataDir, 'Local State');
|
|
136
|
+
const loginDataPath = join(userDataDir, 'Default', 'Login Data');
|
|
137
|
+
|
|
138
|
+
if (!existsSync(localStatePath)) {
|
|
139
|
+
throw new Error(`Local State 文件不存在: ${localStatePath}`);
|
|
140
|
+
}
|
|
141
|
+
if (!existsSync(loginDataPath)) {
|
|
142
|
+
throw new Error(`Login Data 文件不存在: ${loginDataPath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (verbose) {
|
|
146
|
+
console.log(`[decrypt] userDataDir: ${userDataDir}`);
|
|
147
|
+
console.log(`[decrypt] Login Data: ${loginDataPath}`);
|
|
148
|
+
console.log(`[decrypt] Local State: ${localStatePath}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 1. 读取 Local State 获取加密密钥
|
|
152
|
+
const localState = JSON.parse(readFileSync(localStatePath, 'utf8'));
|
|
153
|
+
if (!localState.os_crypt?.encrypted_key) {
|
|
154
|
+
throw new Error('Local State 中未找到 os_crypt.encrypted_key');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const encKeyBuf = Buffer.from(localState.os_crypt.encrypted_key, 'base64');
|
|
158
|
+
if (encKeyBuf.length < 5) throw new Error('加密密钥格式异常');
|
|
159
|
+
|
|
160
|
+
// 去除 "DPAPI" 前缀(5 字节)
|
|
161
|
+
const aesKey = dpapiDecrypt(encKeyBuf.subarray(5));
|
|
162
|
+
if (verbose) console.log(`[decrypt] AES key: ${aesKey.length} bytes`);
|
|
163
|
+
|
|
164
|
+
// 2. 读取 SQLite 数据库
|
|
165
|
+
const SQL = await initSqlJs();
|
|
166
|
+
const db = new SQL.Database(readFileSync(loginDataPath));
|
|
167
|
+
const rows = db.exec(
|
|
168
|
+
`SELECT origin_url, username_value, password_value FROM logins WHERE origin_url LIKE '${filter}'`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const credentials = [];
|
|
172
|
+
const values = rows[0]?.values || [];
|
|
173
|
+
|
|
174
|
+
for (const [url, username, pwdBlob] of values) {
|
|
175
|
+
if (!pwdBlob || pwdBlob.length < 3) continue;
|
|
176
|
+
|
|
177
|
+
const raw = Buffer.from(pwdBlob);
|
|
178
|
+
const prefix = raw.subarray(0, 3).toString();
|
|
179
|
+
|
|
180
|
+
if (prefix === 'v10') {
|
|
181
|
+
try {
|
|
182
|
+
const password = decryptV10Password(aesKey, pwdBlob).toString('utf8');
|
|
183
|
+
credentials.push({ url, username, password });
|
|
184
|
+
if (verbose) console.log(`[decrypt] ✓ ${username} @ ${url}`);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (verbose) console.log(`[decrypt] ✗ ${username}: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
} else if (verbose) {
|
|
189
|
+
console.log(`[decrypt] ⊘ ${username}: unsupported format (${prefix})`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
db.close();
|
|
194
|
+
return credentials;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取指定 URL 的凭据(便捷方法)
|
|
199
|
+
* @param {string} urlPattern - URL 匹配模式
|
|
200
|
+
* @param {object} [options]
|
|
201
|
+
* @returns {Promise<{username: string, password: string}|null>}
|
|
202
|
+
*/
|
|
203
|
+
export async function getCredential(urlPattern, options = {}) {
|
|
204
|
+
const creds = await decryptAllPasswords({
|
|
205
|
+
...options,
|
|
206
|
+
filter: `%${urlPattern}%`,
|
|
207
|
+
verbose: false,
|
|
208
|
+
});
|
|
209
|
+
return creds.length > 0 ? creds[0] : null;
|
|
210
|
+
}
|
|
@@ -13,8 +13,8 @@ import fs from "node:fs";
|
|
|
13
13
|
/** 共享缓存目录 */
|
|
14
14
|
const SHARED_DEPS_DIR = path.join(homedir(), ".szcd-mcp", "deps");
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
const CORE_PACKAGES = ["puppeteer-core"];
|
|
16
|
+
/** 核心浏览器自动化依赖(主链路 observe/act/assert + 凭据解密) */
|
|
17
|
+
const CORE_PACKAGES = ["puppeteer-core", "sql.js"];
|
|
18
18
|
|
|
19
19
|
/** 视觉对比依赖 */
|
|
20
20
|
const VISUAL_COMPARE_PACKAGES = ["pixelmatch", "pngjs", "sharp"];
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* node local-browser-executor.js --action act --menu-path "目录管理/数据集目录" --observe-after
|
|
9
9
|
* node local-browser-executor.js --action api-capture --wait 15000 --include-response-body
|
|
10
10
|
* node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0
|
|
11
|
+
* node local-browser-executor.js --action evaluate --expression "(() => ({a:1}))()" --frame-index 1
|
|
11
12
|
* node local-browser-executor.js --plan '<JSON>' --output /tmp/result.json
|
|
12
13
|
* node local-browser-executor.js --plan-file /path/to/plan.json --output /tmp/result.json
|
|
13
14
|
*
|
|
14
15
|
* 参数:
|
|
15
|
-
* --action 直接执行动作,当前支持 observe/act/assert/api-capture
|
|
16
|
+
* --action 直接执行动作,当前支持 observe/act/assert/evaluate/api-capture/assert-api/run-task/test-case/plan
|
|
16
17
|
* --plan 测试计划 JSON 字符串
|
|
17
18
|
* --plan-file 测试计划 JSON 文件路径(与 --plan 二选一)
|
|
18
19
|
* --output 结果输出文件路径(默认 stdout)
|
|
@@ -228,6 +229,15 @@ function parseArgs() {
|
|
|
228
229
|
case "--test-report":
|
|
229
230
|
parsed.testReport = args[++i];
|
|
230
231
|
break;
|
|
232
|
+
case "--expression":
|
|
233
|
+
parsed.expression = args[++i];
|
|
234
|
+
break;
|
|
235
|
+
case "--expect-result-contains":
|
|
236
|
+
parsed.expectResultContains = args[++i];
|
|
237
|
+
break;
|
|
238
|
+
case "--fail-on-falsy":
|
|
239
|
+
parsed.failOnFalsy = true;
|
|
240
|
+
break;
|
|
231
241
|
case "--help":
|
|
232
242
|
case "-h":
|
|
233
243
|
printHelp();
|
|
@@ -249,11 +259,12 @@ function printHelp() {
|
|
|
249
259
|
node local-browser-executor.js --action api-capture --wait 15000 --include-response-body [options]
|
|
250
260
|
node local-browser-executor.js --action assert-api --capture-file /tmp/browser-api-capture.json --max-failed 0 [options]
|
|
251
261
|
node local-browser-executor.js --action run-task --task menu-api-scan --capture-input-file /tmp/menu-tree.json [options]
|
|
262
|
+
node local-browser-executor.js --action evaluate --expression "(() => ({a:1}))()" [options]
|
|
252
263
|
node local-browser-executor.js --plan '<JSON>' [options]
|
|
253
264
|
node local-browser-executor.js --plan-file <path> [options]
|
|
254
265
|
|
|
255
266
|
选项:
|
|
256
|
-
--action <name> 直接执行动作,当前支持 observe/act/assert/api-capture/assert-api/run-task
|
|
267
|
+
--action <name> 直接执行动作,当前支持 observe/act/assert/evaluate/api-capture/assert-api/run-task/test-case/plan
|
|
257
268
|
--plan <json> 测试计划 JSON 字符串
|
|
258
269
|
--plan-file <path> 测试计划 JSON 文件路径
|
|
259
270
|
--output <path> 结果输出文件路径(默认 stdout)
|
|
@@ -274,6 +285,9 @@ function printHelp() {
|
|
|
274
285
|
--menu-path <a/b> act 时按菜单路径展开并点击,如 "目录管理/数据集目录"
|
|
275
286
|
--wait-after-each <ms> menu-path 每步点击后的等待时间(默认 300)
|
|
276
287
|
--observe-after act 后再次 observe,返回 before/act/after 闭环结果
|
|
288
|
+
--expression <js> evaluate 时执行的 JS 表达式(需用 IIFE 包裹)
|
|
289
|
+
--expect-result-contains <text> evaluate 时期望结果包含的文本
|
|
290
|
+
--fail-on-falsy evaluate 时结果为 falsy 则标记失败
|
|
277
291
|
--url-contains-assert <text> assert 当前 URL 包含文本
|
|
278
292
|
--text-contains <text> assert 页面文本包含内容
|
|
279
293
|
--expect-visible assert selector 可见
|
|
@@ -493,6 +507,34 @@ async function executeAction(args) {
|
|
|
493
507
|
});
|
|
494
508
|
return { type: "actObserve", act: actResult, after, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
495
509
|
}
|
|
510
|
+
case "evaluate": {
|
|
511
|
+
// 先通过 _resolveObserveFrame 设置 _activeFrame,使 safeEval 在正确的 frame 中执行
|
|
512
|
+
const resolved = await engine._resolveObserveFrame({
|
|
513
|
+
frameUrlContains: args.frameUrlContains,
|
|
514
|
+
frameContentContains: args.frameContentContains,
|
|
515
|
+
frameIndex: args.frameIndex,
|
|
516
|
+
});
|
|
517
|
+
engine._activeFrame = resolved.frame;
|
|
518
|
+
const evalResult = await engine.safeEval(args.expression);
|
|
519
|
+
const evalPassed = args.failOnFalsy ? !!evalResult : true;
|
|
520
|
+
const containsMatch = args.expectResultContains
|
|
521
|
+
? JSON.stringify(evalResult).includes(args.expectResultContains)
|
|
522
|
+
: true;
|
|
523
|
+
return {
|
|
524
|
+
type: "evaluate",
|
|
525
|
+
expression: args.expression,
|
|
526
|
+
result: evalResult,
|
|
527
|
+
passed: evalPassed && containsMatch,
|
|
528
|
+
frameTarget: resolved.frameId,
|
|
529
|
+
checks: {
|
|
530
|
+
failOnFalsy: args.failOnFalsy ? { expected: "truthy", actual: !!evalResult, passed: !!evalResult } : null,
|
|
531
|
+
expectResultContains: args.expectResultContains ? { expected: args.expectResultContains, passed: containsMatch } : null,
|
|
532
|
+
},
|
|
533
|
+
browserInfo: initResult,
|
|
534
|
+
startTime: startedAt,
|
|
535
|
+
endTime: new Date().toISOString(),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
496
538
|
case "assert": {
|
|
497
539
|
const expect = {};
|
|
498
540
|
if (args.expectVisible) expect.visible = true;
|
|
@@ -509,6 +551,11 @@ async function executeAction(args) {
|
|
|
509
551
|
});
|
|
510
552
|
return { type: "assert", ...result, browserInfo: initResult, startTime: startedAt, endTime: new Date().toISOString() };
|
|
511
553
|
}
|
|
554
|
+
case "plan": {
|
|
555
|
+
const plan = loadPlan(args);
|
|
556
|
+
const result = await executePlan(plan, { cdpUrl: args.cdpUrl, pageUrl: args.pageUrl || args.urlContains });
|
|
557
|
+
return { type: "plan", ...result, startTime: startedAt, endTime: new Date().toISOString() };
|
|
558
|
+
}
|
|
512
559
|
case "api-capture":
|
|
513
560
|
case "apiCapture": {
|
|
514
561
|
const result = await engine.captureApi({
|
|
@@ -628,7 +675,7 @@ async function executeAction(args) {
|
|
|
628
675
|
return {
|
|
629
676
|
error: `未知 action: ${args.action}`,
|
|
630
677
|
errorCode: "UNKNOWN_ACTION",
|
|
631
|
-
supportedActions: ["observe", "act", "assert", "api-capture", "assert-api", "run-task", "test-case"],
|
|
678
|
+
supportedActions: ["observe", "act", "assert", "evaluate", "api-capture", "assert-api", "run-task", "test-case", "plan"],
|
|
632
679
|
startTime: startedAt,
|
|
633
680
|
endTime: new Date().toISOString(),
|
|
634
681
|
};
|