@tocha688/browser 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,45 +0,0 @@
1
- import path from "path";
2
- import { chromium } from "patchright-core";
3
- import { AutoHelper } from "../actions/auto";
4
- import type { BrowserStartOptions, ProxyServerResult, StartBrowserFn } from "../types";
5
-
6
- export function makeUniqueBrowserConfig() {
7
- const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
8
- const userDataDir = path.join(process.cwd(), "tmps", `brw-session-${sessionId}`);
9
- return { sessionId, userDataDir };
10
- }
11
-
12
-
13
- export async function getBrowserWebSocketUrl(port: number): Promise<string> {
14
- const res = await fetch(`http://127.0.0.1:${port}/json/version`).then(x => x.json()) as any
15
- return res?.webSocketDebuggerUrl || "";
16
- }
17
-
18
-
19
- export async function createBrowser(startBrowserMain: StartBrowserFn, opts: BrowserStartOptions) {
20
- const brwMain = await startBrowserMain({ ...opts });
21
- const wsUrl = await getBrowserWebSocketUrl(brwMain.port);
22
- if (!wsUrl) {
23
- brwMain.close();
24
- throw new Error("无法获取浏览器WebSocket URL");
25
- }
26
- try {
27
- // console.log(wsUrl)
28
- const brw = await chromium.connectOverCDP(wsUrl);
29
- const page = brw.contexts()[0]?.pages()?.[0] ?? (await brw.newPage());
30
- page.setDefaultNavigationTimeout(60000);
31
- page.setDefaultTimeout(60000);
32
- const auto = new AutoHelper(page);
33
- (brw as any)._hk_close = brw.close;
34
- brw.close = async function () {
35
- await (brw as any)._hk_close();
36
- await brwMain.close();
37
- }
38
- return { page, auto, brw }
39
- } catch (e) {
40
- console.log(e)
41
- await brwMain.close();
42
- return await createBrowser(startBrowserMain, opts);
43
- }
44
- }
45
-
package/src/core/pool.ts DELETED
@@ -1,70 +0,0 @@
1
- import { createPool, Pool } from 'generic-pool';
2
- import type { BrowserStartOptions, StartBrowserFn } from '../types';
3
- import { createBrowser } from './launcher';
4
- import type { Browser, Page } from 'patchright-core';
5
- import type { AutoHelper } from '../actions/auto';
6
-
7
- export interface PoolOptions {
8
- min?: number;
9
- max?: number;
10
- testOnBorrow?: boolean;
11
- idleTimeoutMillis?: number;
12
- }
13
-
14
- export interface PooledBrowser {
15
- page: Page;
16
- auto: AutoHelper;
17
- brw: Browser;
18
- release: () => Promise<void>;
19
- }
20
-
21
- export class BrowserPool {
22
- private pool: Pool<Awaited<ReturnType<typeof createBrowser>>>;
23
-
24
- constructor(
25
- private startFn: StartBrowserFn,
26
- private opts: BrowserStartOptions,
27
- poolOpts: PoolOptions = {}
28
- ) {
29
- this.pool = createPool({
30
- create: async () => {
31
- return await createBrowser(this.startFn, this.opts);
32
- },
33
- destroy: async (client) => {
34
- await client.brw.close();
35
- },
36
- validate: async (client) => {
37
- return client.brw.isConnected();
38
- }
39
- }, {
40
- min: poolOpts.min ?? 1,
41
- max: poolOpts.max ?? 5,
42
- testOnBorrow: poolOpts.testOnBorrow ?? true,
43
- idleTimeoutMillis: poolOpts.idleTimeoutMillis ?? 30000,
44
- });
45
- }
46
-
47
- async acquire(): Promise<PooledBrowser> {
48
- const client = await this.pool.acquire();
49
- return {
50
- ...client,
51
- release: async () => {
52
- await this.pool.release(client);
53
- }
54
- };
55
- }
56
-
57
- async drain() {
58
- await this.pool.drain();
59
- await this.pool.clear();
60
- }
61
-
62
- async use<T>(fn: (browser: PooledBrowser) => Promise<T>): Promise<T> {
63
- const resource = await this.acquire();
64
- try {
65
- return await fn(resource);
66
- } finally {
67
- await resource.release();
68
- }
69
- }
70
- }
@@ -1,308 +0,0 @@
1
- // const { Bezier } = require('bezier-js');
2
- // const { sleep } = require('timers/promises');
3
- import { sleep } from '@tocha688/utils';
4
- import { rInt } from '@tocha688/utils/random';
5
- import { Bezier } from 'bezier-js';
6
- import type { Locator, Page } from 'patchright-core';
7
-
8
- /** 坐标点定义 */
9
- type Point = {
10
- /** X坐标 */
11
- x: number;
12
- /** Y坐标 */
13
- y: number;
14
- };
15
-
16
- /** 范围定义,用于指定元素内的随机点范围 */
17
- type Range = {
18
- /** 最小边界比例 (0-1) */
19
- min?: number;
20
- /** 最大边界比例 (0-1) */
21
- max?: number;
22
- };
23
-
24
- /** 鼠标移动配置 */
25
- type Config = {
26
- /** 最小移动步数 */
27
- minSteps: number;
28
- /** 最大移动步数 */
29
- maxSteps: number;
30
- };
31
-
32
- /** 延迟和移动选项 */
33
- type DelayOptions = {
34
- /** 移动步数,覆盖默认配置 */
35
- steps?: number;
36
- /** 移动速度 (0-1),越大越快 */
37
- speed?: number;
38
- /** 随机性 (0-1),越大路径越随机 */
39
- randomness?: number;
40
- };
41
-
42
- /** 移动到元素的选项,结合了延迟选项和范围选项 */
43
- type MoveOptions = DelayOptions & Range;
44
-
45
- /** 鼠标按键类型 */
46
- type MouseButton = 'left' | 'right' | 'middle';
47
-
48
- /** 点击选项 */
49
- export type ClickOptions = DelayOptions & {
50
- /** 是否执行双击 */
51
- doubleClick?: boolean;
52
- /** 鼠标按键 */
53
- button?: MouseButton;
54
- };
55
-
56
- /** 点击元素的选项,结合了点击选项和范围选项 */
57
- type ClickElementOptions = ClickOptions & Range;
58
-
59
- /**
60
- * 模拟真人鼠标移动和点击的类
61
- * 使用贝塞尔曲线生成自然的鼠标轨迹,避免被检测为机器人
62
- */
63
- export class HumanMouse {
64
- /** Playwright页面实例 */
65
- page: Page;
66
- /** 当前鼠标位置 */
67
- currentPosition: Point = { x: 0, y: 0 };
68
- /** 移动配置 */
69
- config: Config = { minSteps: 15, maxSteps: 50 };
70
-
71
- /**
72
- * 构造函数
73
- * @param page Playwright页面实例
74
- * @param config 可选的配置参数
75
- */
76
- constructor(page: Page, config: Partial<Config> = {}) {
77
- this.page = page;
78
- this.currentPosition = { x: 0, y: 0 };
79
- this.config = {
80
- minSteps: 15,
81
- maxSteps: 50,
82
- ...config
83
- };
84
- }
85
-
86
- /**
87
- * 生成随机步数
88
- * @returns 在配置范围内的随机步数
89
- */
90
- private _randomSteps(): number {
91
- return Math.floor(Math.random() *
92
- (this.config.maxSteps - this.config.minSteps)) + this.config.minSteps;
93
- }
94
-
95
- /**
96
- * 计算元素内的随机点
97
- * @param el 目标元素
98
- * @param options 范围选项
99
- * @returns 元素内的随机坐标点
100
- */
101
- private async _toPoint(el: Locator, options?: Range): Promise<Point> {
102
- const { min = 0.2, max = 0.8 } = options || {};
103
- const box = await el.boundingBox();
104
- if (!box) {
105
- throw new Error('Element has no bounding box');
106
- }
107
- return {
108
- x: box.x + box.width * (min + (Math.random() * (max - min))),
109
- y: box.y + box.height * (min + (Math.random() * (max - min)))
110
- };
111
- }
112
-
113
- /**
114
- * 模拟真人鼠标绝对移动
115
- * @param x 目标X坐标
116
- * @param y 目标Y坐标
117
- * @param options 移动选项
118
- */
119
- async moveTo(x: number, y: number, options: DelayOptions = {}): Promise<void> {
120
- const { speed = 0.5, randomness = 0.3 } = options;
121
- const start = this.currentPosition;
122
- const end = { x, y };
123
- // 生成贝塞尔曲线控制点
124
- const controlPoints = this.generateControlPoints(start, end, randomness);
125
- //@ts-ignore
126
- const curve = new Bezier(...controlPoints);
127
- const steps = options.steps || this._randomSteps();
128
- const points = curve.getLUT(steps);
129
-
130
- for (let i = 0; i < points.length; i++) {
131
- const point = points[i] as Point;
132
- const delay = this.getDynamicDelay(i, points.length, speed);
133
-
134
- await this.page.mouse.move(point.x, point.y);
135
- await sleep(delay);
136
- }
137
- this.currentPosition = end;
138
- }
139
-
140
- /**
141
- * 模拟真人鼠标移动到指定元素
142
- * @param el 目标元素
143
- * @param options 移动和范围选项
144
- */
145
- async moveToEl(el: Locator, options: MoveOptions = {}): Promise<void> {
146
- const point = await this._toPoint(el, options);
147
- await this.moveTo(point.x, point.y, options);
148
- }
149
-
150
- /**
151
- * 模拟真人鼠标相对移动
152
- * @param deltaX X轴偏移量
153
- * @param deltaY Y轴偏移量
154
- * @param options 移动和范围选项
155
- */
156
- async moveBy(deltaX: number, deltaY: number, options: MoveOptions = {}): Promise<void> {
157
- const targetX = this.currentPosition.x + deltaX;
158
- const targetY = this.currentPosition.y + deltaY;
159
- return this.moveTo(targetX, targetY, options);
160
- }
161
-
162
- async scrollBy(deltaY: number, options: DelayOptions = {}): Promise<void> {
163
- const { speed = 0.5, randomness = 0.3 } = options;
164
- const steps = options.steps || this._randomSteps();
165
- const baseStep = deltaY / steps;
166
- for (let i = 0; i < steps; i++) {
167
- const jitter = baseStep * (Math.random() - 0.5) * randomness;
168
- const stepY = baseStep + jitter;
169
- //@ts-ignore
170
- await this.page.evaluate(`(y) => { window.scrollBy(0, y); }`, stepY);
171
- const delay = this.getDynamicDelay(i, steps, speed);
172
- await sleep(delay);
173
- }
174
- }
175
-
176
- async scrollTo(y: number, options: DelayOptions = {}): Promise<void> {
177
- const current = await this.page.evaluate(`() => window.scrollY || document.documentElement.scrollTop || 0`) as number;
178
- const delta = y - current;
179
- if (Math.abs(delta) < 1) return;
180
- await this.scrollBy(delta, options);
181
- }
182
-
183
- async scrollToEl(el: Locator, options: DelayOptions = {}): Promise<void> {
184
- const viewport = this.page.viewportSize();
185
- //@ts-ignore
186
- const viewportHeight = viewport?.height ?? await this.page.evaluate(`() => window.innerHeight`) as any;
187
- const metrics = await el.evaluate(`(node) => {
188
- const rect = node.getBoundingClientRect();
189
- return { top: rect.top + window.scrollY, height: rect.height };
190
- }`) as any;
191
- const target = Math.max(0, metrics.top - viewportHeight * 0.3);
192
- await this.scrollTo(target, options);
193
- }
194
-
195
- /**
196
- * 模拟真人鼠标单击
197
- * @param x 目标X坐标
198
- * @param y 目标Y坐标
199
- * @param options 点击选项
200
- */
201
- async click(x: number, y: number, options: ClickOptions = {}): Promise<void> {
202
- const { doubleClick = false, button = 'left' } = options;
203
- await this.moveTo(x, y, {
204
- steps: options.steps,
205
- speed: options.speed,
206
- randomness: options.randomness
207
- });
208
- await this.page.mouse.down({ button });
209
- await sleep(rInt(50, 150));
210
- await this.page.mouse.up({ button });
211
-
212
- if (doubleClick) {
213
- await sleep(rInt(100, 300));
214
- await this.page.mouse.down({ button });
215
- await sleep(rInt(50, 150));
216
- await this.page.mouse.up({ button });
217
- }
218
- }
219
-
220
- /**
221
- * 模拟真人鼠标单击指定元素
222
- * @param el 目标元素
223
- * @param options 点击和范围选项
224
- */
225
- async clickEl(el: Locator, options: ClickElementOptions = {}): Promise<void> {
226
- try {
227
- const point = await this._toPoint(el, options);
228
- await this.click(point.x, point.y, options);
229
- } catch (err) {
230
- console.log('clickEl err: ', err, 'el: ', el);
231
- }
232
- }
233
-
234
- /**
235
- * 生成贝塞尔曲线控制点
236
- * @param start 起始点
237
- * @param end 终点
238
- * @param randomness 随机性程度
239
- * @returns 四个控制点组成的数组
240
- */
241
- generateControlPoints(start: Point, end: Point, randomness: number): Point[] {
242
- const distance = Math.hypot(end.x - start.x, end.y - start.y);
243
- const cp1 = {
244
- x: start.x + (end.x - start.x) * 0.3 + (Math.random() - 0.5) * distance * randomness,
245
- y: start.y + (end.y - start.y) * 0.3 + (Math.random() - 0.5) * distance * randomness
246
- };
247
- const cp2 = {
248
- x: end.x - (end.x - start.x) * 0.3 + (Math.random() - 0.5) * distance * randomness,
249
- y: end.y - (end.y - start.y) * 0.3 + (Math.random() - 0.5) * distance * randomness
250
- };
251
- return [start, cp1, cp2, end];
252
- }
253
-
254
- /**
255
- * 计算动态延迟,模拟加速减速效果
256
- * @param index 当前步骤索引
257
- * @param totalPoints 总步数
258
- * @param speed 速度参数
259
- * @returns 延迟时间(毫秒)
260
- */
261
- getDynamicDelay(index: number, totalPoints: number, speed: number): number {
262
- // 模拟加速减速效果
263
- const progress = index / totalPoints;
264
- const baseDelay = 10 + (1 - speed) * 50;
265
- return Math.max(10, baseDelay * (1 - Math.sin(progress * Math.PI)) * 0.5);
266
- }
267
-
268
- /**
269
- * 生成指定范围内的随机延迟
270
- * @param min 最小延迟(毫秒)
271
- * @param max 最大延迟(毫秒)
272
- * @returns 随机延迟时间(毫秒)
273
- */
274
- getRandomDelay(min = 20, max = 100): number {
275
- return Math.floor(Math.random() * (max - min + 1)) + min;
276
- }
277
-
278
- /**
279
- * 在页面上显示鼠标轨迹,用于调试
280
- * 会在页面上添加一个红色小点跟随鼠标移动
281
- */
282
- async showCursor(): Promise<void> {
283
- await this.page.addInitScript({
284
- content: `
285
- let dot;
286
- document.addEventListener('mousemove', (e) => {
287
- if (!dot) {
288
- dot = document.createElement('div');
289
- Object.assign(dot.style, {
290
- position: 'fixed',
291
- width: '5px',
292
- height: '5px',
293
- borderRadius: '50%',
294
- backgroundColor: 'red',
295
- pointerEvents: 'none',
296
- zIndex: 999999
297
- });
298
- document.body.appendChild(dot);
299
- }
300
- dot.style.left = e.clientX + 'px';
301
- dot.style.top = e.clientY + 'px';
302
- });
303
- `
304
- });
305
- }
306
- }
307
-
308
- export default HumanMouse;
package/src/index.ts DELETED
@@ -1,13 +0,0 @@
1
- export * from "./actions";
2
- export * from "./core/config";
3
- export * from "./core/launcher";
4
- export * from "./core/pool";
5
- export * from "./human/mouse";
6
- export * from "./network/cache";
7
- export * from "./network/resource";
8
- export * from "./providers/local";
9
- export * from "./providers/adspower";
10
- export * from "./providers/ok";
11
- export * from "./providers/base";
12
- export * from "./types";
13
- export * from "./const/browser";
@@ -1,130 +0,0 @@
1
- /**
2
- * 用于浏览器缓存
3
- */
4
-
5
- import { type Page } from "patchright-core";
6
- import fs from "fs";
7
- import path from "path";
8
- import { createHash } from "crypto";
9
- import { BrowserCachePath } from "../core/config";
10
-
11
- function getCachePath(url: string, cachePath: string) {
12
- const hash = createHash("md5").update(url).digest("hex");
13
- const cacheFile = path.join(cachePath, hash);
14
- const metaFile = cacheFile + ".meta.json";
15
- return { cacheFile, metaFile }
16
- }
17
-
18
- /** 缓存浏览器到目录 ./tmp/browser_cache */
19
- export async function routeAssetsCache(page: Page, cachePath: string) {
20
- // 确保缓存目录存在
21
- if (!fs.existsSync(cachePath)) {
22
- fs.mkdirSync(cachePath, { recursive: true });
23
- }
24
-
25
- console.log("启用浏览器缓存")
26
-
27
- await page.route("**/*", async (route) => {
28
- const req = route.request();
29
- //如果不是get直接放行
30
- if (req.method() !== "GET") {
31
- return route.continue();
32
- }
33
- const type = req.resourceType()
34
- if (["xhr", "fetch", "websocket", "document"].includes(type)) {
35
- return route.continue();
36
- }
37
-
38
- // 尝试从缓存读取 (使用 URL hash 作为文件名)
39
- const url = req.url();
40
- const { cacheFile, metaFile } = getCachePath(url, cachePath);
41
-
42
- try {
43
- if (fs.existsSync(cacheFile) && fs.existsSync(metaFile)) {
44
- const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
45
- const body = fs.readFileSync(cacheFile);
46
- // 检查缓存是否过期
47
- if (Date.now() - meta.timestamp > meta.timeout) {
48
- fs.unlinkSync(cacheFile);
49
- fs.unlinkSync(metaFile);
50
- return route.continue();
51
- }
52
- // console.log(`命中缓存 ${url}`, metaFile);
53
- return route.fulfill({
54
- status: 200,
55
- headers: meta.headers,
56
- body: body,
57
- contentType: meta.contentType
58
- });
59
- }
60
- } catch (e) {
61
- // 缓存读取失败,继续请求
62
- // console.log(`缓存 ${url} 读取失败`, e);
63
- }
64
- return route.continue();
65
- });
66
-
67
- // 监听响应事件,检查是否缓存到本地
68
- await page.on("response", async (response) => {
69
- const req = response.request();
70
- if (req.method() !== "GET") {
71
- return;
72
- }
73
- const status = response.status()
74
- if (![200, 201].includes(status)) {
75
- return;
76
- }
77
- const url = response.url();
78
- const { cacheFile, metaFile } = getCachePath(url, cachePath);
79
- if (fs.existsSync(cacheFile) && fs.existsSync(metaFile)) {
80
- return;
81
- }
82
- // 检查响应头是否允许缓存
83
- const headers = response.headers();
84
- const resType = headers["content-type"] || "";
85
- if (resType.includes("text/plain") || url.includes(";")) {
86
- return;
87
- }
88
- const cacheControl = headers["cache-control"] || "";
89
- const pragma = headers["pragma"] || "";
90
- // 如果明确禁止缓存,则不缓存
91
- if (cacheControl.includes("no-store") || pragma.includes("no-cache")) {
92
- return;
93
- }
94
- const type = req.resourceType()
95
- if (!(pragma || cacheControl)) {
96
- // 如果没有cache-control和pragma头,只缓存document类型的资源
97
- if (["xhr", "fetch", "websocket", "document"].includes(type)) {
98
- return;
99
- }
100
- // 如果有参数也不缓存
101
- const params = new URL(url).searchParams
102
- if (params.toString().length > 0) {
103
- return;
104
- }
105
- // if (["document", "script"].includes(type) && params.size > 0) {
106
- // return;
107
- // }
108
- // if (["script"].includes(type) && params.size > 38) {
109
- // return;
110
- // }
111
- }
112
-
113
- try {
114
- // 保存到缓存
115
- const buffer = await response.body();
116
- fs.writeFileSync(cacheFile, buffer);
117
- fs.writeFileSync(metaFile, JSON.stringify({
118
- url: url,
119
- headers: headers,
120
- contentType: headers["content-type"],
121
- timestamp: Date.now(),
122
- // 从header中获取缓存时间,默认1分钟
123
- timeout: parseInt(headers["cache-control"]?.match(/max-age=(\d+)/)?.[1] || "99999999") * 1000,
124
- }));
125
- // console.log(`缓存 ${url}`, resType);
126
- } catch (e) {
127
- // console.log(`缓存 ${url} 写入失败`, e);
128
- }
129
- });
130
- }
@@ -1,62 +0,0 @@
1
- import type { Page, Route, Request } from "patchright-core";
2
-
3
- export async function fetchResource(route: Route, req: Request, page: Page) {
4
- // 使用 Node.js 原生 fetch,不继承浏览器代理设置
5
- return await fetch(req.url(), {
6
- method: req.method(),
7
- body: req.postData(),
8
- headers: req.headers(),
9
- })
10
- .then(async (res) => {
11
- return route.fulfill({
12
- status: res.status,
13
- headers: Object.fromEntries(res.headers.entries()),
14
- body: Buffer.from(await res.arrayBuffer()),
15
- });
16
- }).catch(() => {
17
- return route.abort();
18
- })
19
- }
20
-
21
- export async function routeAssetsLocal(page: Page) {
22
- await page.route("**/*", async (route) => {
23
- const req = route.request();
24
- const url = req.url();
25
- const type = req.resourceType();
26
- //如果不是get直接放行
27
- if (!["GET"].includes(req.method())) {
28
- return await route.continue();
29
- }
30
- if (
31
- ["xhr", "fetch", "websocket", "document"].includes(type)
32
- || (["script"].includes(type) && url.includes("?"))
33
- ) {
34
- //使用浏览器代理进行请求
35
- return await route.continue();
36
- }
37
- //使用本地网络进行请求
38
- return await fetchResource(route, req, page);
39
- });
40
- }
41
-
42
- export async function routeAssetsAbort(page: Page) {
43
- await page.route("**/*", async (route) => {
44
- const req = route.request();
45
- //如果不是get直接放行
46
- if (req.method() !== "GET") {
47
- return route.continue();
48
- }
49
- const type = req.resourceType()
50
- // if (["script"].includes(type)) {
51
- // return await curlResource(route, req, page);
52
- // }
53
- if (
54
- !["xhr", "fetch", "websocket", "document", "script"].includes(type)
55
- ) {
56
- //使用fetch进行请求
57
- return route.abort();
58
- }
59
- //其他请求继续
60
- return route.continue();
61
- });
62
- }
@@ -1,38 +0,0 @@
1
-
2
- /** ads专用参数加密 */
3
- export function adsEncodeBase64(e: string) {
4
- let t, r
5
- void 0 === t && (t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),
6
- void 0 === r && (r = "hTy1bfRJz4nLPcBCO7WtmNIaGvVeul5Zo8kq32UxrYw_-0gsjp96SDFXQiEMKdHA");
7
- var n = t
8
- , a = r
9
- , i = Buffer.from(e).toString('base64').split("")
10
- , s = "";
11
- return i.map((function (e) {
12
- var t = n.indexOf(e);
13
- return s += t < 0 ? e : a.substr(t, 1),
14
- e
15
- }
16
- )),
17
- s
18
- }
19
-
20
-
21
- /** ads专用参数解密 */
22
- export function adsDecodeBase64(e: string) {
23
- let t, r;
24
- if (void 0 === t && (t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"),
25
- void 0 === r && (r = "hTy1bfRJz4nLPcBCO7WtmNIaGvVeul5Zo8kq32UxrYw_-0gsjp96SDFXQiEMKdHA"),
26
- !e)
27
- return "";
28
- var n = t
29
- , a = r
30
- , i = e.split("")
31
- , s = "";
32
- return i.map((function (e) {
33
- var t = a.indexOf(e);
34
- return s += t < 0 ? e : n.substr(t, 1),
35
- e
36
- }
37
- )), Buffer.from(s, 'base64').toString()
38
- }