@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"];
@@ -0,0 +1,230 @@
1
+ /**
2
+ * dev-browser-shortcut.js — 为本地浏览器自动化创建"开发模式"快捷方式
3
+ *
4
+ * 目标:用户装包后无需手动敲 --remote-debugging-port=9222 等一长串参数,
5
+ * 双击快捷方式即可启动一个开 CDP + 独立 profile 的浏览器,配合 local-browser-test skill。
6
+ *
7
+ * 设计原则:
8
+ * 1. 不动用户主浏览器快捷方式 / Dock 图标 / 默认 profile
9
+ * 2. 同时检测 Chrome 和 Edge,找到几个就装几个(每个一个独立 profile)
10
+ * 3. 已存在的快捷方式覆盖(保持参数与 SKILL.md 同步),新版本能自动升级参数
11
+ * 4. 任何子步骤失败都只 warn,绝不让整个 postinstall 退出
12
+ *
13
+ * 三平台落点:
14
+ * Windows: 用户桌面 .lnk(PowerShell + WScript.Shell COM)
15
+ * Linux: ~/.local/share/applications/<id>.desktop + chmod +x(更新 desktop database 可选)
16
+ * macOS: ~/Applications/<Name> DevSession.app (wrapper .app,bash 启动器)
17
+ */
18
+
19
+ import fs from "node:fs";
20
+ import path from "node:path";
21
+ import os from "node:os";
22
+ import { execSync } from "node:child_process";
23
+
24
+ // 与 SKILL.md 第 186-191 行保持一致
25
+ const CDP_PORT = 9222;
26
+ const COMMON_FLAGS = [
27
+ "--disable-features=RendererCodeIntegrity",
28
+ "--disable-web-security",
29
+ `--remote-debugging-port=${CDP_PORT}`,
30
+ ];
31
+
32
+ // user-data-dir 与 SKILL.md 固定路径一致
33
+ function getUserDataDir(browser, platform) {
34
+ if (platform === "win32") {
35
+ return browser === "edge" ? "C:/EdgeDevSession" : "C:/ChromeDevSession";
36
+ }
37
+ return path.join(os.homedir(), browser === "edge" ? "EdgeDevSession" : "ChromeDevSession");
38
+ }
39
+
40
+ // ============ 浏览器检测 ============
41
+
42
+ function detectBrowsers(platform) {
43
+ const found = [];
44
+ if (platform === "win32") {
45
+ const candidates = [
46
+ { id: "edge", name: "Microsoft Edge", paths: [
47
+ "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
48
+ "C:/Program Files/Microsoft/Edge/Application/msedge.exe",
49
+ ]},
50
+ { id: "chrome", name: "Google Chrome", paths: [
51
+ "C:/Program Files/Google/Chrome/Application/chrome.exe",
52
+ "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe",
53
+ path.join(os.homedir(), "AppData/Local/Google/Chrome/Application/chrome.exe"),
54
+ ]},
55
+ ];
56
+ for (const c of candidates) {
57
+ const hit = c.paths.find(p => safeExists(p));
58
+ if (hit) found.push({ ...c, exe: hit });
59
+ }
60
+ } else if (platform === "darwin") {
61
+ const candidates = [
62
+ { id: "chrome", name: "Google Chrome",
63
+ app: "/Applications/Google Chrome.app",
64
+ exe: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" },
65
+ { id: "edge", name: "Microsoft Edge",
66
+ app: "/Applications/Microsoft Edge.app",
67
+ exe: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" },
68
+ ];
69
+ for (const c of candidates) {
70
+ if (safeExists(c.app)) found.push(c);
71
+ }
72
+ } else {
73
+ // linux
74
+ const candidates = [
75
+ { id: "chrome", name: "Google Chrome", cmds: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"] },
76
+ { id: "edge", name: "Microsoft Edge", cmds: ["microsoft-edge", "microsoft-edge-stable"] },
77
+ ];
78
+ for (const c of candidates) {
79
+ const exe = c.cmds.map(which).find(Boolean);
80
+ if (exe) found.push({ id: c.id, name: c.name, exe });
81
+ }
82
+ }
83
+ return found;
84
+ }
85
+
86
+ function safeExists(p) {
87
+ try { return fs.existsSync(p); } catch { return false; }
88
+ }
89
+
90
+ function which(cmd) {
91
+ try {
92
+ const out = execSync(`command -v ${cmd}`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
93
+ return out || null;
94
+ } catch { return null; }
95
+ }
96
+
97
+ // ============ 平台实现 ============
98
+
99
+ function installWindows(browser) {
100
+ const dataDir = getUserDataDir(browser.id, "win32");
101
+ const args = [`--user-data-dir="${dataDir}"`, ...COMMON_FLAGS].join(" ");
102
+ const desktop = path.join(os.homedir(), "Desktop");
103
+ const lnkPath = path.join(desktop, `${browser.name} DevSession.lnk`);
104
+
105
+ // PowerShell 创建 .lnk
106
+ // 注意:PowerShell -Command 参数里的双引号、反斜杠都得转义
107
+ const ps = [
108
+ `$s = (New-Object -ComObject WScript.Shell).CreateShortcut('${lnkPath.replace(/'/g, "''")}')`,
109
+ `$s.TargetPath = '${browser.exe.replace(/'/g, "''")}'`,
110
+ `$s.Arguments = '${args.replace(/'/g, "''")}'`,
111
+ `$s.WorkingDirectory = '${path.dirname(browser.exe).replace(/'/g, "''")}'`,
112
+ `$s.IconLocation = '${browser.exe.replace(/'/g, "''")},0'`,
113
+ `$s.Description = '${browser.name} CDP dev mode (port ${CDP_PORT}) — used by szcd local-browser-test'`,
114
+ `$s.Save()`,
115
+ ].join("; ");
116
+ try {
117
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps}"`, { stdio: "ignore" });
118
+ return { ok: true, path: lnkPath };
119
+ } catch (e) {
120
+ return { ok: false, error: e.message };
121
+ }
122
+ }
123
+
124
+ function installMac(browser) {
125
+ const dataDir = getUserDataDir(browser.id, "darwin");
126
+ const appDir = path.join(os.homedir(), "Applications");
127
+ const wrapperApp = path.join(appDir, `${browser.name} DevSession.app`);
128
+ const macosDir = path.join(wrapperApp, "Contents", "MacOS");
129
+ const resourcesDir = path.join(wrapperApp, "Contents", "Resources");
130
+ const plistPath = path.join(wrapperApp, "Contents", "Info.plist");
131
+ const execName = "launch";
132
+ const execPath = path.join(macosDir, execName);
133
+
134
+ try {
135
+ fs.mkdirSync(macosDir, { recursive: true });
136
+ fs.mkdirSync(resourcesDir, { recursive: true });
137
+ const script = [
138
+ "#!/bin/bash",
139
+ `exec "${browser.exe}" --user-data-dir="${dataDir}" ${COMMON_FLAGS.join(" ")} "$@"`,
140
+ "",
141
+ ].join("\n");
142
+ fs.writeFileSync(execPath, script, { mode: 0o755 });
143
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
144
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
145
+ <plist version="1.0">
146
+ <dict>
147
+ <key>CFBundleExecutable</key><string>${execName}</string>
148
+ <key>CFBundleName</key><string>${browser.name} DevSession</string>
149
+ <key>CFBundleIdentifier</key><string>com.szc-ft.${browser.id}-devsession</string>
150
+ <key>CFBundlePackageType</key><string>APPL</string>
151
+ <key>CFBundleVersion</key><string>1</string>
152
+ <key>LSUIElement</key><false/>
153
+ </dict>
154
+ </plist>
155
+ `;
156
+ fs.writeFileSync(plistPath, plist);
157
+ return { ok: true, path: wrapperApp };
158
+ } catch (e) {
159
+ return { ok: false, error: e.message };
160
+ }
161
+ }
162
+
163
+ function installLinux(browser) {
164
+ const dataDir = getUserDataDir(browser.id, "linux");
165
+ const dir = path.join(os.homedir(), ".local", "share", "applications");
166
+ const file = path.join(dir, `szcd-${browser.id}-devsession.desktop`);
167
+ const content = [
168
+ "[Desktop Entry]",
169
+ "Version=1.0",
170
+ "Type=Application",
171
+ `Name=${browser.name} DevSession`,
172
+ `Comment=${browser.name} with CDP port ${CDP_PORT} and isolated profile — used by szcd local-browser-test`,
173
+ `Exec=${browser.exe} --user-data-dir=${dataDir} ${COMMON_FLAGS.join(" ")} %U`,
174
+ "Terminal=false",
175
+ "Categories=Development;WebBrowser;",
176
+ `Icon=${browser.id === "edge" ? "microsoft-edge" : "google-chrome"}`,
177
+ "StartupNotify=true",
178
+ "",
179
+ ].join("\n");
180
+ try {
181
+ fs.mkdirSync(dir, { recursive: true });
182
+ fs.writeFileSync(file, content, { mode: 0o644 });
183
+ fs.chmodSync(file, 0o755);
184
+ // 更新 desktop database(非必须,失败不影响)
185
+ try { execSync(`update-desktop-database "${dir}"`, { stdio: "ignore" }); } catch {}
186
+ return { ok: true, path: file };
187
+ } catch (e) {
188
+ return { ok: false, error: e.message };
189
+ }
190
+ }
191
+
192
+ // ============ 主入口 ============
193
+
194
+ export function setupDevBrowserShortcut(deps = {}) {
195
+ const platform = process.platform;
196
+ const results = [];
197
+
198
+ try {
199
+ const browsers = detectBrowsers(platform);
200
+ if (browsers.length === 0) {
201
+ console.log("⚠️ No Chrome/Edge detected — skipping dev-browser shortcut.");
202
+ return { ok: false, results, skipped: true };
203
+ }
204
+ const install =
205
+ platform === "win32" ? installWindows :
206
+ platform === "darwin" ? installMac :
207
+ installLinux;
208
+ for (const b of browsers) {
209
+ const r = install(b);
210
+ results.push({ browser: b.id, ...r });
211
+ if (r.ok) {
212
+ console.log(`✓ Created ${b.name} DevSession shortcut: ${r.path}`);
213
+ } else {
214
+ console.warn(`⚠️ Failed to create ${b.name} DevSession shortcut: ${r.error}`);
215
+ }
216
+ }
217
+ const okCount = results.filter(r => r.ok).length;
218
+ if (okCount > 0) {
219
+ const dirHint = platform === "win32" ? "桌面 (Desktop)"
220
+ : platform === "darwin" ? "~/Applications/ (Launchpad / Spotlight)"
221
+ : "应用菜单 (Activities)";
222
+ console.log(`\n💡 Double-click the "DevSession" shortcut in ${dirHint} to start a CDP-enabled browser`);
223
+ console.log(` on port ${CDP_PORT} with an isolated profile, then re-run your local-browser-test plan.`);
224
+ }
225
+ return { ok: okCount > 0, results };
226
+ } catch (e) {
227
+ console.warn(`⚠️ setupDevBrowserShortcut error: ${e.message}`);
228
+ return { ok: false, results, error: e.message };
229
+ }
230
+ }
@@ -24,6 +24,7 @@ import { setupClaudeCode } from "./lib/claude-code.js";
24
24
  import { setupOpenCode } from "./lib/opencode.js";
25
25
  import { setupQwenCode } from "./lib/qwen-code.js";
26
26
  import { setupQoder } from "./lib/qoder.js";
27
+ import { setupDevBrowserShortcut } from "./lib/dev-browser-shortcut.js";
27
28
 
28
29
  // 递归守卫在 common.js 加载时已执行
29
30
  // 超时保护在 common.js 中已初始化
@@ -227,6 +228,10 @@ function main() {
227
228
  console.log("\n🔧 Setting up Qoder CLI compatibility...\n");
228
229
  const qoderResult = setupQoder(deps);
229
230
 
231
+ // ---- Dev Browser Shortcut(local-browser-test 一键启动) ----
232
+ console.log("\n🔧 Setting up dev-browser shortcut for local-browser-test...\n");
233
+ setupDevBrowserShortcut(deps);
234
+
230
235
  showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult, openCodeResult);
231
236
  } catch (error) {
232
237
  console.error("\n❌ Error during installation:", error.message);