diff --git a/package.json b/package.json index a9277b8..b6ad98f 100644 --- a/package.json +++ b/package.json @@ -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" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30304cb..49cdac8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e4a4b5b..f2721cf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ onlyBuiltDependencies: - better-sqlite3 + - esbuild diff --git a/src/playwright/browser.ts b/src/playwright/browser.ts new file mode 100644 index 0000000..78f8fc8 --- /dev/null +++ b/src/playwright/browser.ts @@ -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} + */ +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; +}; \ No newline at end of file diff --git a/src/playwright/core.ts b/src/playwright/core.ts index f716308..7284b00 100644 --- a/src/playwright/core.ts +++ b/src/playwright/core.ts @@ -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 { 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 { } 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); } } diff --git a/src/routes/browser/browser.ts b/src/routes/browser/browser.ts new file mode 100644 index 0000000..df696ff --- /dev/null +++ b/src/routes/browser/browser.ts @@ -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); \ No newline at end of file diff --git a/src/routes/browser/index.ts b/src/routes/browser/index.ts index a34baff..b6f3e64 100644 --- a/src/routes/browser/index.ts +++ b/src/routes/browser/index.ts @@ -1,2 +1,3 @@ import './pane-manager.ts'; -import './page.ts'; \ No newline at end of file +import './page.ts'; +import './browser.ts' \ No newline at end of file diff --git a/src/routes/browser/page.ts b/src/routes/browser/page.ts index e73d20c..6b91f95 100644 --- a/src/routes/browser/page.ts +++ b/src/routes/browser/page.ts @@ -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; diff --git a/src/routes/browser/pane-manager.ts b/src/routes/browser/pane-manager.ts index 7f424a9..d9c3edf 100644 --- a/src/routes/browser/pane-manager.ts +++ b/src/routes/browser/pane-manager.ts @@ -40,7 +40,7 @@ app.route({ description: desc, middleware: ['auth'], metadata: { - tags: ['浏览器操作', '窗口管理'], + tags: ['浏览器操作', '窗口管理', '核心功能'], } }).define(async (ctx) => { const bounds = ctx.query as Bounds || {}; diff --git a/src/test/pane.ts b/src/test/pane.ts index f7907a9..4fbe2ed 100644 --- a/src/test/pane.ts +++ b/src/test/pane.ts @@ -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) { diff --git a/start-browser.js b/start-browser.js index 6ba950c..80b9b8c 100644 --- a/start-browser.js +++ b/start-browser.js @@ -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() \ No newline at end of file