@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
|
@@ -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
|
|
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,6 +178,9 @@ 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
|
用户一次性配置:
|
|
172
185
|
```bash
|
|
173
186
|
# macOS
|
|
@@ -376,7 +389,7 @@ node "$EXECUTOR" \
|
|
|
376
389
|
--output /tmp/browser-observe.json
|
|
377
390
|
```
|
|
378
391
|
|
|
379
|
-
`observe` 输出 `frames`、`tree`、`interactiveElements[index]`、`selectorCandidates`、`bbox`、`state
|
|
392
|
+
`observe` 输出 `frames`、`tree`、`interactiveElements[index]`、`selectorCandidates`、`bbox`、`state`、`wujieContext`(含 `shadowBodyText` 全文 + `shadowBodyTextLength`)。Agent 后续应优先使用 `index` 调用 `act`,不要从 DOM 树重新猜 selector。直接 `--action observe` 默认只连接用户已启动的真实浏览器 CDP;如确需无 CDP 时启动 headless 回退,追加 `--allow-launch`。
|
|
380
393
|
|
|
381
394
|
### 观察后按 index 交互(主链路闭环)
|
|
382
395
|
```bash
|
|
@@ -440,6 +453,38 @@ node "$EXECUTOR" \
|
|
|
440
453
|
|
|
441
454
|
计划内可直接在 `apiCapture` 后追加 `{ "type": "assertApi", "maxFailed": 0, "forbidStatusGte": 400 }`,会自动读取上一步捕获结果。
|
|
442
455
|
|
|
456
|
+
### 浏览器端 JS 执行(构造 testCase 前提取表单元数据)
|
|
457
|
+
```bash
|
|
458
|
+
node "$EXECUTOR" \
|
|
459
|
+
--action evaluate \
|
|
460
|
+
--url-contains "platform.aicityos.com" \
|
|
461
|
+
--frame-index 1 \
|
|
462
|
+
--expression '(() => Array.from(document.querySelectorAll(".ant-form-item")).map(item => ({
|
|
463
|
+
label: item.querySelector(".ant-form-item-label")?.textContent?.trim() || "",
|
|
464
|
+
required: !!item.querySelector(".ant-form-item-required"),
|
|
465
|
+
error: item.querySelector(".ant-form-item-explain-error")?.textContent?.trim() || ""
|
|
466
|
+
})).filter(x => x.label))()' \
|
|
467
|
+
--fail-on-falsy \
|
|
468
|
+
--output /tmp/form-fields.json
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
`--action evaluate` 在目标 frame 中执行任意 JS 表达式(需用 IIFE 包裹),返回 `{ result, passed }`。这是构造 testCase JSON 前的核心步骤:**无源码依赖地提取页面表单元数据、校验错误、Select 选项列表等**。
|
|
472
|
+
|
|
473
|
+
关键参数:
|
|
474
|
+
- `--expression <js>`:执行的 JS 表达式(必填)
|
|
475
|
+
- `--fail-on-falsy`:结果为 falsy 时标记 FAIL
|
|
476
|
+
- `--expect-result-contains <text>`:结果 JSON 包含指定文本
|
|
477
|
+
|
|
478
|
+
### 执行测试计划 JSON(ES Module 版)
|
|
479
|
+
```bash
|
|
480
|
+
node "$EXECUTOR" \
|
|
481
|
+
--action plan \
|
|
482
|
+
--plan-file /tmp/test-plan.json \
|
|
483
|
+
--output /tmp/browser-test-result.json
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
`--action plan` 使用 ES Module 执行器运行测试计划 JSON,支持全部步骤类型(包括 evaluate、antSelect 等)。与 `--action test-case` 的区别:plan 不走 dataset 循环,适合单次执行。
|
|
487
|
+
|
|
443
488
|
### 快速诊断(过渡兼容)
|
|
444
489
|
```bash
|
|
445
490
|
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.cjs" \
|
|
@@ -526,7 +571,8 @@ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.
|
|
|
526
571
|
--scroll down --scroll-amount 500
|
|
527
572
|
```
|
|
528
573
|
选择器语法:
|
|
529
|
-
- `text=Build` →
|
|
574
|
+
- `text=Build` → 按文本内容模糊查找,多个匹配时选 innerText 最短的元素(叶子节点优先,避免命中父容器)
|
|
575
|
+
- `text==Build` → 精确文本匹配(Playwright 风格),只匹配 innerText 完全相等的元素
|
|
530
576
|
- `role=button` → 按 ARIA role 查找
|
|
531
577
|
- `~ant-menu-item` → CSS class 模糊匹配(`[class*="ant-menu-item"]`)
|
|
532
578
|
- `.my-class` → 精确 CSS 选择器
|
|
@@ -599,12 +645,12 @@ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.
|
|
|
599
645
|
## 依赖管理
|
|
600
646
|
|
|
601
647
|
浏览器测试依赖采用**共享缓存 + 按需安装**方案:
|
|
602
|
-
-
|
|
648
|
+
- 主链路(`observe/act/assert/diagnose/screenshot` + 凭据解密)安装 `puppeteer-core` + `sql.js` 到 `~/.szcd-mcp/deps/`
|
|
603
649
|
- 只有执行设计稿像素对比 `compare` 时才安装 `pixelmatch/pngjs/sharp`
|
|
604
650
|
- 后续所有项目共享复用,无需重复安装
|
|
605
651
|
- 如果核心依赖自动安装失败,引导用户手动执行:
|
|
606
652
|
```bash
|
|
607
|
-
cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com puppeteer-core
|
|
653
|
+
cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com puppeteer-core sql.js
|
|
608
654
|
```
|
|
609
655
|
- 如果视觉对比依赖自动安装失败,引导用户手动执行:
|
|
610
656
|
```bash
|
|
@@ -687,6 +733,133 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
687
733
|
```
|
|
688
734
|
`runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
|
|
689
735
|
|
|
736
|
+
## 🔑 凭据解密(自动获取浏览器保存的密码)
|
|
737
|
+
|
|
738
|
+
当测试需要登录但浏览器已保存密码时,使用内置的 `decrypt-creds.mjs` 自动从 Edge/Chrome 的密码管理器中解密凭据,**无需人工输入密码**。
|
|
739
|
+
|
|
740
|
+
### 工作原理
|
|
741
|
+
|
|
742
|
+
```
|
|
743
|
+
Agent / SKILL.md
|
|
744
|
+
→ $SKILL_DIR/lib/cdp-browser-config.js 自动检测浏览器 user-data-dir
|
|
745
|
+
→ $SKILL_DIR/lib/decrypt-passwords.js DPAPI + AES-GCM 解密 v10 密码
|
|
746
|
+
→ $SKILL_DIR/decrypt-creds.mjs CLI 入口(一键调用)
|
|
747
|
+
→ 返回 { username, password, url }
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### 前置条件
|
|
751
|
+
|
|
752
|
+
- 浏览器以 `--remote-debugging-port=9222` 启动(CDP 已连接)
|
|
753
|
+
- 浏览器以 `--user-data-dir=<path>` 指定了自定义数据目录(开发/测试环境常见)
|
|
754
|
+
- 密码以 **v10 格式**存储(开发环境默认;生产环境 v20 需 Python 方案)
|
|
755
|
+
- skill 目录下已安装 `sql.js`:`cd $SKILL_DIR && npm install sql.js`
|
|
756
|
+
- 首次解密后结果自动缓存到 `~/.szcd-mcp/deps/decrypted-creds.json`,后续秒级返回
|
|
757
|
+
|
|
758
|
+
### 使用方式
|
|
759
|
+
|
|
760
|
+
```bash
|
|
761
|
+
# 自动检测浏览器配置 + 解密所有凭据(使用共享缓存依赖)
|
|
762
|
+
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos
|
|
763
|
+
|
|
764
|
+
# 首次运行:全量解密 + 写入缓存(~/.szcd-mcp/deps/decrypted-creds.json)
|
|
765
|
+
# 后续运行:秒级返回(自动检测 Login Data 修改时间判断缓存是否过期)
|
|
766
|
+
|
|
767
|
+
# 强制刷新缓存(如手动修改了密码)
|
|
768
|
+
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --refresh
|
|
769
|
+
|
|
770
|
+
# JSON 格式输出(供脚本解析)
|
|
771
|
+
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --json
|
|
772
|
+
|
|
773
|
+
# 手动指定 user-data-dir(跳过自动检测)
|
|
774
|
+
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --user-data-dir "C:/EdgeDevSession" --url aicityos
|
|
775
|
+
|
|
776
|
+
# 查看帮助
|
|
777
|
+
NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --help
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### 典型场景:SSO 登录自动化
|
|
781
|
+
|
|
782
|
+
**⚠️ 强制流程:解密后必须先向用户澄清使用哪一个账号登录,禁止自动选择!**
|
|
783
|
+
|
|
784
|
+
```bash
|
|
785
|
+
# 1. 解密凭据(使用共享缓存依赖)
|
|
786
|
+
CREDS=$(NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/decrypt-creds.mjs" --url aicityos --json)
|
|
787
|
+
|
|
788
|
+
# ❌ 错误:拿到第一个凭据就直接登录
|
|
789
|
+
# USER=$(echo "$CREDS" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'))[0].username")
|
|
790
|
+
|
|
791
|
+
# ✅ 正确:先把去重后的用户列表展示给用户,让用户选择
|
|
792
|
+
# LLM 根据解密结果,用 AskUserQuestion 列出所有可用账号供用户选择:
|
|
793
|
+
# "解密到以下 X 个账号,请选择使用哪一个登录:"
|
|
794
|
+
# - admin (密码: Jx***01#)
|
|
795
|
+
# - 18718576547 (密码: Jx***01#)
|
|
796
|
+
# - zengzhijie (密码: zen***123)
|
|
797
|
+
# ...
|
|
798
|
+
# 用户选择后,再继续后续步骤
|
|
799
|
+
|
|
800
|
+
# 2. 用户确认后,用选定的凭据构造登录 testCase
|
|
801
|
+
cat > /tmp/login.json <<EOF
|
|
802
|
+
{
|
|
803
|
+
"title": "自动登录",
|
|
804
|
+
"options": { "timeout": 30000 },
|
|
805
|
+
"steps": [
|
|
806
|
+
{ "type": "antInput", "placeholder": "请输入账户", "value": "$USER" },
|
|
807
|
+
{ "type": "antInput", "placeholder": "请输入密码", "value": "$PASS" },
|
|
808
|
+
{ "type": "antInput", "placeholder": "请输入图形验证码", "value": "6" },
|
|
809
|
+
{ "type": "act", "click": "text==登 录" }
|
|
810
|
+
]
|
|
811
|
+
}
|
|
812
|
+
EOF
|
|
813
|
+
|
|
814
|
+
# 3. 执行登录
|
|
815
|
+
node "$EXECUTOR" --action plan --plan-file /tmp/login.json --url-contains "platform.aicityos.com" --wait 8000
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**LLM 处理流程:**
|
|
819
|
+
1. 调用 `decrypt-creds.mjs --json` 获取用户列表(首次慢,后续秒级缓存命中)
|
|
820
|
+
2. 对凭据去重(同一用户名取首次出现的密码)
|
|
821
|
+
3. **用 `AskUserQuestion` 列出所有可用账号(只展示完整用户名,不展示密码),让用户选择**
|
|
822
|
+
4. 用户选择后,从解密结果中查找对应密码,构造登录 testCase
|
|
823
|
+
5. 执行登录
|
|
824
|
+
|
|
825
|
+
**⚠️ 强制约束:**
|
|
826
|
+
- 只展示账号列表,**不展示密码**(含脱敏也不展示)
|
|
827
|
+
- **展示完整账号**,不截断
|
|
828
|
+
- **每次登录都向用户询问**,不自动选择
|
|
829
|
+
|
|
830
|
+
**多账号切换场景(如测试不同角色权限):**
|
|
831
|
+
- 首次登录时一次性询问:"本次测试需要用到哪些账号?",用户可多选
|
|
832
|
+
- 后续切换直接用缓存的选择,不再重复询问
|
|
833
|
+
- 构造 testCase 时,用 `dataset` 数组参数化不同账号:
|
|
834
|
+
```json
|
|
835
|
+
{
|
|
836
|
+
"title": "多账号权限测试",
|
|
837
|
+
"dataset": [
|
|
838
|
+
{ "user": "admin", "pwd": "Jx@shSmt01#" },
|
|
839
|
+
{ "user": "18718576547", "pwd": "Jx@shSmt01#" }
|
|
840
|
+
],
|
|
841
|
+
"steps": [
|
|
842
|
+
{ "type": "cdpScript", "script": "logout" },
|
|
843
|
+
{ "type": "cdpScript", "script": "login", "params": { "username": "{{user}}", "password": "{{pwd}}" } },
|
|
844
|
+
{ "type": "act", "click": "text=提交", "waitAfterEach": 3000 }
|
|
845
|
+
]
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### 技术栈(纯 Node.js,零原生编译)
|
|
850
|
+
|
|
851
|
+
| 模块 | 功能 | 依赖 |
|
|
852
|
+
|------|------|------|
|
|
853
|
+
| `lib/cdp-browser-config.js` | 通过 wmic + CDP 自动获取 `--user-data-dir` | Node.js 内置 |
|
|
854
|
+
| `lib/decrypt-passwords.js` | DPAPI 解密 AES 密钥 + AES-256-GCM 解密密码 | `sql.js`(共享缓存)+ `crypto` + PowerShell |
|
|
855
|
+
| `decrypt-creds.mjs` | CLI 入口:检测配置 → 解密 → 输出 | 以上两者 |
|
|
856
|
+
|
|
857
|
+
### 兼容性说明
|
|
858
|
+
|
|
859
|
+
- ✅ **v10 格式**(开发/测试环境 Edge/Chrome):完全支持
|
|
860
|
+
- ❌ **v20 格式**(生产环境,App-Bound Encryption):需 Python `pywin32` + `cryptography` 方案
|
|
861
|
+
- ⚠️ Windows 专属(DPAPI),macOS/Linux 需 Keychain/KWallet 方案
|
|
862
|
+
|
|
690
863
|
## 反模式备忘(来自 6/17 + 6/18 实战反馈)
|
|
691
864
|
|
|
692
865
|
| 错误做法 | 正确做法 | 原因 |
|
|
@@ -701,6 +874,76 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
|
|
|
701
874
|
| 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
|
|
702
875
|
| 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
|
|
703
876
|
| 表单校验用 ad-hoc evaluate 检查 | 用 `expect validation.toPass` 或 `formFill` 的 `validateAfter` | 语义断言自带 poll + 结构化错误信息(field + message),可直接进报告 |
|
|
877
|
+
| 登录/登出写多步 antInput + act | 用 `cdpScript` 一次搞定:`{ "type": "cdpScript", "script": "login", "params": {...} }` | 已封装为内置脚本,自动处理 appId/viewId 参数保留和 cookie 清理 |
|
|
878
|
+
| 登出后硬编码裸 URL 导航 | 从主 frame 提取 `appId`/`viewId` 构建完整登录 URL | 裸 URL 不会自动补充 SSO 参数,导致后续登录失败 |
|
|
879
|
+
| CDP captureApi 请求数为 0 | 关闭 DevTools 后再用 `Network.enable` 监听 | DevTools 的 Network 面板会抢占 CDP Network 域,导致 captureApi 捕获不到请求 |
|
|
880
|
+
|
|
881
|
+
## 🛡️ 测试健壮性三层架构(L1/L2/L3)
|
|
882
|
+
|
|
883
|
+
每次 testCase 执行都会自动运行三层监控与恢复,无需额外配置:
|
|
884
|
+
|
|
885
|
+
```
|
|
886
|
+
testCase 执行
|
|
887
|
+
├── L1 自动监控(每步内置)
|
|
888
|
+
│ ├── Runtime.exceptionThrown → browserErrors[]
|
|
889
|
+
│ ├── console.error → consoleErrors[]
|
|
890
|
+
│ └── Network.response ≥400 → failedRequests[]
|
|
891
|
+
│
|
|
892
|
+
├── L2 智能诊断(失败时自动生成)
|
|
893
|
+
│ ├── 失败步骤定位(哪个 step/dataRow 失败)
|
|
894
|
+
│ ├── 网络错误归类(401/403/5xx 统计)
|
|
895
|
+
│ └── 推断建议(session 过期/验证码/服务器异常)
|
|
896
|
+
│
|
|
897
|
+
└── L3 自动修复(条件触发)
|
|
898
|
+
├── 登录失败 → 刷新页面 + 重新登录(maxRetries 次)
|
|
899
|
+
├── 元素未找到 → 等待渲染 + 重试
|
|
900
|
+
└── 网络错误 → 页面重载 + 重试
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
### testCase 新增选项
|
|
904
|
+
|
|
905
|
+
```json
|
|
906
|
+
{
|
|
907
|
+
"title": "测试",
|
|
908
|
+
"options": {
|
|
909
|
+
"autoRecover": true, // 开启自动恢复(默认 true)
|
|
910
|
+
"autoRecoverRetries": 2 // 最大恢复重试次数(默认 2)
|
|
911
|
+
},
|
|
912
|
+
"steps": [...]
|
|
913
|
+
}
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
### testCase 返回增强
|
|
917
|
+
|
|
918
|
+
```json
|
|
919
|
+
{
|
|
920
|
+
"passedSteps": 8, // 最终通过数
|
|
921
|
+
"failedSteps": 0, // 最终失败数
|
|
922
|
+
"browserErrors": [...], // L1: JS 异常详情
|
|
923
|
+
"failedRequests": [...], // L1: 4xx/5xx 请求列表
|
|
924
|
+
"consoleErrors": [...], // L1: console.error 消息
|
|
925
|
+
"diagnosticHint": { // L2: 诊断结果(仅失败时)
|
|
926
|
+
"failedAt": [...],
|
|
927
|
+
"suggestions": ["建议刷新登录页重试..."],
|
|
928
|
+
"summary": "1 个步骤失败, 0 个请求异常"
|
|
929
|
+
},
|
|
930
|
+
"autoRecovery": { // L3: 恢复结果(仅触发时)
|
|
931
|
+
"strategies": [{"type": "login", "result": "success"}],
|
|
932
|
+
"recovered": true,
|
|
933
|
+
"retries": 2
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### cdpScript login 增强
|
|
939
|
+
|
|
940
|
+
- `params.username` 必填,`params.password` 可选(自动从 `engine._credentials` 查找)
|
|
941
|
+
- 内置验证码过期重试:失败时自动刷新页面重新填表(`params.maxRetries` 默认 3)
|
|
942
|
+
- 自动处理 appId/viewId URL 参数
|
|
943
|
+
|
|
944
|
+
```json
|
|
945
|
+
{ "type": "cdpScript", "script": "login", "params": { "username": "admin" } }
|
|
946
|
+
```
|
|
704
947
|
|
|
705
948
|
## Ant Design 适配层(6/22 反馈落地)
|
|
706
949
|
|
|
@@ -766,18 +1009,20 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
|
|
|
766
1009
|
|------|---------|---------|------|---------|
|
|
767
1010
|
| 1 | evaluate | 浏览器端 | DOM 查询/操作 | 简单检查、点击元素 |
|
|
768
1011
|
| 2 | rawScript | Node.js 端 | 完整 puppeteer API | 跨 frame、等待、多标签页 |
|
|
769
|
-
| 3 | cdpScript | Node.js 端 |
|
|
770
|
-
| 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 |
|
|
1012
|
+
| 3 | cdpScript | Node.js 端 | 调用内置或用户注册脚本 | 登录/登出/切换账号/抽屉/弹窗等通用交互 |
|
|
1013
|
+
| 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 + 凭据管理 | login/logout/getCredentials/drawer-action/modal-action/switch-tab/wait-api/get-table-data |
|
|
771
1014
|
|
|
772
1015
|
**evaluate**:浏览器端执行 JS,只能操作 DOM,不能等待/跨 frame/调用 puppeteer
|
|
773
1016
|
**rawScript**:Node.js 端执行 async 函数,接收 `(page, frame, params, browser)`,可调用所有 puppeteer API
|
|
774
1017
|
**registerCdpScript**:注册可复用脚本,后续通过 cdpScript 步骤按 name 调用
|
|
775
|
-
**cdpScript
|
|
1018
|
+
**cdpScript**:调用内置脚本或用户注册脚本。内置脚本包括登录(支持自动查凭据+验证码重试)、登出(自动保留 appId/viewId)、凭据列表获取等
|
|
776
1019
|
|
|
777
1020
|
### 内置 cdpScript(通用 UI 交互模式)
|
|
778
1021
|
|
|
779
1022
|
| script | 功能 | params |
|
|
780
1023
|
|--------|------|--------|
|
|
1024
|
+
| `login` | **自动登录**(填表+提交) | `username`, `password`, `captcha?`(默认6), `loginUrl?` |
|
|
1025
|
+
| `logout` | **登出**(清 cookie+跳转登录页) | `appUrl?`(默认/szc-ai-data/) |
|
|
781
1026
|
| `drawer-action` | Drawer 操作 | `action: 'search'\|'select'\|'confirm'\|'cancel'`, `radioIndex?`, `searchText?`, `searchPlaceholder?` |
|
|
782
1027
|
| `modal-action` | Modal/Popconfirm 操作 | `action: 'confirm'\|'cancel'`, `buttonText?` |
|
|
783
1028
|
| `switch-tab` | 多标签页切换 | `urlContains?`, `titleIncludes?`, `pageIndex?` |
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 浏览器凭据解密工具
|
|
4
|
+
*
|
|
5
|
+
* 自动检测 Edge/Chrome 的 user-data-dir 配置并解密保存的密码。
|
|
6
|
+
* 解密结果缓存到 ~/.szcd-mcp/deps/decrypted-creds.json,避免重复解密。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* NODE_PATH=~/.szcd-mcp/deps/node_modules node decrypt-creds.mjs --url aicityos
|
|
10
|
+
* NODE_PATH=~/.szcd-mcp/deps/node_modules node decrypt-creds.mjs --json
|
|
11
|
+
* NODE_PATH=~/.szcd-mcp/deps/node_modules node decrypt-creds.mjs --refresh # 强制刷新缓存
|
|
12
|
+
*
|
|
13
|
+
* 前置条件:
|
|
14
|
+
* - 浏览器以 --remote-debugging-port 和 --user-data-dir 启动
|
|
15
|
+
* - ~/.szcd-mcp/deps/ 已安装 sql.js(共享缓存,跨 IDE 共享)
|
|
16
|
+
*/
|
|
17
|
+
import { getFullBrowserConfig } from './lib/cdp-browser-config.js';
|
|
18
|
+
import { decryptAllPasswords } from './lib/decrypt-passwords.js';
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, statSync } from 'fs';
|
|
20
|
+
import { resolve, dirname, join } from 'path';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// 缓存文件路径(共享缓存目录)
|
|
27
|
+
const CACHE_DIR = join(homedir(), '.szcd-mcp', 'deps');
|
|
28
|
+
const CACHE_FILE = join(CACHE_DIR, 'decrypted-creds.json');
|
|
29
|
+
|
|
30
|
+
// 解析命令行参数
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const options = {
|
|
33
|
+
url: null,
|
|
34
|
+
json: false,
|
|
35
|
+
refresh: false, // 强制刷新,忽略缓存
|
|
36
|
+
cdpUrl: 'http://localhost:9222',
|
|
37
|
+
userDataDir: null,
|
|
38
|
+
verbose: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
switch (args[i]) {
|
|
43
|
+
case '--url': options.url = args[++i]; break;
|
|
44
|
+
case '--json': options.json = true; break;
|
|
45
|
+
case '--refresh': case '-r': options.refresh = true; break;
|
|
46
|
+
case '--cdp': options.cdpUrl = args[++i]; break;
|
|
47
|
+
case '--user-data-dir': options.userDataDir = args[++i]; break;
|
|
48
|
+
case '--verbose': case '-v': options.verbose = true; break;
|
|
49
|
+
case '--help': case '-h':
|
|
50
|
+
console.log(`浏览器凭据解密工具(带缓存)
|
|
51
|
+
|
|
52
|
+
用法:
|
|
53
|
+
node decrypt-creds.mjs [选项]
|
|
54
|
+
|
|
55
|
+
选项:
|
|
56
|
+
--url <pattern> 按 URL 过滤(如 aicityos)
|
|
57
|
+
--json JSON 格式输出
|
|
58
|
+
--refresh, -r 强制刷新,忽略缓存重新解密
|
|
59
|
+
--cdp <url> CDP 地址(默认 http://localhost:9222)
|
|
60
|
+
--user-data-dir <path> 手动指定 user-data-dir 路径
|
|
61
|
+
--verbose, -v 输出详细信息
|
|
62
|
+
--help, -h 显示帮助
|
|
63
|
+
|
|
64
|
+
缓存说明:
|
|
65
|
+
解密结果自动缓存到 ${CACHE_FILE}
|
|
66
|
+
缓存以 userDataDir + filter 为 key,监测 Login Data 文件修改时间判断是否过期
|
|
67
|
+
`);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 尝试从缓存读取凭据
|
|
74
|
+
* @returns {Array|null} 缓存的凭据数组,null 表示缓存无效
|
|
75
|
+
*/
|
|
76
|
+
function tryReadCache(userDataDir, filter) {
|
|
77
|
+
if (options.refresh) {
|
|
78
|
+
if (options.verbose) console.error('[decrypt-creds] --refresh 强制刷新,跳过缓存');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (!existsSync(CACHE_FILE)) {
|
|
82
|
+
if (options.verbose) console.error('[decrypt-creds] 缓存文件不存在');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const cache = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
|
|
88
|
+
const cacheKey = `${userDataDir}::${filter}`;
|
|
89
|
+
|
|
90
|
+
if (!cache[cacheKey]) {
|
|
91
|
+
if (options.verbose) console.error('[decrypt-creds] 缓存 key 不匹配');
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 检查缓存有效性:Login Data 未变 + 24 小时内
|
|
96
|
+
const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
97
|
+
const entry = cache[cacheKey];
|
|
98
|
+
const age = Date.now() - entry.cachedAt;
|
|
99
|
+
|
|
100
|
+
const loginDataPath = join(userDataDir, 'Default', 'Login Data');
|
|
101
|
+
if (existsSync(loginDataPath)) {
|
|
102
|
+
const loginDataMtime = statSync(loginDataPath).mtimeMs;
|
|
103
|
+
if (loginDataMtime > entry.cachedAt) {
|
|
104
|
+
if (options.verbose) console.error('[decrypt-creds] Login Data 已更新,缓存过期');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (age > CACHE_MAX_AGE_MS) {
|
|
110
|
+
if (options.verbose) console.error(`[decrypt-creds] 缓存已过期 (${Math.round(age / 3600000)}h),重新解密`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
console.error(`[decrypt-creds] ✓ 使用缓存(${Math.round(age / 60000)}min 前缓存,${entry.credentials.length} 条凭据)`);
|
|
116
|
+
}
|
|
117
|
+
return entry.credentials;
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (options.verbose) console.error('[decrypt-creds] 缓存读取失败:', e.message);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 将凭据写入缓存
|
|
126
|
+
*/
|
|
127
|
+
function writeCache(userDataDir, filter, credentials) {
|
|
128
|
+
try {
|
|
129
|
+
let cache = {};
|
|
130
|
+
if (existsSync(CACHE_FILE)) {
|
|
131
|
+
try { cache = JSON.parse(readFileSync(CACHE_FILE, 'utf8')); } catch {}
|
|
132
|
+
}
|
|
133
|
+
const cacheKey = `${userDataDir}::${filter}`;
|
|
134
|
+
cache[cacheKey] = {
|
|
135
|
+
cachedAt: Date.now(),
|
|
136
|
+
userDataDir,
|
|
137
|
+
filter,
|
|
138
|
+
credentials,
|
|
139
|
+
};
|
|
140
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), 'utf8');
|
|
141
|
+
if (options.verbose) console.error('[decrypt-creds] ✓ 已缓存到', CACHE_FILE);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (options.verbose) console.error('[decrypt-creds] 缓存写入失败:', e.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function main() {
|
|
148
|
+
console.error('[decrypt-creds] 检测浏览器配置...');
|
|
149
|
+
|
|
150
|
+
// 1. 获取 user-data-dir
|
|
151
|
+
let userDataDir = options.userDataDir;
|
|
152
|
+
if (!userDataDir) {
|
|
153
|
+
try {
|
|
154
|
+
const config = await getFullBrowserConfig(options.cdpUrl);
|
|
155
|
+
if (config) {
|
|
156
|
+
userDataDir = config.userDataDir;
|
|
157
|
+
if (options.verbose) {
|
|
158
|
+
console.error('[decrypt-creds] 浏览器:', config.browserInfo?.browser);
|
|
159
|
+
console.error('[decrypt-creds] CDP 端口:', config.remoteDebuggingPort);
|
|
160
|
+
console.error('[decrypt-creds] user-data-dir:', userDataDir);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error('[decrypt-creds] CDP 检测失败:', e.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!userDataDir) {
|
|
169
|
+
console.error('[decrypt-creds] 错误: 未检测到 user-data-dir,请用 --user-data-dir 指定');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const filter = options.url ? `%${options.url}%` : '%';
|
|
174
|
+
|
|
175
|
+
// 2. 尝试读缓存
|
|
176
|
+
let credentials = tryReadCache(userDataDir, filter);
|
|
177
|
+
|
|
178
|
+
// 3. 缓存未命中,执行解密
|
|
179
|
+
if (!credentials) {
|
|
180
|
+
console.error(`[decrypt-creds] 解密凭据 (filter: ${filter})...`);
|
|
181
|
+
try {
|
|
182
|
+
credentials = await decryptAllPasswords({
|
|
183
|
+
userDataDir,
|
|
184
|
+
filter,
|
|
185
|
+
verbose: options.verbose,
|
|
186
|
+
});
|
|
187
|
+
// 写入缓存
|
|
188
|
+
writeCache(userDataDir, filter, credentials);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error('[decrypt-creds] 解密失败:', e.message);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 4. 输出结果
|
|
196
|
+
if (options.json) {
|
|
197
|
+
console.log(JSON.stringify(credentials, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
if (credentials.length === 0) {
|
|
200
|
+
console.log('(未找到匹配的凭据)');
|
|
201
|
+
} else {
|
|
202
|
+
const maxUserLen = Math.max(...credentials.map(c => c.username.length));
|
|
203
|
+
for (const { url, username, password } of credentials) {
|
|
204
|
+
console.log(`${username.padEnd(maxUserLen)} ${password} (${url})`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main();
|