feat: 新增 debugOpts 支持配置 Puppeteer 信号处理行为,优化 demo 中 Ctrl+C 处理逻辑以保持浏览器可复用

This commit is contained in:
WJZ_P
2026-03-16 01:50:06 +08:00
parent aa1fc46875
commit 9b6fa6f829
3 changed files with 40 additions and 4 deletions

View File

@@ -286,9 +286,19 @@ async function connectBrowser(port) {
* @param {number} opts.port * @param {number} opts.port
* @param {string} opts.userDataDir * @param {string} opts.userDataDir
* @param {boolean} opts.headless * @param {boolean} opts.headless
* @param {object} [opts.debugOpts] - 调试/信号控制选项
* @param {boolean} [opts.debugOpts.handleSIGINT=true] - Puppeteer 是否在 SIGINT 时自动关闭浏览器
* @param {boolean} [opts.debugOpts.handleSIGTERM=true] - Puppeteer 是否在 SIGTERM 时自动关闭浏览器
* @param {boolean} [opts.debugOpts.handleSIGHUP=true] - Puppeteer 是否在 SIGHUP 时自动关闭浏览器
* @returns {Promise<import('puppeteer-core').Browser>} * @returns {Promise<import('puppeteer-core').Browser>}
*/ */
async function launchBrowser({ executablePath, port, userDataDir, headless }) { async function launchBrowser({ executablePath, port, userDataDir, headless, debugOpts = {} }) {
const {
handleSIGINT = true,
handleSIGTERM = true,
handleSIGHUP = true,
} = debugOpts;
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
executablePath, executablePath,
headless, headless,
@@ -300,6 +310,9 @@ async function launchBrowser({ executablePath, port, userDataDir, headless }) {
], ],
ignoreDefaultArgs: ['--enable-automation'], ignoreDefaultArgs: ['--enable-automation'],
protocolTimeout: config.browserProtocolTimeout, protocolTimeout: config.browserProtocolTimeout,
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
}); });
console.log('[browser] launched, pid:', browser.process()?.pid, 'port:', port, 'path:', executablePath); console.log('[browser] launched, pid:', browser.process()?.pid, 'port:', port, 'path:', executablePath);
return browser; return browser;
@@ -349,6 +362,7 @@ async function findOrCreateGeminiPage(browser) {
* @param {number} [opts.port] - 调试端口env: BROWSER_DEBUG_PORT默认 9222 * @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 * @param {boolean} [opts.headless] - 无头模式env: BROWSER_HEADLESS默认 false
* @param {object} [opts.debugOpts] - 调试/信号控制选项(透传给 Puppeteer launch
* @returns {Promise<{browser: import('puppeteer-core').Browser, page: import('puppeteer-core').Page}>} * @returns {Promise<{browser: import('puppeteer-core').Browser, page: import('puppeteer-core').Page}>}
*/ */
export async function ensureBrowser(opts = {}) { export async function ensureBrowser(opts = {}) {
@@ -357,6 +371,7 @@ export async function ensureBrowser(opts = {}) {
port = config.browserDebugPort, port = config.browserDebugPort,
userDataDir = resolveUserDataDir(), userDataDir = resolveUserDataDir(),
headless = config.browserHeadless, headless = config.browserHeadless,
debugOpts,
} = opts; } = opts;
// 1. 复用已有连接 // 1. 复用已有连接
@@ -393,7 +408,7 @@ export async function ensureBrowser(opts = {}) {
} }
try { try {
_browser = await launchBrowser({ executablePath: resolvedPath, port, userDataDir, headless }); _browser = await launchBrowser({ executablePath: resolvedPath, port, userDataDir, headless, debugOpts });
} catch (err) { } catch (err) {
// 大概率是用户数据目录被正在运行的浏览器锁住了 // 大概率是用户数据目录被正在运行的浏览器锁住了
if (err.message?.includes('EPERM') || err.message?.includes('lock') || err.message?.includes('already')) { if (err.message?.includes('EPERM') || err.message?.includes('lock') || err.message?.includes('already')) {

View File

@@ -50,8 +50,18 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
* 创建会话,如果因浏览器目录被锁而失败,自动杀掉全部浏览器进程后重试一次 * 创建会话,如果因浏览器目录被锁而失败,自动杀掉全部浏览器进程后重试一次
*/ */
async function createSessionWithRetry() { async function createSessionWithRetry() {
// 禁止 Puppeteer 在 Ctrl+C 等信号时自动杀浏览器进程,
// 由 demo 自己处理 SIGINT → disconnect浏览器保持运行可复用。
const opts = {
debugOpts: {
handleSIGINT: false,
handleSIGTERM: false,
handleSIGHUP: false,
},
};
try { try {
return await createGeminiSession(); return await createGeminiSession(opts);
} catch (err) { } catch (err) {
const msg = err.message || ''; const msg = err.message || '';
const isLocked = msg.includes('EPERM') || msg.includes('lock') || msg.includes('already'); const isLocked = msg.includes('EPERM') || msg.includes('lock') || msg.includes('already');
@@ -67,7 +77,7 @@ async function createSessionWithRetry() {
await sleep(2000); await sleep(2000);
// 重试一次,还失败就直接抛出 // 重试一次,还失败就直接抛出
return await createGeminiSession(); return await createGeminiSession(opts);
} }
} }
@@ -77,6 +87,13 @@ async function main() {
// 创建会话:自带杀进程重试逻辑 // 创建会话:自带杀进程重试逻辑
const { ops } = await createSessionWithRetry(); const { ops } = await createSessionWithRetry();
// ── Ctrl+C 时只断开连接,不杀浏览器进程(下次可复用) ──
process.on('SIGINT', () => {
console.log('\n[demo] Ctrl+C 收到,断开浏览器连接(浏览器保持运行)...');
disconnect();
process.exit(0);
});
try { try {
// 1. 探测页面状态 // 1. 探测页面状态
console.log('[1] 探测页面元素...'); console.log('[1] 探测页面元素...');

View File

@@ -30,6 +30,10 @@ export { disconnect, close };
* @param {number} [opts.port] - 调试端口env: BROWSER_DEBUG_PORT默认 9222 * @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 * @param {boolean} [opts.headless] - 无头模式env: BROWSER_HEADLESS默认 false
* @param {object} [opts.debugOpts] - 调试/信号控制选项(透传给 Puppeteer launch
* @param {boolean} [opts.debugOpts.handleSIGINT=true] - Puppeteer 是否在 SIGINT 时自动关闭浏览器
* @param {boolean} [opts.debugOpts.handleSIGTERM=true] - Puppeteer 是否在 SIGTERM 时自动关闭浏览器
* @param {boolean} [opts.debugOpts.handleSIGHUP=true] - Puppeteer 是否在 SIGHUP 时自动关闭浏览器
* @returns {Promise<{ops: ReturnType<typeof createOps>, page: import('puppeteer-core').Page, browser: import('puppeteer-core').Browser}>} * @returns {Promise<{ops: ReturnType<typeof createOps>, page: import('puppeteer-core').Page, browser: import('puppeteer-core').Browser}>}
*/ */
export async function createGeminiSession(opts = {}) { export async function createGeminiSession(opts = {}) {