@szc-ft/mcp-szcd-client 0.30.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.
@@ -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,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
@@ -632,12 +645,12 @@ NODE_PATH=~/.szcd-mcp/deps/node_modules node "$SKILL_DIR/local-browser-executor.
632
645
  ## 依赖管理
633
646
 
634
647
  浏览器测试依赖采用**共享缓存 + 按需安装**方案:
635
- - `observe/act/assert/diagnose/screenshot` 主链路只安装 `puppeteer-core` 到 `~/.szcd-mcp/deps/`
648
+ - 主链路(`observe/act/assert/diagnose/screenshot` + 凭据解密)安装 `puppeteer-core` + `sql.js` 到 `~/.szcd-mcp/deps/`
636
649
  - 只有执行设计稿像素对比 `compare` 时才安装 `pixelmatch/pngjs/sharp`
637
650
  - 后续所有项目共享复用,无需重复安装
638
651
  - 如果核心依赖自动安装失败,引导用户手动执行:
639
652
  ```bash
640
- 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
641
654
  ```
642
655
  - 如果视觉对比依赖自动安装失败,引导用户手动执行:
643
656
  ```bash
@@ -720,6 +733,133 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
720
733
  ```
721
734
  `runTask` 会自动从 `context.lastApiCapture` 拿菜单树,不需要再传文件。
722
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
+
723
863
  ## 反模式备忘(来自 6/17 + 6/18 实战反馈)
724
864
 
725
865
  | 错误做法 | 正确做法 | 原因 |
@@ -734,6 +874,76 @@ cd ~/.szcd-mcp/deps && npm install --registry=https://registry.npmmirror.com --@
734
874
  | 每个 Select 后忘记 `closeAllDropdowns` | `act` 自动检测上一次 antSelect/antTreeSelect 并预清理 | 下拉浮层残留会污染下一次 Select 操作,选项串选 |
735
875
  | 硬编码 `sleep(15*200)` 等选项渲染 | `antSelect` 内置 `pollEval` 15×200ms 轮询,`expect(poll)` 可自定义条件 | 选项渲染时间受网络/数据量影响,固定 sleep 时机不可靠 |
736
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
+ ```
737
947
 
738
948
  ## Ant Design 适配层(6/22 反馈落地)
739
949
 
@@ -799,18 +1009,20 @@ node local-browser-executor.js --action test-case --test-case-file /tmp/case.jso
799
1009
  |------|---------|---------|------|---------|
800
1010
  | 1 | evaluate | 浏览器端 | DOM 查询/操作 | 简单检查、点击元素 |
801
1011
  | 2 | rawScript | Node.js 端 | 完整 puppeteer API | 跨 frame、等待、多标签页 |
802
- | 3 | cdpScript | Node.js 端 | 调用已注册脚本 | 复用通用交互模式 |
803
- | 4 | 内置 cdpScript | Node.js 端 | 预定义通用 UI 交互 | Drawer/Modal/表格等 |
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 |
804
1014
 
805
1015
  **evaluate**:浏览器端执行 JS,只能操作 DOM,不能等待/跨 frame/调用 puppeteer
806
1016
  **rawScript**:Node.js 端执行 async 函数,接收 `(page, frame, params, browser)`,可调用所有 puppeteer API
807
1017
  **registerCdpScript**:注册可复用脚本,后续通过 cdpScript 步骤按 name 调用
808
- **cdpScript**:调用 registerCdpScript 注册的脚本或内置脚本
1018
+ **cdpScript**:调用内置脚本或用户注册脚本。内置脚本包括登录(支持自动查凭据+验证码重试)、登出(自动保留 appId/viewId)、凭据列表获取等
809
1019
 
810
1020
  ### 内置 cdpScript(通用 UI 交互模式)
811
1021
 
812
1022
  | script | 功能 | params |
813
1023
  |--------|------|--------|
1024
+ | `login` | **自动登录**(填表+提交) | `username`, `password`, `captcha?`(默认6), `loginUrl?` |
1025
+ | `logout` | **登出**(清 cookie+跳转登录页) | `appUrl?`(默认/szc-ai-data/) |
814
1026
  | `drawer-action` | Drawer 操作 | `action: 'search'\|'select'\|'confirm'\|'cancel'`, `radioIndex?`, `searchText?`, `searchPlaceholder?` |
815
1027
  | `modal-action` | Modal/Popconfirm 操作 | `action: 'confirm'\|'cancel'`, `buttonText?` |
816
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();