重构浏览器启动逻辑,优化连接设置,更新依赖项,增强功能和可读性

This commit is contained in:
2025-12-29 19:43:45 +08:00
parent 9067be1293
commit f15f6f14e4
11 changed files with 144 additions and 136 deletions

View File

@@ -17,9 +17,8 @@
"dev": "tsx watch src/index.ts",
"init:browser": "npx playwright install",
"build": "bun run bun.config.ts",
"browser": "pm2 start start-browser.js --name browser-helper",
"browser": "pm2 start start-browser.js --name browser ",
"cmd": "tsx src/test/cmd.ts ",
"pm2": "pm2 start dist/app.js --name /root/browser-helper",
"init": "pnpm run init:pnpm && pnpm run init:db && pnpm run init:browser",
"init:pnpm": "pnpm approve-builds",
"init:db": "npx drizzle-kit push",
@@ -57,7 +56,6 @@
"drizzle-orm": "^0.45.1",
"es-toolkit": "^1.43.0",
"eventemitter3": "^5.0.1",
"lru-cache": "^11.2.4",
"window-size": "^1.1.1"
"lru-cache": "^11.2.4"
}
}

80
pnpm-lock.yaml generated
View File

@@ -60,9 +60,6 @@ importers:
lru-cache:
specifier: ^11.2.4
version: 11.2.4
window-size:
specifier: ^1.1.1
version: 1.1.1
packages:
@@ -487,10 +484,6 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
define-property@1.0.0:
resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==}
engines: {node: '>=0.10.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -656,19 +649,12 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
@@ -682,29 +668,6 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
is-accessor-descriptor@1.0.1:
resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==}
engines: {node: '>= 0.10'}
is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
is-data-descriptor@1.0.1:
resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==}
engines: {node: '>= 0.4'}
is-descriptor@1.0.3:
resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==}
engines: {node: '>= 0.4'}
is-number@3.0.0:
resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==}
engines: {node: '>=0.10.0'}
kind-of@3.2.2:
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
engines: {node: '>=0.10.0'}
lru-cache@11.2.4:
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
engines: {node: 20 || >=22}
@@ -872,11 +835,6 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
window-size@1.1.1:
resolution: {integrity: sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==}
engines: {node: '>= 0.10.0'}
hasBin: true
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@@ -1220,10 +1178,6 @@ snapshots:
deep-extend@0.6.0: {}
define-property@1.0.0:
dependencies:
is-descriptor: 1.0.3
depd@2.0.0: {}
detect-libc@2.1.2: {}
@@ -1333,18 +1287,12 @@ snapshots:
fsevents@2.3.2:
optional: true
function-bind@1.1.2: {}
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
github-from-package@0.0.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -1359,29 +1307,6 @@ snapshots:
ini@1.3.8: {}
is-accessor-descriptor@1.0.1:
dependencies:
hasown: 2.0.2
is-buffer@1.1.6: {}
is-data-descriptor@1.0.1:
dependencies:
hasown: 2.0.2
is-descriptor@1.0.3:
dependencies:
is-accessor-descriptor: 1.0.1
is-data-descriptor: 1.0.1
is-number@3.0.0:
dependencies:
kind-of: 3.2.2
kind-of@3.2.2:
dependencies:
is-buffer: 1.1.6
lru-cache@11.2.4: {}
mime-db@1.54.0: {}
@@ -1561,9 +1486,4 @@ snapshots:
util-deprecate@1.0.2: {}
window-size@1.1.1:
dependencies:
define-property: 1.0.0
is-number: 3.0.0
wrappy@1.0.2: {}

View File

@@ -1,2 +1,3 @@
onlyBuiltDependencies:
- better-sqlite3
- esbuild

92
src/playwright/browser.ts Normal file
View File

