@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
@@ -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?` |
@@ -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();