@szc-ft/mcp-szcd-client 0.30.0 → 0.32.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.
Files changed (22) hide show
  1. package/opencode-extension/skills/local-browser-test/SKILL.md +229 -9
  2. package/opencode-extension/skills/local-browser-test/decrypt-creds.mjs +210 -0
  3. package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +642 -5
  4. package/opencode-extension/skills/local-browser-test/lib/cdp-browser-config.js +115 -0
  5. package/opencode-extension/skills/local-browser-test/lib/decrypt-passwords.js +210 -0
  6. package/opencode-extension/skills/local-browser-test/lib/shared-deps.js +2 -2
  7. package/package.json +1 -1
  8. package/qwen-extension/qwen-extension.json +1 -1
  9. package/qwen-extension/skills/local-browser-test/SKILL.md +229 -9
  10. package/qwen-extension/skills/local-browser-test/decrypt-creds.mjs +210 -0
  11. package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +642 -5
  12. package/qwen-extension/skills/local-browser-test/lib/cdp-browser-config.js +115 -0
  13. package/qwen-extension/skills/local-browser-test/lib/decrypt-passwords.js +210 -0
  14. package/qwen-extension/skills/local-browser-test/lib/shared-deps.js +2 -2
  15. package/scripts/lib/dev-browser-shortcut.js +230 -0
  16. package/scripts/postinstall.js +5 -0
  17. package/standard-skill/local-browser-test/SKILL.md +229 -9
  18. package/standard-skill/local-browser-test/decrypt-creds.mjs +210 -0
  19. package/standard-skill/local-browser-test/lib/browser-engine.js +642 -5
  20. package/standard-skill/local-browser-test/lib/cdp-browser-config.js +115 -0
  21. package/standard-skill/local-browser-test/lib/decrypt-passwords.js +210 -0
  22. package/standard-skill/local-browser-test/lib/shared-deps.js +2 -2
@@ -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"];