@@ -0,0 +1,92 @@
import { spawn } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
export const getExecutablePath = () => {
// 根据不同平台返回 Chrome 的可执行文件路径
switch (process.platform) {
case 'win32':
return 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
case 'darwin':
return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
case 'linux':
return '/usr/bin/google-chrome';
default:
throw new Error('Unsupported platform: ' + process.platform);
}
};
/**
*
* @param {*} opts
* executablePath: 可执行文件路径
* userDataDir: 用户数据目录
* debugPort: 远程调试端口
* kiosk: 是否全屏模式
*
* 启动 Chrome 浏览器,带远程调试端口
* 注意:需要手动登录账号和安装插件
*
* @returns {Promise<void>}
*/
export const main = async (opts?: {
executablePath?: string;
userDataDir?: string;
debugPort?: number;
kiosk?: boolean;
}) => {
// Chrome 路径和配置
const executablePath = opts?.executablePath || getExecutablePath();
// 使用独立的用户数据目录,避免与 Chrome 冲突
const userDataDir = opts?.userDataDir || path.join(process.cwd(), 'browser-context');
const debugPort = opts?.debugPort || 9223;
console.log('启动 Chrome...', executablePath);
console.log(`端口: ${debugPort}`);
console.log(`用户数据目录: ${userDataDir}`);
// console.log('注意:需要手动登录账号和安装插件');
const params = [
`--remote-debugging-port=${debugPort}`,
`--user-data-dir=${userDataDir}`,
// '--kiosk', // 全屏模式,无修改边框
];
console.log('启动参数:', params);
if (opts?.kiosk) {
params.push('--kiosk'); // 全屏模式,无修改边框
}
// 确保用户数据目录存在
try {
await fs.promises.mkdir(userDataDir, { recursive: true });
} catch (err) {
console.error('创建用户数据目录失败:', err);
return;
}
// 检查 Chrome 可执行文件是否存在
if (!fs.existsSync(executablePath)) {
console.error('Chrome 可执行文件不存在:', executablePath);
return;
}
// 启动 Chrome带远程调试端口
const chromeProcess = spawn(
executablePath,
params,
{
windowsHide: true, // 隐藏 CMD 窗口
detached: false,
stdio: ['ignore', 'ignore', 'ignore'],
},
);
chromeProcess.on('error', (err) => {
console.error('Chrome 启动失败:', err);
// 需要重新启动
});
chromeProcess.on('exit', (code, signal) => {
console.log(`Chrome 进程退出,代码: ${code}, 信号: ${signal}`);
});
return chromeProcess;
};

View File

