@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"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@szc-ft/mcp-szcd-client",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "MCP client for szcd component library - auto-configures AI coding tools with MCP server, skills, agents and commands",
5
5
  "keywords": [
6
6
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "szcd-component-helper",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "szcd 组件库 MCP 助手 — 查询组件信息、匹配需求、生成代码",
5
5
  "mcpServers": {
6
6
  "szcd-component-helper": {
@@ -25,6 +25,9 @@ compatibility:
25
25
  **视觉对比(用 --action compare):**
26
26
  - "对比设计稿" / "还原度" / "像素对比"
27
27
 
28
+ **凭据管理(用 decrypt-creds.mjs):**
29
+ - "获取密码" / "解密密码" / "自动登录" / "读取浏览器保存的密码" / "接管浏览器,帮我登录"
30
+
28
31
  ## ⚡ 路径解析(必读,避免 ad-hoc 脚本)
29
32
 
30
33
  **执行器内置在 skill 目录中(自包含),会话开始时先解析 skill 路径:**
@@ -149,15 +152,22 @@ Agent / SKILL.md
149
152
  → $SKILL_DIR/lib/expect.js (validation/locator/url/api + poll)
150
153
  → puppeteer-core + CDP
151
154
  → 用户真实 Chrome / Edge
155
+
156
+ ── 凭据分支 ──
157
+ → $SKILL_DIR/decrypt-creds.mjs (CLI 入口)
158
+ → $SKILL_DIR/lib/cdp-browser-config.js (检测 user-data-dir)
159
+ → $SKILL_DIR/lib/decrypt-passwords.js (DPAPI + AES-GCM 解密)
160
+ → 输出 { username, password, url }
152
161
  ```
153
162
 
154
- 执行器 + lib 内置在 skill 目录中(自包含),`postinstall` 会把整个 skill 目录复制到各 IDE。
163
+ 执行器 + lib + 凭据解密模块内置在 skill 目录中(自包含),`postinstall` 会把整个 skill 目录复制到各 IDE。
155
164
  **禁止 AI 在用户机器上写 ad-hoc puppeteer 脚本**——会丢失:
156
165
  - 微前端 frame 探活 + 自动召回
157
166
  - `closeAllDropdowns` 跨 frame 清理
158
167
  - Ant Select VERIFY 回写
159
168
  - testCase recovery 重试
160
169
  - API 按 frame scope 过滤
170
+ - 浏览器凭据自动解密(使用内置 `decrypt-creds.mjs`)
161
171
 
162
172
  `local-browser-executor.cjs`(同目录)是 CommonJS 过渡版,仅支持基础诊断。**Ant 适配层 / testCase DSL / expect 断言仅 `local-browser-executor.js`(ES Module 版)支持。**
163
173
 
@@ -168,14 +178,25 @@ Agent / SKILL.md
168
178
  ### connect 模式(推荐)
169
179
  连接用户已启动的调试 Chrome,天然继承登录态/微前端环境。
170
180
 
181
+ **connect 时自动解密凭据**:连接浏览器后,engine 自动检测 `--user-data-dir` 并解密所有保存的密码,
182
+ 结果缓存到 `~/.szcd-mcp/deps/decrypted-creds.json`。后续可直接用 `engine._credentials` 获取。
183
+
171
184
  用户一次性配置:
185
+
186
+ > 💡 **快捷方式自动创建**:`npm install @szc-ft/mcp-szcd-client` 时,postinstall 会自动检测本机
187
+ > Chrome / Edge 并在系统应用启动器里创建一个 **"DevSession"** 快捷方式(不动主图标、独立 profile、
188
+ > 默认 9222 端口)。双击该图标即可启动,下面命令仅作 fallback / 自定义参考。
189
+ >
190
+ > 落点:Windows 桌面 `.lnk`、macOS `~/Applications/<Name> DevSession.app`、
191
+ > Linux `~/.local/share/applications/szcd-<id>-devsession.desktop`。
192
+
172
193
  ```bash
173
194
  # macOS
174
- open -a "Google Chrome" --args --remote-debugging-port=9222
195
+ open -a "Google Chrome" --args --disable-features=RendererCodeIntegrity --disable-web-security --user-data-dir="$HOME/ChromeDevSession" --remote-debugging-port=9222
175
196
  # Linux
176
- google-chrome --remote-debugging-port=9222
197
+ google-chrome --disable-features=RendererCodeIntegrity --disable-web-security --user-data-dir="$HOME/ChromeDevSession" --remote-debugging-port=9222
177
198
  # Windows
178
- "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
199
+ "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --disable-features=RendererCodeIntegrity --disable-web-security --user-data-dir="C:/EdgeDevSession" --remote-debugging-port=9222
179
200
  ```
180
201
 
181
202
  ### launch 模式(回退)
@@ -632,12 +653,12 @@ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.
632
653
  ## 依赖管理
633
654
 
634
655
  浏览器测试依赖采用**共享缓存 + 按需安装**方案:
635
- - `observe/act/assert/diagnose/screenshot` 主链路只安装 `puppeteer-core` 到 `~/.szcd-mcp/deps/`
656
+ - 主链路(`observe/act/assert/diagnose/screenshot` + 凭据解密)安装 `puppeteer-core` + `sql.js` 到 `~/.szcd-mcp/deps/`
636
657
  - 只有执行设计稿像素对比 `compare` 时才安装 `pixelmatch/pngjs/sharp`
637
658
  - 后续所有项目共享复用,无需重复安装
638
659
  - 如果核心依赖自动安装失败,引导用户手动执行:
639
660
  ```bash
640
- cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com puppeteer-core
661
+ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com puppeteer-core sql.js
641
662
  ```
642
663
  - 如果视觉对比依赖自动安装失败,引导用户手动执行:
643
664
  ```bash
@@ -720,6 +741,133 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
720
741
  ```
721
742
  `runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
722
743
 
744
+ ## 🔑 凭据解密(自动获取浏览器保存的密码)
745
+
746
+ 当测试需要登录但浏览器已保存密码时,使用内置的 `decrypt-creds.mjs` 自动从 Edge/Chrome 的密码管理器中解密凭据,**无需人工输入密码**。
747
+
748
+ ### 工作原理
749
+
750
+ ```
751
+ Agent / SKILL.md
752
+ → $SKILL_DIR/lib/cdp-browser-config.js 自动检测浏览器 user-data-dir
753
+ → $SKILL_DIR/lib/decrypt-passwords.js DPAPI + AES-GCM 解密 v10 密码
754
+ → $SKILL_DIR/decrypt-creds.mjs CLI 入口(一键调用)
755
+ → 返回 { username, password, url }
756
+ ```
757
+
758
+ ### 前置条件
759
+
760
+ - 浏览器以 `--remote-debugging-port=9222` 启动(CDP 已连接)
761
+ - 浏览器以 `--user-data-dir=<path>` 指定了自定义数据目录(开发/测试环境常见)
762
+ - 密码以 **v10 格式**存储(开发环境默认;生产环境 v20 需 Python 方案)
763
+ - skill 目录下已安装 `sql.js`:`cd $SKILL_DIR && npm install sql.js`
764
+ - 首次解密后结果自动缓存到 `~/.szcd-mcp/deps/decrypted-creds.json`,后续秒级返回
765
+
766
+ ### 使用方式
767
+
768
+ ```bash
769
+ # 自动检测浏览器配置 + 解密所有凭据(使用共享缓存依赖)
770
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos
771
+
772
+ # 首次运行:全量解密 + 写入缓存(~/.szcd-mcp/deps/decrypted-creds.json)
773
+ # 后续运行:秒级返回(自动检测 Login Data 修改时间判断缓存是否过期)
774
+
775
+ # 强制刷新缓存(如手动修改了密码)
776
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --refresh
777
+
778
+ # JSON 格式输出(供脚本解析)
779
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --json
780
+
781
+ # 手动指定 user-data-dir(跳过自动检测)
782
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --user-data-dir "C:/EdgeDevSession" --url aicityos
783
+
784
+ # 查看帮助
785
+ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --help
786
+ ```
787
+
788
+ ### 典型场景:SSO 登录自动化
789
+
790
+ **⚠️ 强制流程:解密后必须先向用户澄清使用哪一个账号登录,禁止自动选择!**
791
+
792
+ ```bash
793
+ # 1. 解密凭据(使用共享缓存依赖)
794
+ CREDS=$(NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --json)
795
+
796
+ # ❌ 错误:拿到第一个凭据就直接登录
797
+ # USER=$(echo "$CREDS" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'))[0].username")
798
+
799
+ # ✅ 正确:先把去重后的用户列表展示给用户,让用户选择
800
+ # LLM 根据解密结果,用 AskUserQuestion 列出所有可用账号供用户选择:
801
+ # "解密到以下 X 个账号,请选择使用哪一个登录:"
802
+ # - admin (密码: Jx***01#)
803
+ # - 18718576547 (密码: Jx***01#)
804
+ # - zengzhijie (密码: zen***123)
805
+ # ...
806
+ # 用户选择后,再继续后续步骤
807
+
808
+ # 2. 用户确认后,用选定的凭据构造登录 testCase
809
+ cat > /tmp/login.json <<EOF
810
+ {
811
+ "title": "自动登录",
812
+ "options": { "timeout": 30000 },
813
+ "steps": [
814
+ { "type": "antInput", "placeholder": "请输入账户", "value": "$USER" },
815
+ { "type": "antInput", "placeholder": "请输入密码", "value": "$PASS" },
816
+ { "type": "antInput", "placeholder": "请输入图形验证码", "value": "6" },
817
+ { "type": "act", "click": "text==登 录" }
818
+ ]
819
+ }
820
+ EOF
821
+
822
+ # 3. 执行登录
823
+ node "$EXECUTOR" --action plan --plan-file /tmp/login.json --url-contains "platform.aicityos.com" --wait 8000
824
+ ```
825
+
826
+ **LLM 处理流程:**
827
+ 1. 调用 `decrypt-creds.mjs --json` 获取用户列表(首次慢,后续秒级缓存命中)
828
+ 2. 对凭据去重(同一用户名取首次出现的密码)
829
+ 3. **用 `AskUserQuestion` 列出所有可用账号(只展示完整用户名,不展示密码),让用户选择**
830
+ 4. 用户选择后,从解密结果中查找对应密码,构造登录 testCase
831
+ 5. 执行登录
832
+
833
+ **⚠️ 强制约束:**
834
+ - 只展示账号列表,**不展示密码**(含脱敏也不展示)
835
+ - **展示完整账号**,不截断
836
+ - **每次登录都向用户询问**,不自动选择
837
+
838
+ **多账号切换场景(如测试不同角色权限):**
839
+ - 首次登录时一次性询问:"本次测试需要用到哪些账号?",用户可多选
840
+ - 后续切换直接用缓存的选择,不再重复询问
841
+ - 构造 testCase 时,用 `dataset` 数组参数化不同账号:
842
+ ```json
843
+ {
844
+ "title": "多账号权限测试",
845
+ "dataset": [
846
+ { "user": "admin", "pwd": "Jx@shSmt01#" },
847
+ { "user": "18718576547", "pwd": "Jx@shSmt01#" }
848
+ ],
849
+ "steps": [
850
+ { "type": "cdpScript", "script": "logout" },
851
+ { "type": "cdpScript", "script": "login", "params": { "username": "{{user}}", "password": "{{pwd}}" } },
852
+ { "type": "act", "click": "text=提交", "waitAfterEach": 3000 }
853
+ ]
854
+ }
855
+ ```
856
+
857
+ ### 技术栈(纯 Node.js,零原生编译)
858
+
859
+ | 模块 | 功能 | 依赖 |
860
+ |------|------|------|
861
+ | `lib/cdp-browser-config.js` | 通过 wmic + CDP 自动获取 `--user-data-dir` | Node.js 内置 |
862
+ | `lib/decrypt-passwords.js` | DPAPI 解密 AES 密钥 + AES-256-GCM 解密密码 | `sql.js`(共享缓存)+ `crypto` + PowerShell |
863
+ | `decrypt-creds.mjs` | CLI 入口:检测配置 → 解密 → 输出 | 以上两者 |
864
+
865
+ ### 兼容性说明
866
+
867
+ - ✅ **v10 格式**(开发/测试环境 Edge/Chrome):完全支持
868
+ - ❌ **v20 格式**(生产环境,App-Bound Encryption):需 Python `pywin32` + `cryptography` 方案
869
+ - ⚠️ Windows 专属(DPAPI),macOS/Linux 需 Keychain/KWallet 方案
870
+
723
871
  ## 反模式备忘(来自 6/17 + 6/18 实战反馈)
724
872
 
725
873
  | 错误做法 | 正确做法 | 原因 |
@@ -734,6 +882,76 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
734
882
  | 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
735
883
  | 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
736
884
  | 表单校验用 ad-hoc evaluate 检查 | 用 `expect validation.toPass` 或 `formFill` 的 `validateAfter` | 语义断言自带 poll + 结构化错误信息(field + message),可直接进报告 |
885
+ | 登录/登出写多步 antInput + act | 用 `cdpScript` 一次搞定:`{ "type": "cdpScript", "script": "login", "params": {...} }` | 已封装为内置脚本,自动处理 appId/viewId 参数保留和 cookie 清理 |
886
+ | 登出后硬编码裸 URL 导航 | 从主 frame 提取 `appId`/`viewId` 构建完整登录 URL | 裸 URL 不会自动补充 SSO 参数,导致后续登录失败 |
887
+ | CDP captureApi 请求数为 0 | 关闭 DevTools 后再用 `Network.enable` 监听 | DevTools 的 Network 面板会抢占 CDP Network 域,导致 captureApi 捕获不到请求 |
888
+
889
+ ## 🛡️ 测试健壮性三层架构(L1/L2/L3)
890
+
891
+ 每次 testCase 执行都会自动运行三层监控与恢复,无需额外配置:
892
+
893
+ ```
894
+ testCase 执行
895
+ ├── L1 自动监控(每步内置)
896
+ │ ├── Runtime.exceptionThrown → browserErrors[]
897
+ │ ├── console.error → consoleErrors[]
898
+ │ └── Network.response ≥400 → failedRequests[]
899
+
900
+ ├── L2 智能诊断(失败时自动生成)
901
+ │ ├── 失败步骤定位(哪个 step/dataRow 失败)
902
+ │ ├── 网络错误归类(401/403/5xx 统计)
903
+ │ └── 推断建议(session 过期/验证码/服务器异常)
904
+
905
+ └── L3 自动修复(条件触发)
906
+ ├── 登录失败 → 刷新页面 + 重新登录(maxRetries 次)
907
+ ├── 元素未找到 → 等待渲染 + 重试
908
+ └── 网络错误 → 页面重载 + 重试
909
+ ```
910
+
911
+ ### testCase 新增选项
912
+
913
+ ```json
914
+ {
915
+ "title": "测试",
916
+ "options": {
917
+ "autoRecover": true, // 开启自动恢复(默认 true)
918
+ "autoRecoverRetries": 2 // 最大恢复重试次数(默认 2)
919
+ },
920
+ "steps": [...]
921
+ }
922
+ ```
923
+
924
+ ### testCase 返回增强
925
+
926
+ ```json
927
+ {
928
+ "passedSteps": 8, // 最终通过数
929
+ "failedSteps": 0, // 最终失败数
930
+ "browserErrors": [...], // L1: JS 异常详情
931
+ "failedRequests": [...], // L1: 4xx/5xx 请求列表
932
+ "consoleErrors": [...], // L1: console.error 消息
933
+ "diagnosticHint": { // L2: 诊断结果(仅失败时)
934
+ "failedAt": [...],
935
+ "suggestions": ["建议刷新登录页重试..."],
936
+ "summary": "1 个步骤失败, 0 个请求异常"
937
+ },
938
+ "autoRecovery": { // L3: 恢复结果(仅触发时)
939
+ "strategies": [{"type": "login", "result": "success"}],
940
+ "recovered": true,
941
+ "retries": 2
942
+ }
943
+ }
944
+ ```
945
+
946
+ ### cdpScript login 增强
947
+
948
+ - `params.username` 必填,`params.password` 可选(自动从 `engine._credentials` 查找)
949
+ - 内置验证码过期重试:失败时自动刷新页面重新填表(`params.maxRetries` 默认 3)
950
+ - 自动处理 appId/viewId URL 参数
951
+
952
+ ```json
953
+ { "type": "cdpScript", "script": "login", "params": { "username": "admin" } }
954
+ ```
737
955
 
738
956
  ## Ant Design 适配层(6/22 反馈落地)
739
957
 
@@ -799,18 +1017,20 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
799
1017
  |------|---------|---------|------|---------|
800
1018
  | 1 | evaluate | 浏览器端 | DOM 查询/操作 | 简单检查、点击元素 |
801
1019
  | 2 | rawScript | Node.js 端 | 完整 puppeteer API | 跨 frame、等待、多标签页 |
802
- | 3 | cdpScript | Node.js 端 | 调用已注册脚本 | 复用通用交互模式 |
803
- | 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 | Drawer/Modal/表格等 |
1020
+ | 3 | cdpScript | Node.js 端 | 调用内置或用户注册脚本 | 登录/登出/切换账号/抽屉/弹窗等通用交互 |
1021
+ | 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 + 凭据管理 | login/logout/getCredentials/drawer-action/modal-action/switch-tab/wait-api/get-table-data |
804
1022
 
805
1023
  **evaluate**:浏览器端执行 JS,只能操作 DOM,不能等待/跨 frame/调用 puppeteer
806
1024
  **rawScript**:Node.js 端执行 async 函数,接收 `(page, frame, params, browser)`,可调用所有 puppeteer API
807
1025
  **registerCdpScript**:注册可复用脚本,后续通过 cdpScript 步骤按 name 调用
808
- **cdpScript**:调用 registerCdpScript 注册的脚本或内置脚本
1026
+ **cdpScript**:调用内置脚本或用户注册脚本。内置脚本包括登录(支持自动查凭据+验证码重试)、登出(自动保留 appId/viewId)、凭据列表获取等
809
1027
 
810
1028
  ### 内置 cdpScript(通用 UI 交互模式)
811
1029
 
812
1030
  | script | 功能 | params |
813
1031
  |--------|------|--------|
1032
+ | `login` | **自动登录**(填表+提交) | `username`, `password`, `captcha?`(默认6), `loginUrl?` |
1033
+ | `logout` | **登出**(清 cookie+跳转登录页) | `appUrl?`(默认/szc-ai-data/) |
814
1034
  | `drawer-action` | Drawer 操作 | `action: 'search'\|'select'\|'confirm'\|'cancel'`, `radioIndex?`, `searchText?`, `searchPlaceholder?` |
815
1035
  | `modal-action` | Modal/Popconfirm 操作 | `action: 'confirm'\|'cancel'`, `buttonText?` |
816
1036
  | `switch-tab` | 多标签页切换 | `urlContains?`, `titleIncludes?`, `pageIndex?` |