From 2c3bc79e6e5a0b54148bbb84534dd9139958f203 Mon Sep 17 00:00:00 2001 From: xiongxiao Date: Thu, 1 Jan 2026 22:04:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E9=A1=B9?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20user-agents=20=E5=BA=93=EF=BC=9B?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B5=8F=E8=A7=88=E5=99=A8=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E6=97=A0=E5=A4=B4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=92=8C=E9=9A=90=E8=BA=AB=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 23 ++++++++++++ src/playwright/browser.ts | 41 ++++++++++++++++---- src/playwright/core.ts | 79 ++++++++++++++++++++++++++++++++++++++- src/routes/good/index.ts | 6 ++- 5 files changed, 140 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f5d2f55..2f0b374 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "better-sqlite3": "^12.5.0", "playwright": "^1.57.0", + "user-agents": "^1.1.669", "zod": "^4.2.1", "zod-to-json-schema": "^3.25.1" }, @@ -52,6 +53,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.5", "@types/node": "^25.0.3", + "@types/user-agents": "^1.0.4", "commander": "^14.0.2", "dotenv": "^17.2.3", "drizzle-kit": "^0.31.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faae7f0..aa79619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: playwright: specifier: ^1.57.0 version: 1.57.0 + user-agents: + specifier: ^1.1.669 + version: 1.1.669 zod: specifier: ^4.2.1 version: 4.2.1 @@ -45,6 +48,9 @@ importers: '@types/node': specifier: ^25.0.3 version: 25.0.3 + '@types/user-agents': + specifier: ^1.0.4 + version: 1.0.4 commander: specifier: ^14.0.2 version: 14.0.2 @@ -436,6 +442,9 @@ packages: '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/user-agents@1.0.4': + resolution: {integrity: sha512-AjeFc4oX5WPPflgKfRWWJfkEk7Wu82fnj1rROPsiqFt6yElpdGFg8Srtm/4PU4rA9UiDUZlruGPgcwTMQlwq4w==} + asn1js@3.0.7: resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} engines: {node: '>=12.0.0'} @@ -674,6 +683,9 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -838,6 +850,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + user-agents@1.1.669: + resolution: {integrity: sha512-pbIzG+AOqCaIpySKJ4IAm1l0VyE4jMnK4y1thV8lm8PYxI+7X5uWcppOK7zY79TCKKTAnJH3/4gaVIZHsjrmJA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1142,6 +1157,8 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/user-agents@1.0.4': {} + asn1js@3.0.7: dependencies: pvtsutils: 1.3.6 @@ -1321,6 +1338,8 @@ snapshots: ini@1.3.8: {} + lodash.clonedeep@4.5.0: {} + lru-cache@11.2.4: {} mime-db@1.54.0: {} @@ -1498,6 +1517,10 @@ snapshots: undici-types@7.16.0: {} + user-agents@1.1.669: + dependencies: + lodash.clonedeep: 4.5.0 + util-deprecate@1.0.2: {} wrappy@1.0.2: {} diff --git a/src/playwright/browser.ts b/src/playwright/browser.ts index 78f8fc8..4b536d1 100644 --- a/src/playwright/browser.ts +++ b/src/playwright/browser.ts @@ -1,6 +1,8 @@ import { spawn } from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; +import UserAgent from 'user-agents'; +import { chromium } from 'playwright'; export const getExecutablePath = () => { // 根据不同平台返回 Chrome 的可执行文件路径 @@ -33,22 +35,48 @@ export const main = async (opts?: { userDataDir?: string; debugPort?: number; kiosk?: boolean; + headless?: boolean; }) => { // Chrome 路径和配置 - const executablePath = opts?.executablePath || getExecutablePath(); + let executablePath = opts?.executablePath || getExecutablePath(); // 使用独立的用户数据目录,避免与 Chrome 冲突 const userDataDir = opts?.userDataDir || path.join(process.cwd(), 'browser-context'); const debugPort = opts?.debugPort || 9223; + const headless = opts?.headless || false; console.log('启动 Chrome...', executablePath); console.log(`端口: ${debugPort}`); console.log(`用户数据目录: ${userDataDir}`); - // console.log('注意:需要手动登录账号和安装插件'); + console.log(`无头模式: ${headless}`); + const userAgent = new UserAgent().toString(); + const params = [ `--remote-debugging-port=${debugPort}`, `--user-data-dir=${userDataDir}`, - // '--kiosk', // 全屏模式,无修改边框 + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--no-first-run', + `--user-agent=${userAgent}`, ]; + + if (headless) { + params.push( + '--headless', + '--disable-blink-features=AutomationControlled', + '--disable-infobars', + '--disable-features=IsolateOrigins,site-per-process', + '--disable-features=VizDisplayCompositor', + '--window-size=1920,1080', + '--disable-background-networking', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--disable-component-extensions-with-background-pages', + '--disable-features=TranslateUI', + '--disable-ipc-flooding-protection', + ); + } console.log('启动参数:', params); if (opts?.kiosk) { params.push('--kiosk'); // 全屏模式,无修改边框 @@ -62,13 +90,12 @@ export const main = async (opts?: { return; } - // 检查 Chrome 可执行文件是否存在 + // 检查 Chrome 可执行文件是否存在,不存在则使用 Playwright 的浏览器 if (!fs.existsSync(executablePath)) { - console.error('Chrome 可执行文件不存在:', executablePath); - return; + console.log('Chrome 可执行文件不存在,使用 Playwright 的浏览器'); + executablePath = chromium.executablePath(); } - // 启动 Chrome(带远程调试端口) const chromeProcess = spawn( executablePath, diff --git a/src/playwright/core.ts b/src/playwright/core.ts index 672416e..cdd6b70 100644 --- a/src/playwright/core.ts +++ b/src/playwright/core.ts @@ -27,13 +27,14 @@ export class Core { page: Page | null = null; debugPort = 9223; debugHost = '127.0.0.1'; + headless = false; status: 'disconnected' | 'connecting' | 'connected' | 'failed' = 'disconnected'; emitter = new EventEmitter(); listeners: Listener[] = []; recordReady: boolean = false; timer: NodeJS.Timeout | null = null; data: T | null = null; - constructor(opts?: { debugPort?: number, debugHost?: string, listeners?: Listener[] }) { + constructor(opts?: { debugPort?: number, debugHost?: string, listeners?: Listener[], headless?: boolean }) { if (opts?.debugPort) { this.debugPort = opts.debugPort; } @@ -43,9 +44,12 @@ export class Core { if (opts?.listeners) { this.listeners = opts.listeners; } + if (opts?.headless !== undefined) { + this.headless = opts.headless; + } } async createBrowser() { - await main({ debugPort: this.debugPort }); + await main({ debugPort: this.debugPort, headless: this.headless }); } async init() { const debugPort = this.debugPort; @@ -59,6 +63,9 @@ export class Core { this.browserContext = browser.contexts()[0]; this.handleRequest(this.browserContext); this.page = this.browserContext.pages()[0] || await this.browserContext.newPage(); + if (this.headless) { + await this.stealthMode(this.page); + } this.emitter.emit('connected'); return; } catch (error: any) { @@ -147,6 +154,74 @@ export class Core { } this.data = data; } + async stealthMode(page: Page) { + const stealthScript = ` + () => { + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined, + }); + + window.chrome = { + runtime: {}, + }; + + Object.defineProperty(navigator, 'plugins', { + get: () => [1, 2, 3, 4, 5], + }); + + Object.defineProperty(navigator, 'languages', { + get: () => ['en-US', 'en'], + }); + + const originalQuery = window.navigator.permissions.query; + window.navigator.permissions.query = (parameters) => ( + parameters.name === 'notifications' ? + Promise.resolve({ state: Notification.permission }) : + originalQuery(parameters) + ); + + Object.defineProperty(navigator, 'hardwareConcurrency', { + get: () => 4, + }); + + Object.defineProperty(navigator, 'deviceMemory', { + get: () => 8, + }); + + const originalGetContext = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function(type) { + const context = originalGetContext.apply(this, arguments); + if (type === '2d' && context) { + const originalGetImageData = context.getImageData; + context.getImageData = function() { + const imageData = originalGetImageData.apply(this, arguments); + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i] = imageData.data[i] + Math.random() * 0.1 - 0.05; + } + return imageData; + }; + } + return context; + }; + + Object.defineProperty(navigator, 'connection', { + get: () => ({ + effectiveType: '4g', + rtt: 100, + downlink: 10, + }), + }); + + window.navigator.getBattery = () => Promise.resolve({ + charging: true, + chargingTime: 0, + dischargingTime: Infinity, + level: 1, + }); + } + `; + await page.addInitScript(stealthScript); + } async handleRequest(context: BrowserContext) { context.on('request', request => { const url = request.url(); diff --git a/src/routes/good/index.ts b/src/routes/good/index.ts index faca073..f3f26f9 100644 --- a/src/routes/good/index.ts +++ b/src/routes/good/index.ts @@ -10,13 +10,14 @@ app.route({ icon: 'search', } }).define(async (ctx) => { - const keyword = ctx.query?.keyword as string || '信息差'; + const { keyword = '信息差', ...rest } = ctx.query; const res = await app.run({ path: 'xhs', key: 'search-notes', payload: { keyword: keyword, scrollTimes: 5, + ...rest, token: ctx.query?.token as string, } }) @@ -34,13 +35,14 @@ app.route({ icon: 'search', } }).define(async (ctx) => { - const keyword = ctx.query?.keyword as string || '工作 杭州'; + const { keyword = '工作 杭州', ...rest } = ctx.query; const res = await app.run({ path: 'xhs', key: 'search-notes', payload: { keyword: keyword, scrollTimes: 5, + ...rest, token: ctx.query?.token as string, } })