@@ -2,6 +2,7 @@ import { chromium, Page, BrowserContext, Browser, CDPSession, Request } from 'pl
import { execSync } from 'node:child_process';
import { EventEmitter } from 'eventemitter3'
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
import { main } from "./browser.ts";
type RequestObject = {
url: string;
@@ -25,25 +26,33 @@ export class Core<T = {}> {
browser: Browser | null = null;
page: Page | null = null;
debugPort = 9223;
debugHost = '127.0.0.1';
status: 'disconnected' | 'connecting' | 'connected' | 'failed' = 'disconnected';
emitter = new EventEmitter();
listeners: Listener[] = [];
recordReady: boolean = false;
data: T | null = null;
constructor(opts?: { debugPort?: number, listeners?: Listener[] }) {
constructor(opts?: { debugPort?: number, debugHost?: string, listeners?: Listener[] }) {
if (opts?.debugPort) {
this.debugPort = opts.debugPort;
}
if (opts?.debugHost) {
this.debugHost = opts.debugHost;
}
if (opts?.listeners) {
this.listeners = opts.listeners;
}
}
async createBrowser() {
await main({ debugPort: this.debugPort });
}
async init() {
const debugPort = this.debugPort;
try {
const stdout = execSync(`netstat -ano | findstr :${debugPort}`);
console.log(`端口 ${debugPort} 已在监听:\n${stdout}`);
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${debugPort}`);
const debugHost = this.debugHost;
const browser = await chromium.connectOverCDP(`http://${debugHost}:${debugPort}`);
console.log('成功连接到 Chrome CDP!');
this.browser = browser;
this.browserContext = browser.contexts()[0];
@@ -74,13 +83,18 @@ export class Core<T = {}> {
}
if (this.status === 'disconnected' || this.status === 'failed') {
this.status = 'connecting';
for (let i = 0; i < 10; i++) {
for (let i = 0; i < 6; i++) {
if (i === 3) {
console.log('尝试启动浏览器实例...');
await this.createBrowser();
await sleep(5000);
}
try {
await this.init();
this.status = 'connected'
return true;
} catch (e) {
console.log(`尝试 ${i + 1}/10 连接失败: ${(e as Error).message.slice(0, 100)}`);
console.log(`尝试 ${i + 1}/3 连接失败: ${(e as Error).message.slice(0, 100)}`);
await sleep(2000);
}
}

View File

@@ -0,0 +1,25 @@
import { app, core } from "../../app.ts";
app.route({
path: 'browser',
key: 'getBrowser',
middleware: ['auth'],
description: '获取浏览器实例。',
metadata: {
note: "此接口用于获取当前浏览器实例的连接状态和版本信息。开发环境不要使用此接口。最好默认启动一个实例使用pnpm browser",
tags: ['浏览器操作', '核心功能'],
}
}).define(async (ctx) => {
try {
const browser = await core.getBrowser();
const isConnected = browser.isConnected();
const browserVersion = browser.version()
ctx.body = {
isConnected,
version: browserVersion
};
} catch (error) {
ctx.throw!(500, error.message);
}
}).addTo(app);

View File

@@ -1,2 +1,3 @@
import './pane-manager.ts';
import './page.ts';
import './browser.ts'

View File

@@ -7,7 +7,7 @@ app.route({
middleware: ['auth'],
description: '导航到指定页面。参数url (string, 必需) - 目标 URL 地址',
metadata: {
tags: ['浏览器操作'],
tags: ['浏览器操作', '核心功能'],
}
}).define(async (ctx) => {
const url = ctx.query?.url as string;

View File

@@ -40,7 +40,7 @@ app.route({
description: desc,
middleware: ['auth'],
metadata: {
tags: ['浏览器操作', '窗口管理'],
tags: ['浏览器操作', '窗口管理', '核心功能'],
}
}).define(async (ctx) => {
const bounds = ctx.query as Bounds || {};

View File

@@ -1,6 +1,5 @@
import { Command } from "commander";
import { core, program, exit } from './common.ts'
// import size from 'window-size'
const main = async () => {
const connected = await core.connect();
if (!connected) {

View File

@@ -1,45 +1,3 @@
import { spawn } from 'node:child_process';
import path from 'node:path';
import { main } from "./src/playwright/browser.ts";
export const main = async () => {
// Chrome 路径和配置
const executablePath = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
// 使用独立的用户数据目录,避免与 Chrome 冲突
const userDataDir = path.join(process.cwd(), 'browser-context');
const debugPort = 9223;
console.log('启动 Chrome...');
console.log(`端口: ${debugPort}`);
console.log(`用户数据目录: ${userDataDir}`);
// console.log('注意:需要手动登录账号和安装插件');
// 启动 Chrome带远程调试端口
const chromeProcess = spawn(
executablePath,
[
`--remote-debugging-port=${debugPort}`,
`--user-data-dir=${userDataDir}`,
// '--kiosk', // 全屏模式,无修改边框
],
{
windowsHide: true, // 隐藏 CMD 窗口
detached: false,
stdio: ['ignore', 'ignore', 'ignore'],
},
);
chromeProcess.on('error', (err) => {
console.error('Chrome 启动失败:', err);
// 需要重新启动
});
chromeProcess.on('exit', (code, signal) => {
console.log(`Chrome 进程退出,代码: ${code}, 信号: ${signal}`);
if (code === 0) {
// 重启
main();
}
});
};
main();
await main()