@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,236 +0,0 @@
1
- import type { Locator, Page, Request } from "patchright-core";
2
- import HumanMouse, { type ClickOptions } from "../../human/mouse";
3
- import type { AutoPage, AutoLocator, MonitorOptions, WaitForIframeCompleteOptions } from "./types";
4
- import { isLocator, isElementFocused, splitTextIntoChunks } from "./utils";
5
- import * as clickActions from "./click";
6
- import * as inputActions from "./input";
7
- import * as selectActions from "./select";
8
- import * as waitActions from "./wait";
9
- import * as pollActions from "./poll";
10
- import * as scrollActions from "./scroll";
11
-
12
- // Re-export types
13
- export * from "./types";
14
- export * from "./utils";
15
-
16
- const prefix = "_ah_";
17
-
18
- export class AutoHelper {
19
- private page: Page;
20
- public mouse: HumanMouse;
21
-
22
- constructor(page: Page) {
23
- this.page = page;
24
- this.mouse = new HumanMouse(page);
25
- // this.mouse.showCursor()
26
- }
27
-
28
- static create(page: Page): AutoPage {
29
- const helper = new AutoHelper(page);
30
-
31
- // 创建增强的 page 对象
32
- const autoPage = Object.create(page);
33
- Object.assign(autoPage, page);
34
- Object.assign(autoPage, helper);
35
-
36
- const hooks: string[] = [
37
- "locator",
38
- "getByRole",
39
- "getByTestId",
40
- "getByLabel",
41
- "getByPlaceholder",
42
- "getByText",
43
- "getByAltText",
44
- "getByTitle",
45
- ];
46
- hooks.forEach((hook) => {
47
- if (!(page as any)[hook]) return;
48
- const page2 = page as any;
49
- const oldFn = page2[hook].bind(page);
50
- const oldName = prefix + hook;
51
- autoPage[oldName] = oldFn;
52
- autoPage[hook] = function (...args: any[]) {
53
- const loc = autoPage[oldName](...args);
54
- if (loc) {
55
- return helper.enhanceLocator(loc);
56
- }
57
- return loc;
58
- };
59
- });
60
- return autoPage;
61
- }
62
-
63
- //增强locator
64
- private enhanceLocator(locator: Locator): AutoLocator {
65
- const autoLoc = Object.create(locator) as AutoLocator;
66
- Object.assign(autoLoc, locator);
67
- autoLoc._loc = locator;
68
- autoLoc.click = this.click.bind(this, locator);
69
- autoLoc.input = autoLoc.fill = this.input.bind(this, locator);
70
- (autoLoc as any).selectOption = autoLoc.select = this.select.bind(
71
- this,
72
- locator
73
- );
74
- autoLoc.check = this.check.bind(this, locator);
75
- autoLoc.uncheck = this.uncheck.bind(this, locator);
76
- return autoLoc;
77
- }
78
-
79
- /** 点击元素 */
80
- async click(
81
- locator: Locator | string,
82
- options?: Parameters<Locator["waitFor"]>[0] &
83
- Parameters<Locator["click"]>[0] & {
84
- /** 滚动超时时间 */
85
- scrollTimeout?: number;
86
- /** 是否跳过滚动检查 */
87
- skipScroll?: boolean;
88
- }
89
- ) {
90
- return clickActions.click(this.page, this.mouse, locator, options);
91
- }
92
-
93
- /** 点击指定坐标 */
94
- async clickPoint(x: number, y: number, options?: ClickOptions) {
95
- return clickActions.clickPoint(this.mouse, x, y, options);
96
- }
97
-
98
- isLocator(obj: any): obj is Locator {
99
- return isLocator(obj);
100
- }
101
-
102
- /**
103
- * 模拟真人输入文本到指定元素
104
- */
105
- async input(
106
- locator: Locator | string,
107
- text: string,
108
- options?: Parameters<Locator["waitFor"]>[0] & {
109
- clear?: boolean;
110
- delay?: { min: number; max: number };
111
- retries?: number;
112
- }
113
- ): Promise<void> {
114
- return inputActions.input(this.page, this.mouse, locator, text, options);
115
- }
116
-
117
- /**
118
- * 模拟真人输入文本到指定元素 (Force)
119
- */
120
- async inputFofce(
121
- locator: Locator | string,
122
- text: string,
123
- options?: Parameters<Locator["waitFor"]>[0] & {
124
- clear?: boolean;
125
- delay?: { min: number; max: number };
126
- retries?: number;
127
- }
128
- ): Promise<void> {
129
- return inputActions.inputFofce(this.page, this.mouse, locator, text, options);
130
- }
131
-
132
- /**
133
- * 选择下拉框选项
134
- */
135
- async select(
136
- locator: Locator | string,
137
- value: Parameters<Locator["selectOption"]>[0],
138
- options?: Parameters<Locator["waitFor"]>[0] &
139
- Parameters<Locator["selectOption"]>[1] & {
140
- retries?: number;
141
- verify?: boolean;
142
- }
143
- ): Promise<void> {
144
- return selectActions.select(this.page, this.mouse, locator, value, options);
145
- }
146
-
147
- /**
148
- * 勾选复选框或单选框
149
- */
150
- async check(
151
- locator: Locator | string,
152
- options?: Parameters<Locator["waitFor"]>[0] & {
153
- force?: boolean;
154
- retries?: number;
155
- verify?: boolean;
156
- }
157
- ): Promise<void> {
158
- return selectActions.check(this.page, this.mouse, locator, options);
159
- }
160
-
161
- /**
162
- * 取消勾选复选框或单选框
163
- */
164
- async uncheck(
165
- locator: Locator | string,
166
- options?: Parameters<Locator["waitFor"]>[0] & {
167
- force?: boolean;
168
- retries?: number;
169
- verify?: boolean;
170
- }
171
- ): Promise<void> {
172
- return selectActions.uncheck(this.page, this.mouse, locator, options);
173
- }
174
-
175
- /** 随机等待 */
176
- async rWait(minMs = 200, maxMs = 800) {
177
- return waitActions.rWait(minMs, maxMs);
178
- }
179
-
180
- /**
181
- * 通用且安全的等待页面加载状态完成
182
- */
183
- async safeWaitForLoadState(
184
- state: "load" | "domcontentloaded" | "networkidle" | undefined = "load",
185
- timeoutMs: number = 30000
186
- ): Promise<boolean> {
187
- return waitActions.safeWaitForLoadState(this.page, state, timeoutMs);
188
- }
189
-
190
- /**
191
- * 等待指定 iframe 完全加载并等待指定目标元素可见
192
- */
193
- waitForIframeComplete(
194
- frameLocator: string | number | symbol,
195
- frameInLocator: string | Locator,
196
- options: WaitForIframeCompleteOptions = {}
197
- ) {
198
- return waitActions.waitForIframeComplete(this.page, frameLocator, frameInLocator, options);
199
- }
200
-
201
- /**
202
- * 开启后台请求轮询监听
203
- */
204
- startRequestPolling(
205
- urlRule: string | RegExp,
206
- callback?: (request: Request) => void,
207
- options: MonitorOptions = {}
208
- ) {
209
- return pollActions.startRequestPolling(this.page, urlRule, callback, options);
210
- }
211
-
212
- /**
213
- * 开启后台元素轮询监听
214
- */
215
- startElementPolling(
216
- page: Page,
217
- selector: string,
218
- callback?: (locator: Locator) => Promise<void> | void,
219
- options: MonitorOptions = {}
220
- ) {
221
- return pollActions.startElementPolling(page, selector, callback, options);
222
- }
223
-
224
- /** 可取消等待出现函数 */
225
- waitForAbort(el: Locator, options: Parameters<Locator["waitFor"]>[0] = {}) {
226
- return waitActions.waitForAbort(el, options);
227
- }
228
-
229
-
230
- /**
231
- * 模拟人类滚动
232
- */
233
- async humanScroll(distance: number, targetLocator?: Locator) {
234
- return scrollActions.humanScroll(this.page, distance, targetLocator);
235
- }
236
- }
@@ -1,157 +0,0 @@
1
- import type { Locator, Page } from "patchright-core";
2
- import HumanMouse from "../../human/mouse";
3
- import { splitTextIntoChunks, isElementFocused, isLocator } from "./utils";
4
- import { rWait } from "./wait";
5
- import { click } from "./click";
6
- import { rInt } from "@tocha688/utils";
7
-
8
- export async function input(
9
- page: Page,
10
- mouse: HumanMouse,
11
- locator: Locator | string,
12
- text: string,
13
- options?: Parameters<Locator["waitFor"]>[0] & {
14
- clear?: boolean;
15
- delay?: { min: number; max: number };
16
- retries?: number;
17
- }
18
- ): Promise<void> {
19
- // 标准化locator
20
- if (typeof locator === "string") {
21
- locator = page.locator(locator);
22
- }
23
- if (!isLocator(locator)) {
24
- throw new Error("locator 类型错误");
25
- }
26
-
27
- const {
28
- clear = true,
29
- delay = { min: 50, max: 150 },
30
- retries = 3,
31
- ...waitOptions
32
- } = options || {};
33
-
34
- // 等待元素可见和可用
35
- await locator.waitFor({ state: "visible", ...waitOptions });
36
- await locator.waitFor({ state: "attached", ...waitOptions });
37
-
38
- // 点击元素确保聚焦
39
- await click(page, mouse, locator, waitOptions);
40
- await rWait(50, 100);
41
-
42
- // 清空现有内容
43
- if (clear) {
44
- const currentValue = await locator.inputValue().catch(() => "");
45
- if (currentValue && currentValue.length > 0) {
46
- // 使用三击选中全部内容,然后删除
47
- await locator.click({ clickCount: 3 });
48
- await rWait(20, 50);
49
- await page.keyboard.press("Delete");
50
- await rWait(20, 50);
51
- }
52
- }
53
-
54
- // 分批输入文本,避免长文本一次性输入
55
- const chunks = splitTextIntoChunks(text, 5); // 每5个字符一组
56
-
57
- for (let i = 0; i < chunks.length; i++) {
58
- const chunk = chunks[i];
59
- // 检查元素是否仍然聚焦
60
- const isFocused = await isElementFocused(locator);
61
- if (!isFocused) {
62
- await click(page, mouse, locator, waitOptions);
63
- // 如果光标没有在最后面,将光标移动到最后
64
- const currentValue = await locator.inputValue().catch(() => "");
65
- if (currentValue && currentValue.length > 0) {
66
- await page.keyboard.press("End");
67
- }
68
- await rWait(30, 60);
69
- }
70
-
71
- // 使用pressSequentially进行更自然的输入
72
- if (chunk) {
73
- await locator.pressSequentially(chunk, {
74
- delay: rInt(delay.min, delay.max),
75
- });
76
- }
77
-
78
- // 随机等待,模拟真人输入节奏
79
- if (i < chunks.length - 1) {
80
- await rWait(50, 150);
81
- }
82
- }
83
- // 验证输入结果
84
- await rWait(100, 200);
85
- // 失焦
86
- await locator.blur();
87
- }
88
-
89
- export async function inputFofce(
90
- page: Page,
91
- mouse: HumanMouse,
92
- locator: Locator | string,
93
- text: string,
94
- options?: Parameters<Locator["waitFor"]>[0] & {
95
- clear?: boolean;
96
- delay?: { min: number; max: number };
97
- retries?: number;
98
- }
99
- ): Promise<void> {
100
- // 标准化locator
101
- if (typeof locator === "string") {
102
- locator = page.locator(locator);
103
- }
104
- if (!isLocator(locator)) {
105
- throw new Error("locator 类型错误");
106
- }
107
-
108
- const {
109
- clear = true,
110
- delay = { min: 50, max: 150 },
111
- retries = 3,
112
- ...waitOptions
113
- } = options || {};
114
-
115
- // 等待元素可见和可用
116
- await locator.waitFor({ state: "visible", ...waitOptions });
117
- await locator.waitFor({ state: "attached", ...waitOptions });
118
-
119
- // 点击元素确保聚焦
120
- await click(page, mouse, locator, waitOptions);
121
- await rWait(50, 100);
122
-
123
- // 清空现有内容
124
- if (clear) {
125
- const currentValue = await locator.inputValue().catch(() => "");
126
- if (currentValue && currentValue.length > 0) {
127
- // 使用三击选中全部内容,然后删除
128
- await locator.click({ clickCount: 3 });
129
- await rWait(20, 50);
130
- await page.keyboard.press("Delete");
131
- await rWait(20, 50);
132
- }
133
- }
134
-
135
- // 分批输入文本,避免长文本一次性输入
136
- const chunks = splitTextIntoChunks(text, 5); // 每5个字符一组
137
-
138
- for (let i = 0; i < chunks.length; i++) {
139
- const chunk = chunks[i];
140
-
141
- // 使用pressSequentially进行更自然的输入
142
- if (chunk) {
143
- await locator.pressSequentially(chunk, {
144
- delay: rInt(delay.min, delay.max),
145
- });
146
- }
147
-
148
- // 随机等待,模拟真人输入节奏
149
- if (i < chunks.length - 1) {
150
- await rWait(50, 150);
151
- }
152
- }
153
- // 验证输入结果
154
- await rWait(100, 200);
155
- // 失焦
156
- await locator.blur();
157
- }
@@ -1,145 +0,0 @@
1
- import type { Page, Request, Locator } from "patchright-core";
2
- import type { MonitorOptions } from "./types";
3
-
4
- export function startRequestPolling(
5
- page: Page,
6
- urlRule: string | RegExp,
7
- callback?: (request: Request) => void,
8
- options: MonitorOptions = {}
9
- ) {
10
- const { interval = 1000, debug = false } = options;
11
- const matchedQueue: Request[] = [];
12
- let isRunning = true;
13
-
14
- // 1. 实时收集请求放入队列 (不阻塞主流程)
15
- const requestListener = (request: Request) => {
16
- const url = request.url();
17
- // 判断 URL 是否匹配
18
- const isMatch =
19
- urlRule instanceof RegExp ? urlRule.test(url) : url.includes(urlRule);
20
-
21
- if (isMatch) {
22
- if (debug) console.log(`[Collector] 捕获到目标请求: ${url}`);
23
- matchedQueue.push(request);
24
- }
25
- };
26
-
27
- // 注册监听器
28
- page.on("request", requestListener);
29
-
30
- // 2. 开启定时轮询 (setInterval) 处理队列
31
- const timer = setInterval(async () => {
32
- if (!isRunning) return;
33
-
34
- if (debug)
35
- console.log(
36
- `[Poller] 轮询检查中... 当前队列积压: ${matchedQueue.length}`
37
- );
38
-
39
- // 处理队列中所有新发现的请求
40
- while (matchedQueue.length > 0) {
41
- const req = matchedQueue.shift(); // 取出最早的一个
42
- if (!req) continue;
43
-
44
- if (callback) {
45
- // --- 情况 A: 传入了回调函数 ---
46
- // try {
47
- await callback(req);
48
- // } catch (e) {
49
- // console.error('[Callback Error] 回调函数执行出错:', e);
50
- // }
51
- } else {
52
- // --- 情况 B: 未传入回调 -> 默认行为 (打印 + Throw) ---
53
- const msg = `[Error] 致命错误: 检测到被禁止的请求/未处理的请求发送! URL: ${req.url()}`;
54
- console.error(msg);
55
-
56
- cleanup(); // 抛错前先清理
57
- }
58
- }
59
- }, interval);
60
-
61
- // 清理函数
62
- const cleanup = () => {
63
- if (!isRunning) return;
64
- isRunning = false;
65
- clearInterval(timer);
66
- page.off("request", requestListener); // 移除监听,防止内存泄漏
67
- if (debug) console.log("[Poller] 监听已停止");
68
- };
69
-
70
- // 返回控制器,以便在测试结束时手动停止
71
- return { stop: cleanup };
72
- }
73
-
74
- export function startElementPolling(
75
- page: Page,
76
- selector: string,
77
- callback?: (locator: Locator) => Promise<void> | void,
78
- options: MonitorOptions = {}
79
- ) {
80
- const { interval = 1000, debug = false } = options;
81
- let isRunning = true;
82
- let isProcessing = false; // 防止上一轮回调还没跑完,下一轮轮询就开始了
83
-
84
- if (debug) console.log(`[ElementMonitor] 开始监听元素: "${selector}"`);
85
-
86
- const timer = setInterval(async () => {
87
- // 1. 如果停止了,或者上一轮正在处理中,跳过
88
- if (!isRunning || isProcessing) return;
89
-
90
- try {
91
- // 2. 检查页面是否已关闭 (防止报错)
92
- if (page.isClosed()) {
93
- cleanup();
94
- return;
95
- }
96
-
97
- // 3. 检查元素是否可见 (isVisible 不会报错,只会返回 true/false)
98
- // 使用 isVisible 而不是 $,因为我们通常关心的是“用户是否看到了”
99
- const isVisible = await page.isVisible(selector);
100
-
101
- if (isVisible) {
102
- if (debug)
103
- console.log(`[ElementMonitor] 发现目标元素: "${selector}"`);
104
-
105
- // 标记正在处理,暂停下一轮轮询
106
- isProcessing = true;
107
-
108
- if (callback) {
109
- // --- 情况 A: 有回调 -> 执行逻辑 (如点击关闭、输入内容) ---
110
- try {
111
- await callback(page.locator(selector));
112
- } catch (err) {
113
- console.error(
114
- `[Callback Error] 处理元素 "${selector}" 回调失败:`,
115
- err
116
- );
117
- } finally {
118
- // 处理完后,释放锁,允许下一轮检测 (如果元素还在,会再次触发)
119
- isProcessing = false;
120
- }
121
- } else {
122
- // --- 情况 B: 无回调 -> 默认行为 (报错) ---
123
- const msg = `[Error] 致命错误: 页面出现了不该出现的元素! Selector: "${selector}"`;
124
- console.error(msg);
125
-
126
- // 停止监听并抛出错误
127
- cleanup();
128
- }
129
- }
130
- } catch (error) {
131
- // 捕获轮询过程中的意外错误(如 context 崩溃),防止中断主进程
132
- if (debug) console.warn("[ElementMonitor] 轮询检查异常:", error);
133
- }
134
- }, interval);
135
-
136
- // 清理函数
137
- const cleanup = () => {
138
- if (!isRunning) return;
139
- isRunning = false;
140
- clearInterval(timer);
141
- if (debug) console.log(`[ElementMonitor] 停止监听元素: "${selector}"`);
142
- };
143
-
144
- return { stop: cleanup };
145
- }
@@ -1,81 +0,0 @@
1
- import { rInt, sleep } from "@tocha688/utils";
2
- import type { Page, Locator } from "patchright-core";
3
-
4
- export async function humanScroll(
5
- page: Page,
6
- distance: number,
7
- targetLocator?: Locator
8
- ) {
9
- let scrolledAmount = 0;
10
- // 增加或减少随机距离,模拟人类目标的不精确性
11
- const finalDistance = distance + rInt(-100, 100);
12
- console.log(`开始模拟滚动,目标距离: ${finalDistance}px`);
13
-
14
- if (targetLocator) {
15
- // 确保元素可见并获取位置
16
- // 注意:如果元素在 iframe 里,boundingBox 返回的是相对于主页面的坐标,直接 move 是没问题的
17
- const box = await targetLocator.boundingBox();
18
- if (box) {
19
- // 移动到元素中心稍微偏随机一点的位置
20
- const x = box.x + box.width / 2 + rInt(-10, 10);
21
- const y = box.y + box.height / 2 + rInt(-10, 10);
22
- await page.mouse.move(x, y);
23
- } else {
24
- console.warn(
25
- "Target locator provided but element is not visible/present. Falling back to global scroll."
26
- );
27
- }
28
- } else {
29
- // 如果没有指定元素,也可以先移动鼠标到屏幕中间,防止鼠标停留在某个不相干的侧边栏上
30
- // 视情况可选
31
- // await this.page.mouse.move(300, 300);
32
- }
33
-
34
- // --- 滚动执行函数 ---
35
- const scrollExecutor = async (step: number) => {
36
- // if (targetLocator) {
37
- // await targetLocator.evaluate(
38
- // `(el, stepValue) => {
39
- // if (el) {
40
- // el.scrollBy(0, stepValue);
41
- // }
42
- // }`,
43
- // step
44
- // );
45
- // } else {
46
- // 在整个页面上执行滚动
47
- await page.mouse.wheel(0, step);
48
- // await this.page.mouse.move(this.random(100, 500), this.random(100, 500));
49
- // 偶尔微调鼠标位置,模拟人的手不稳 (可选,增加真实感)
50
- if (!targetLocator && Math.random() < 0.3) {
51
- await page.mouse.move(rInt(100, 500), rInt(100, 500));
52
- }
53
- // }
54
- };
55
-
56
- // --- 滚动循环 ---
57
- while (scrolledAmount < finalDistance) {
58
- // 1. 随机步长:模拟手指拨动滚轮的力度 (50px - 150px)
59
- let step = rInt(50, 150);
60
-
61
- // 防止最后一步滚过头
62
- if (scrolledAmount + step > finalDistance) {
63
- step = finalDistance - scrolledAmount;
64
- }
65
-
66
- // 2. 执行滚动
67
- await scrollExecutor(step);
68
- scrolledAmount += step;
69
-
70
- // 3. 随机停顿:模拟人眼的反应时间和手指的动作间隙
71
- if (Math.random() < 0.1) {
72
- // 10% 的概率停顿久一点 (500ms - 800ms) 模拟“阅读”
73
- await sleep(rInt(500, 800));
74
- } else {
75
- // 正常滚动间隙 (30ms - 100ms)
76
- await sleep(rInt(30, 100));
77
- }
78
- }
79
-
80
- console.log("滚动完成");
81
- }