refactor(browser): 使用全局数据目录并支持克隆

This commit is contained in:
WJZ_P
2026-03-17 00:43:40 +08:00
parent 33147c3610
commit 394a589a24
2 changed files with 92 additions and 54 deletions

View File

@@ -16,10 +16,9 @@ import puppeteerCore from 'puppeteer-core';
import { addExtra } from 'puppeteer-extra'; import { addExtra } from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth'; import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { createConnection } from 'node:net'; import { createConnection } from 'node:net';
import { existsSync, mkdirSync } from 'node:fs'; import { existsSync, mkdirSync, cpSync } from 'node:fs';
import { platform, homedir } from 'node:os'; import { platform, homedir } from 'node:os';
import { join } from 'node:path'; import { join, basename } from 'node:path';
import { execFileSync } from 'node:child_process';
import config from './config.js'; import config from './config.js';
// ── 用 puppeteer-extra 包装 puppeteer-core注入 stealth 插件 ── // ── 用 puppeteer-extra 包装 puppeteer-core注入 stealth 插件 ──
@@ -93,36 +92,17 @@ function detectBrowser() {
return undefined; return undefined;
} }
// ── userDataDir 兜底目录 ── // ── userDataDirWJZ_P 全局浏览器数据目录 ──
const SKILL_FALLBACK_DATA_DIR = join(homedir(), '.gemini-skill', 'browser-data'); // 所有伟大的 WJZ_P 项目共享同一个浏览器数据目录,保证 cookie / 登录态跨项目统一。
// 不使用浏览器默认数据目录的原因:
// - macOS 下 Chrome 不能用默认路径开启 debug 模式(数据目录被锁)
// - 独立目录保证与日常浏览器完全隔离,反爬更安全
const GLOBAL_WJZ_DATA_DIR = join(homedir(), '.wjz_browser_data');
/** /**
* 尝试从 OpenClaw 获取 userDataDir * 获取浏览器默认 userDataDir 路径(作为克隆源)
* *
* 执行: openclaw browser --browser-profile openclaw status --json * 按优先级尝试 Chrome > Edge > Chromium返回第一个存在的路径。
* 解析返回的 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} * @returns {string | undefined}
*/ */
@@ -166,13 +146,76 @@ function getDefaultBrowserDataDir() {
} }
/** /**
* 多级兜底解析 userDataDir * 从浏览器默认数据目录克隆关键资产到 WJZ 数据目录
*
* 只拷贝 cookie、登录态、偏好设置等"资产",跳过锁文件和缓存,
* 确保克隆后的目录能正常启动且不与原浏览器实例冲突。
*
* 跳过的文件 / 目录basename 匹配):
* - SingletonLock / SingletonSocket / SingletonCookie — 进程锁,拷贝会导致无法启动
* - lockfile — 锁文件
* - Cache / Code Cache / GPUCache / DawnCache / GrShaderCache — 缓存目录,体积大且不必要
* - CrashpadMetrics-active.pma — 崩溃指标活跃文件
* - BrowserMetrics / BrowserMetrics-spare.pma — 浏览器指标文件
*
* @param {string} sourceDir - 浏览器默认数据目录
* @param {string} targetDir - WJZ 数据目录
*/
function cloneProfileFromDefault(sourceDir, targetDir) {
console.log(`[browser] 首次运行,正在从浏览器默认数据克隆资产...`);
console.log(`[browser] 源:${sourceDir}`);
console.log(`[browser] 目标:${targetDir}`);
/** 需要跳过的文件 / 目录名(全部小写比较) */
const SKIP_NAMES = new Set([
// 进程锁
'singletonlock',
'singletonsocket',
'singletoncookie',
'lockfile',
// 缓存(体积大,浏览器会自动重建)
'cache',
'code cache',
'gpucache',
'dawncache',
'grshadercache',
// 崩溃 / 指标
'crashpadmetrics-active.pma',
'browsermetrics',
'browsermetrics-spare.pma',
]);
/**
* cpSync 的 filter 回调:返回 true 表示拷贝false 表示跳过
* @param {string} src
* @param {string} _dest
* @returns {boolean}
*/
const filterFunc = (src, _dest) => {
const name = basename(src).toLowerCase();
if (SKIP_NAMES.has(name)) {
return false;
}
return true;
};
try {
cpSync(sourceDir, targetDir, { recursive: true, filter: filterFunc });
console.log(`[browser] 克隆完成`);
} catch (err) {
// 克隆失败不致命:目录已创建,浏览器会以全新状态启动(需手动登录)
console.warn(`[browser] ⚠ 克隆过程中出现错误(浏览器仍可启动,但需要重新登录):`, err.message);
}
}
/**
* 解析 userDataDir
* *
* 优先级: * 优先级:
* 1. 环境变量 BROWSER_USER_DATA_DIRconfig 已处理) * 1. 环境变量 BROWSER_USER_DATA_DIRconfig 已处理)
* 2. OpenClaw 运行状态中的 userDataDir * 2. WJZ_P 全局目录 ~/.wjz_browser_data
* 3. 浏览器默认 userDataDirChrome > Edge > Chromium * - 目录已存在 → 直接使用
* 4. Skill 内部创建 ~/.gemini-skill/browser-data兜底 + warning * - 目录不存在(首次运行)→ 创建并从浏览器默认数据目录克隆关键资产
* *
* @returns {string} * @returns {string}
*/ */
@@ -182,29 +225,24 @@ function resolveUserDataDir() {
return config.browserUserDataDir; return config.browserUserDataDir;
} }
// 2. OpenClaw // 2. WJZ_P 全局目录
const openclawDir = getOpenClawUserDataDir(); if (existsSync(GLOBAL_WJZ_DATA_DIR)) {
if (openclawDir) { console.log(`[browser] using WJZ data dir: ${GLOBAL_WJZ_DATA_DIR}`);
return openclawDir; return GLOBAL_WJZ_DATA_DIR;
} }
// 3. 浏览器默认目录 // 首次运行:创建目录并尝试从浏览器默认数据克隆
console.log(`[browser] WJZ data dir not found, initializing: ${GLOBAL_WJZ_DATA_DIR}`);
mkdirSync(GLOBAL_WJZ_DATA_DIR, { recursive: true });
const defaultDir = getDefaultBrowserDataDir(); const defaultDir = getDefaultBrowserDataDir();
if (defaultDir) { if (defaultDir) {
return defaultDir; cloneProfileFromDefault(defaultDir, GLOBAL_WJZ_DATA_DIR);
} else {
console.log('[browser] 未找到浏览器默认数据目录,将使用空白配置(首次启动需手动登录)');
} }
// 4. Skill 兜底 return GLOBAL_WJZ_DATA_DIR;
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;
} }
/** /**
@@ -362,7 +400,7 @@ async function findOrCreateGeminiPage(browser) {
* 3. 否则自动检测 / 使用配置的路径启动浏览器 * 3. 否则自动检测 / 使用配置的路径启动浏览器
* *
* userDataDir 解析优先级: * userDataDir 解析优先级:
* opts.userDataDir > env BROWSER_USER_DATA_DIR > OpenClaw > 浏览器默认 > skill 兜底 * opts.userDataDir > env BROWSER_USER_DATA_DIR > ~/.wjz_browser_data首次自动从浏览器默认数据克隆
* *
* @param {object} [opts] * @param {object} [opts]
* @param {string} [opts.executablePath] - 浏览器路径(不传则自动检测) * @param {string} [opts.executablePath] - 浏览器路径(不传则自动检测)

View File

@@ -111,14 +111,14 @@ export function createOps(page) {
// 等待页面导航 / 内容刷新完成 // 等待页面导航 / 内容刷新完成
try { try {
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout }); await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout });
} catch { } catch {
// 部分场景下按钮不触发 navigation 而是 SPA 内部路由,静默跳过 // 部分场景下按钮不触发 navigation 而是 SPA 内部路由,静默跳过
console.log('[ops] temp chat: navigation wait timed out, continuing (may be SPA routing)'); console.log('[ops] temp chat: navigation wait timed out, continuing (may be SPA routing)');
} }
// 再给一点时间让 UI 稳定 // 再给一点时间让 UI 稳定
await sleep(1000); await sleep(500);
console.log('[ops] entered temp chat mode'); console.log('[ops] entered temp chat mode');
return { ok: true }; return { ok: true };