feat: 新增浏览器用户数据目录多级兜底解析逻辑

This commit is contained in:
WJZ_P
2026-03-16 00:38:32 +08:00
parent e97810ff82
commit b552c88ba0
3 changed files with 129 additions and 12 deletions

2
.env
View File

@@ -9,7 +9,9 @@ BROWSER_PATH=
BROWSER_DEBUG_PORT=
# 浏览器用户数据目录保持登录态、cookies 等)
# 不设则自动解析OpenClaw 状态 → 浏览器默认目录 → ~/.gemini-skill/browser-data
# Browser user data directory (persists login session, cookies, etc.)
# Auto-resolves if not set: OpenClaw status → browser default dir → ~/.gemini-skill/browser-data
BROWSER_USER_DATA_DIR=
# 是否无头模式true / false

View File

@@ -16,8 +16,10 @@ import puppeteerCore from 'puppeteer-core';
import { addExtra } from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { createConnection } from 'node:net';
import { existsSync } from 'node:fs';
import { platform } from 'node:os';
import { existsSync, mkdirSync } from 'node:fs';
import { platform, homedir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import config from './config.js';
// ── 用 puppeteer-extra 包装 puppeteer-core注入 stealth 插件 ──
@@ -91,6 +93,120 @@ function detectBrowser() {
return undefined;
}
// ── userDataDir 兜底目录 ──
const SKILL_FALLBACK_DATA_DIR = join(homedir(), '.gemini-skill', 'browser-data');
/**
* 尝试从 OpenClaw 获取 userDataDir
*
* 执行: openclaw browser --browser-profile openclaw status --json
* 解析返回的 JSON 中的 userDataDir 字段
*
* @returns {string | undefined}
*/
function getOpenClawUserDataDir() {
try {
const stdout = execFileSync('openclaw', [
'browser', '--browser-profile', 'openclaw', 'status', '--json',
], { timeout: 5000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
const json = JSON.parse(stdout);
if (json.userDataDir && typeof json.userDataDir === 'string') {
console.log('[browser] got userDataDir from OpenClaw:', json.userDataDir);
return json.userDataDir;
}
} catch {
// openclaw 不存在或执行失败,静默跳过
}
return undefined;
}
/**
* 获取浏览器默认 userDataDir 路径(不同浏览器/平台)
*
* @returns {string | undefined}
*/
function getDefaultBrowserDataDir() {
const os = platform();
const home = homedir();
const candidates = [];
if (os === 'win32') {
const localAppData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local');
candidates.push(
join(localAppData, 'Google', 'Chrome', 'User Data'),
join(localAppData, 'Microsoft', 'Edge', 'User Data'),
join(localAppData, 'Chromium', 'User Data'),
);
} else if (os === 'darwin') {
const lib = join(home, 'Library', 'Application Support');
candidates.push(
join(lib, 'Google', 'Chrome'),
join(lib, 'Microsoft Edge'),
join(lib, 'Chromium'),
);
} else {
// Linux
candidates.push(
join(home, '.config', 'google-chrome'),
join(home, '.config', 'microsoft-edge'),
join(home, '.config', 'chromium'),
);
}
for (const dir of candidates) {
if (existsSync(dir)) {
console.log('[browser] found default browser data dir:', dir);
return dir;
}
}
return undefined;
}
/**
* 多级兜底解析 userDataDir
*
* 优先级:
* 1. 环境变量 BROWSER_USER_DATA_DIRconfig 已处理)
* 2. OpenClaw 运行状态中的 userDataDir
* 3. 浏览器默认 userDataDirChrome > Edge > Chromium
* 4. Skill 内部创建 ~/.gemini-skill/browser-data兜底 + warning
*
* @returns {string}
*/
function resolveUserDataDir() {
// 1. 环境变量(已由 config 读取)
if (config.browserUserDataDir) {
return config.browserUserDataDir;
}
// 2. OpenClaw
const openclawDir = getOpenClawUserDataDir();
if (openclawDir) {
return openclawDir;
}
// 3. 浏览器默认目录
const defaultDir = getDefaultBrowserDataDir();
if (defaultDir) {
return defaultDir;
}
// 4. Skill 兜底
console.warn(
`[browser] ⚠ 未找到任何已有的 userDataDir将使用 skill 内部目录:${SKILL_FALLBACK_DATA_DIR}\n` +
` 建议通过以下方式指定:\n` +
` - 设置环境变量 BROWSER_USER_DATA_DIR\n` +
` - 安装 OpenClaw 并配置 browser profile`
);
if (!existsSync(SKILL_FALLBACK_DATA_DIR)) {
mkdirSync(SKILL_FALLBACK_DATA_DIR, { recursive: true });
}
return SKILL_FALLBACK_DATA_DIR;
}
/**
* 探测指定端口是否有浏览器在监听
* @param {number} port
@@ -225,10 +341,13 @@ async function findOrCreateGeminiPage(browser) {
* 2. 检查端口是否有浏览器 → connect
* 3. 否则自动检测 / 使用配置的路径启动浏览器
*
* userDataDir 解析优先级:
* opts.userDataDir > env BROWSER_USER_DATA_DIR > OpenClaw > 浏览器默认 > skill 兜底
*
* @param {object} [opts]
* @param {string} [opts.executablePath] - 浏览器路径(仅 launch 时需要,不传则自动检测)
* @param {string} [opts.executablePath] - 浏览器路径(不传则自动检测)
* @param {number} [opts.port] - 调试端口env: BROWSER_DEBUG_PORT默认 9222
* @param {string} [opts.userDataDir] - 用户数据目录env: BROWSER_USER_DATA_DIR
* @param {string} [opts.userDataDir] - 用户数据目录env: BROWSER_USER_DATA_DIR,不传则多级兜底
* @param {boolean} [opts.headless] - 无头模式env: BROWSER_HEADLESS默认 false
* @returns {Promise<{browser: import('puppeteer-core').Browser, page: import('puppeteer-core').Page}>}
*/
@@ -236,7 +355,7 @@ export async function ensureBrowser(opts = {}) {
const {
executablePath = config.browserPath,
port = config.browserDebugPort,
userDataDir = config.browserUserDataDir,
userDataDir = resolveUserDataDir(),
headless = config.browserHeadless,
} = opts;

View File

@@ -9,8 +9,7 @@
* 2. .env 文件(需调用方自行加载,如 dotenv
* 3. 代码默认值
*/
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { resolve } from 'node:path';
const env = process.env;
@@ -44,11 +43,8 @@ const config = {
/** CDP 远程调试端口 */
browserDebugPort: envInt('BROWSER_DEBUG_PORT', 9222),
/** 浏览器用户数据目录 */
browserUserDataDir: envStr(
'BROWSER_USER_DATA_DIR',
join(homedir(), '.gemini-skill', 'browser-data'),
),
/** 浏览器用户数据目录(不设则自动解析,见 browser.js resolveUserDataDir */
browserUserDataDir: envStr('BROWSER_USER_DATA_DIR', undefined),
/** 是否无头模式 */
browserHeadless: envBool('BROWSER_HEADLESS', false),