Files
eze 918d9334ce feat: Microsoft Edge 支持 + 浏览器偏好持久化 (v2.5.2)
- CDP Proxy 新增 Edge 适配(及 Chromium / Chrome Canary 等 Chromium 系),
  通过 browser-discovery 模块统一发现 + 决策,不绑定 Chrome
- 新增 config.env 持久化默认浏览器偏好(WEB_ACCESS_BROWSER),
  多浏览器同开 toggle 时询问用户;支持 --browser 单次覆盖
- 偏好/指定的浏览器未启动时硬错并给出明确处理步骤,不擅自降级;
  proxy 首次成功连接后 pin 住浏览器 id,防止运行中漂移
- find-url 同步支持 Chrome / Edge,默认遍历所有已安装的,
  --browser 可限定单一来源

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:24:04 +08:00

139 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 浏览器 CDP 端口发现 + 选择 - 单一职责模块
// 被 check-deps.mjs 和 cdp-proxy.mjs 共享。
//
// 选择规则(resolution):
// 1. 调用方传入 override 参数(来自命令行 --browser) → 严格模式,找不到则硬错
// 2. config.env 里 WEB_ACCESS_BROWSER 设了 → 严格模式,找不到则硬错
// 3. 都没设 → "ask" 模式,提示调用方询问用户
//
// 不擅自降级:偏好不可用一律硬错,让用户介入。
// 持久态只有 config.env 一处;override 是单次 spawn 通过命令行参数表达,不读 process.env。
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const CONFIG_PATH = path.join(SKILL_ROOT, 'config.env');
// 已知支持 chrome://inspect#remote-debugging toggle 的浏览器
// 加新浏览器:只改这里
export function knownBrowsers() {
const home = os.homedir();
const localAppData = process.env.LOCALAPPDATA || '';
switch (os.platform()) {
case 'darwin':
return [
{ id: 'chrome', label: 'Chrome', devToolsPath: path.join(home, 'Library/Application Support/Google/Chrome/DevToolsActivePort') },
{ id: 'chrome-canary', label: 'Chrome Canary', devToolsPath: path.join(home, 'Library/Application Support/Google/Chrome Canary/DevToolsActivePort') },
{ id: 'chromium', label: 'Chromium', devToolsPath: path.join(home, 'Library/Application Support/Chromium/DevToolsActivePort') },
{ id: 'edge', label: 'Microsoft Edge', devToolsPath: path.join(home, 'Library/Application Support/Microsoft Edge/DevToolsActivePort') },
];
case 'linux':
return [
{ id: 'chrome', label: 'Chrome', devToolsPath: path.join(home, '.config/google-chrome/DevToolsActivePort') },
{ id: 'chromium', label: 'Chromium', devToolsPath: path.join(home, '.config/chromium/DevToolsActivePort') },
{ id: 'edge', label: 'Microsoft Edge', devToolsPath: path.join(home, '.config/microsoft-edge/DevToolsActivePort') },
];
case 'win32':
return [
{ id: 'chrome', label: 'Chrome', devToolsPath: path.join(localAppData, 'Google/Chrome/User Data/DevToolsActivePort') },
{ id: 'chromium', label: 'Chromium', devToolsPath: path.join(localAppData, 'Chromium/User Data/DevToolsActivePort') },
{ id: 'edge', label: 'Microsoft Edge', devToolsPath: path.join(localAppData, 'Microsoft/Edge/User Data/DevToolsActivePort') },
];
default:
return [];
}
}
// TCP 端口监听检测
// 用 TCP connect 而非 WebSocket,避免触发浏览器的远程调试授权弹窗。
export function checkPort(port, host = '127.0.0.1', timeoutMs = 2000) {
return new Promise((resolve) => {
const socket = net.createConnection(port, host);
const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeoutMs);
socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); });
socket.once('error', () => { clearTimeout(timer); resolve(false); });
});
}
// 读 config.env 文件(不写入 process.env,分清来源)
// 格式:KEY=VALUE,# 开头是注释
function readConfig() {
const cfg = {};
let content;
try { content = fs.readFileSync(CONFIG_PATH, 'utf8'); }
catch { return cfg; }
for (const line of content.split(/\r?\n/)) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const i = t.indexOf('=');
if (i === -1) continue;
const k = t.slice(0, i).trim();
const v = t.slice(i + 1).trim();
if (k && v) cfg[k] = v;
}
return cfg;
}
// 返回所有开了 toggle 且端口活的浏览器
async function detectAll() {
const result = [];
for (const browser of knownBrowsers()) {
let content;
try { content = fs.readFileSync(browser.devToolsPath, 'utf8'); }
catch { continue; }
const lines = content.trim().split(/\r?\n/).filter(Boolean);
const port = parseInt(lines[0], 10);
if (!(port > 0 && port < 65536)) continue;
if (!(await checkPort(port))) continue;
result.push({ ...browser, port, wsPath: lines[1] || null });
}
return result;
}
// 决策入口
// 参数:override — 调用方解析自命令行 --browser 的值(null 表示未传)
// 返回 { kind, browser?, source?, detected, configured, override? }
// kind ∈ 'ok' | 'ambiguous' | 'mismatch' | 'empty'
// source ∈ 'override' | 'preference' | undefined
// ambiguous = 没设偏好 + 至少一个浏览器开了 toggle,需问用户
// mismatch = override/配偏好设了但未检测到对应 toggle,硬错
// empty = 0 浏览器开 toggle 且未设偏好/override
export async function selectBrowser(override = null) {
const detected = await detectAll();
const configured = readConfig().WEB_ACCESS_BROWSER || null;
// 1. 命令行 override(最高优先,单次有效)
if (override) {
const match = detected.find(b => b.id === override);
if (match) return { kind: 'ok', browser: match, source: 'override', detected, configured, override };
return { kind: 'mismatch', source: 'override', detected, configured, override };
}
// 2. config.env preference(持久)
if (configured) {
const match = detected.find(b => b.id === configured);
if (match) return { kind: 'ok', browser: match, source: 'preference', detected, configured };
return { kind: 'mismatch', source: 'preference', detected, configured };
}
// 3. 无偏好 —— 一律询问用户(哪怕 detected 只有一个)
if (detected.length === 0) {
return { kind: 'empty', detected, configured };
}
return { kind: 'ambiguous', detected, configured };
}
// 兜底:扫描常用固定端口
// 适用场景:用户手动 --remote-debugging-port=9222 启动浏览器,
// 此时 DevToolsActivePort 可能不在默认 user-data-dir。
export async function findFallbackPort() {
for (const port of [9222, 9229, 9333]) {
if (await checkPort(port)) return port;
}
return null;
}