feat(js): add types to humanized method options (#205)

Replace `any` with proper TypeScript types on all humanized method options
(Playwright, Puppeteer, ElementHandle, Frame). Support flat per-call config
overrides alongside existing `human_config` style. Internalize `humanIdle`
duration computation with backward-compatible overloads.

Co-authored-by: Eternal <chasezou09@gmail.com>
This commit is contained in:
Eternal
2026-05-10 23:50:01 +02:00
committed by CloakHQ
parent 114b3c826b
commit 80d9f7c14e
6 changed files with 518 additions and 302 deletions
+83 -34
View File
@@ -319,7 +319,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}
// ==== goto ====
const humanGoto = async (url: string, options?: any) => {
const humanGoto = async (url: string, options?: {
referer?: string;
timeout?: number;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
}) => {
const response = await originals.goto(url, options);
stealth.invalidate();
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
@@ -327,11 +331,18 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// ==== click (with clickCount support for dblclick) ====
const humanClickFn = async (selector: string, options?: any) => {
const humanClickFn = async (selector: string, options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config);
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX;
@@ -355,11 +366,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// ==== hover ====
const humanHoverFn = async (selector: string, options?: any) => {
const humanHoverFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config);
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX;
@@ -371,8 +382,12 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// ==== type ====
const humanTypeFn = async (selector: string, text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
const humanTypeFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options);
await sleep(rand(100, 250));
@@ -395,7 +410,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// ==== tap ====
const humanTapFn = async (selector: string, options?: any) => {
const humanTapFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanClickFn(selector, options);
};
@@ -413,14 +428,19 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
// ============================================================
// Mouse patches
// ============================================================
page.mouse.move = async (x: number, y: number, options?: any) => {
page.mouse.move = async (x: number, y: number, options?: { steps?: number }) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
cursor.y = y;
};
page.mouse.click = async (x: number, y: number, options?: any) => {
page.mouse.click = async (x: number, y: number, options?: {
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
}) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
@@ -455,7 +475,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page.mouse as any).dragAndDrop = async (
start: { x: number; y: number },
target: { x: number; y: number },
options?: any,
options?: { delay?: number },
) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, start.x, start.y, cfg);
@@ -475,12 +495,12 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
// ============================================================
// Keyboard patches
// ============================================================
page.keyboard.type = async (text: string, options?: any) => {
page.keyboard.type = async (text: string, options?: { delay?: number }) => {
const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp);
};
page.keyboard.press = async (key: any, options?: any) => {
page.keyboard.press = async (key: any, options?: { delay?: number }) => {
await sleep(rand(20, 60));
await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold));
@@ -551,7 +571,11 @@ function patchElementHandle(
return els;
};
(page as any).waitForSelector = async (selector: string, options?: any) => {
(page as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const el = await origWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
@@ -603,14 +627,18 @@ function patchSingleElementHandle(
return children;
};
(el as any).waitForSelector = async (selector: string, options?: any) => {
(el as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const child = await origElWaitForSelector(selector, options);
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child;
};
// --- Helper: get box and move cursor. Accepts a per-call ``callCfg``
// so type/fill overrides like ``el.type(text, { human_config: {...} })``
// so type/fill overrides like ``el.type(text, { typing_delay: 30 })``
// carry through to mouse timing for that single call. Also scrolls into
// view first so off-screen elements work (#129, #172 follow-up).
const moveToElement = async (callCfg: HumanConfig = cfg) => {
@@ -633,7 +661,7 @@ function patchSingleElementHandle(
const target = clickTarget(box, isInp, callCfg);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
@@ -643,8 +671,14 @@ function patchSingleElementHandle(
};
// --- el.click() ---
(el as any).click = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).click = async (options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElClick(options);
@@ -667,8 +701,8 @@ function patchSingleElementHandle(
};
// --- el.type() ---
(el as any).type = async (text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).type = async (text: string, options?: Partial<HumanConfig> & ({ delay?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElType(text, options);
await humanClick(raw, info.isInp, callCfg);
@@ -684,8 +718,8 @@ function patchSingleElementHandle(
// page.click(). Only patched when the underlying ElementHandle exposes
// ``scrollIntoView`` (Puppeteer v22+).
if (origElScrollIntoView) {
(el as any).scrollIntoView = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).scrollIntoView = async (options?: Partial<HumanConfig> & { human_config?: Partial<HumanConfig> }) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await (page as any)._ensureCursorInit();
try {
const { cursorX, cursorY } = await humanScrollIntoView(
@@ -703,7 +737,7 @@ function patchSingleElementHandle(
// --- el.press() ---
if (origElPress) {
(el as any).press = async (key: string, options?: any) => {
(el as any).press = async (key: string, options?: { delay?: number }) => {
await sleep(rand(20, 60));
await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold));
@@ -742,7 +776,7 @@ function patchSingleElementHandle(
// --- el.drop() ---
if (origElDrop) {
(el as any).drop = async (draggable: ElementHandle, options?: any) => {
(el as any).drop = async (draggable: ElementHandle, options?: { delay?: number }) => {
const srcBox = await draggable.boundingBox();
const tgtBox = await el.boundingBox();
@@ -772,7 +806,7 @@ function patchSingleElementHandle(
// --- el.dragAndDrop() ---
if (origElDragAndDrop) {
(el as any).dragAndDrop = async (targetEl: ElementHandle, options?: any) => {
(el as any).dragAndDrop = async (targetEl: ElementHandle, options?: { delay?: number }) => {
const srcBox = await el.boundingBox();
const tgtBox = await targetEl.boundingBox();
@@ -836,15 +870,26 @@ function patchSingleFrame(
const origFrameSelect = frame.select.bind(frame);
(frame as any).click = async (selector: string, options?: any) => {
(frame as any).click = async (selector: string, options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle' | 'back' | 'forward';
clickCount?: number;
count?: number;
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await (page as any).click(selector, options);
};
(frame as any).hover = async (selector: string, options?: any) => {
(frame as any).hover = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await (page as any).hover(selector, options);
};
(frame as any).type = async (selector: string, text: string, options?: any) => {
(frame as any).type = async (selector: string, text: string, options?: Partial<HumanConfig> & ({
delay?: number;
human_config?: Partial<HumanConfig>;
timeout?: number;
})) => {
await (page as any).type(selector, text, options);
};
@@ -858,7 +903,7 @@ function patchSingleFrame(
await (page as any).focus(selector);
};
(frame as any).tap = async (selector: string, options?: any) => {
(frame as any).tap = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await (page as any).click(selector, options);
};
@@ -881,7 +926,11 @@ function patchSingleFrame(
return els;
};
(frame as any).waitForSelector = async (selector: string, options?: any) => {
(frame as any).waitForSelector = async (selector: string, options?: {
hidden?: boolean;
timeout?: number;
visible?: boolean;
}) => {
const el = await origFrameWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
@@ -927,7 +976,7 @@ export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) {
if (typeof (browser as any)[methodName] === 'function') {
const origCreateContext = (browser as any)[methodName].bind(browser);
(browser as any)[methodName] = async (options?: any) => {
(browser as any)[methodName] = async (options?: Parameters<typeof origCreateContext>[0]) => {
const context: BrowserContext = await origCreateContext(options);
const origCtxNewPage = context.newPage.bind(context);
+107 -27
View File
@@ -125,16 +125,21 @@ export function patchSingleElementHandle(
return children;
};
(el as any).waitForSelector = async (selector: string, options?: any) => {
const child = await origElWaitForSelector(selector, options);
(el as any).waitForSelector = async (selector: string, options?: {
state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const child = await origElWaitForSelector(selector, options ?? {});
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child;
};
// --- Helper: get bounding box and move cursor to element ---
// Accepts a per-call ``callCfg`` so type/fill overrides like
// ``el.type(text, { human_config: { typing_delay: 30 } })`` carry through to
// mouse movement & idle timing for that single call.
// ``el.type(text, { human_config: { typing_delay: 30 } })`` or
// ``el.type(text, { typing_delay: 30 })`` carry through to mouse movement
// & idle timing for that single call.
// Also scrolls the element into view first so off-screen elements work
// (#129, #172 follow-up): otherwise boundingBox() returns null and we'd
// silently fall back to the unpatched native method.
@@ -164,7 +169,7 @@ export function patchSingleElementHandle(
const target = clickTarget(box, isInp, callCfg);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, callCfg);
@@ -174,16 +179,37 @@ export function patchSingleElementHandle(
};
// --- el.click() ---
(el as any).click = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).click = async (options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle';
clickCount?: number;
delay?: number;
force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElClick(options);
await humanClick(raw, info.isInp, callCfg);
};
// --- el.dblclick() ---
(el as any).dblclick = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).dblclick = async (options?: Partial<HumanConfig> & ({
button?: 'left' | 'right' | 'middle';
delay?: number;
force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElDblclick(options);
await raw.down({ clickCount: 2 });
@@ -192,16 +218,28 @@ export function patchSingleElementHandle(
};
// --- el.hover() ---
(el as any).hover = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).hover = async (options?: Partial<HumanConfig> & ({
force?: boolean;
human_config?: Partial<HumanConfig>;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElHover(options);
// Just move — no click
};
// --- el.type() ---
(el as any).type = async (text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).type = async (text: string, options?: Partial<HumanConfig> & ({
delay?: number;
human_config?: Partial<HumanConfig>;
noWaitAfter?: boolean;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElType(text, options);
await humanClick(raw, info.isInp, callCfg);
@@ -212,8 +250,13 @@ export function patchSingleElementHandle(
};
// --- el.fill() ---
(el as any).fill = async (value: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).fill = async (value: string, options?: Partial<HumanConfig> & ({
force?: boolean;
human_config?: Partial<HumanConfig>;
noWaitAfter?: boolean;
timeout?: number;
})) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const info = await moveToElement(callCfg);
if (!info) return origElFill(value, options);
await humanClick(raw, info.isInp, callCfg);
@@ -229,7 +272,7 @@ export function patchSingleElementHandle(
};
// --- el.press() ---
(el as any).press = async (key: string, options?: any) => {
(el as any).press = async (key: string, options?: { delay?: number; noWaitAfter?: boolean; timeout?: number }) => {
await sleep(rand(20, 60));
await originals.keyboardDown(key);
await sleep(randRange(cfg.key_hold));
@@ -237,7 +280,11 @@ export function patchSingleElementHandle(
};
// --- el.selectOption() ---
(el as any).selectOption = async (values: any, options?: any) => {
(el as any).selectOption = async (values: any, options?: {
force?: boolean;
noWaitAfter?: boolean;
timeout?: number;
}) => {
const info = await moveToElement();
if (!info) return origElSelectOption(values, options);
await humanClick(raw, false, cfg);
@@ -246,7 +293,13 @@ export function patchSingleElementHandle(
};
// --- el.check() ---
(el as any).check = async (options?: any) => {
(el as any).check = async (options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try {
const checked = await el.isChecked();
if (checked) return; // Already checked
@@ -257,7 +310,13 @@ export function patchSingleElementHandle(
};
// --- el.uncheck() ---
(el as any).uncheck = async (options?: any) => {
(el as any).uncheck = async (options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try {
const checked = await el.isChecked();
if (!checked) return; // Already unchecked
@@ -269,7 +328,13 @@ export function patchSingleElementHandle(
// --- el.setChecked() ---
if (origElSetChecked) {
(el as any).setChecked = async (checked: boolean, options?: any) => {
(el as any).setChecked = async (checked: boolean, options?: {
force?: boolean;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
try {
const current = await el.isChecked();
if (current === checked) return;
@@ -281,7 +346,14 @@ export function patchSingleElementHandle(
}
// --- el.tap() ---
(el as any).tap = async (options?: any) => {
(el as any).tap = async (options?: {
force?: boolean;
modifiers?: Array<'Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift'>;
noWaitAfter?: boolean;
position?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
const info = await moveToElement();
if (!info) return origElTap(options);
await humanClick(raw, info.isInp, cfg);
@@ -302,8 +374,8 @@ export function patchSingleElementHandle(
// wheel sequence used by page.click() etc. Falls back to the native
// method if the element is detached or scrolling fails.
if (origElScrollIntoViewIfNeeded) {
(el as any).scrollIntoViewIfNeeded = async (options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(el as any).scrollIntoViewIfNeeded = async (options?: Partial<HumanConfig> & ({ human_config?: Partial<HumanConfig>; timeout?: number })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
const ensureCursorInit = (page as any)._ensureCursorInit;
if (ensureCursorInit) await ensureCursorInit();
try {
@@ -360,8 +432,12 @@ export function patchPageElementHandles(
// Patch page.waitForSelector()
if (typeof page.waitForSelector === 'function') {
const origWaitForSelector = page.waitForSelector.bind(page);
(page as any).waitForSelector = async (selector: string, options?: any) => {
const el = await origWaitForSelector(selector, options);
(page as any).waitForSelector = async (selector: string, options?: {
state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const el = await origWaitForSelector(selector, options ?? {});
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
@@ -408,8 +484,12 @@ export function patchFrameElementHandles(
// Patch frame.waitForSelector()
if (typeof frame.waitForSelector === 'function') {
const origFrameWaitForSelector = frame.waitForSelector.bind(frame);
(frame as any).waitForSelector = async (selector: string, options?: any) => {
const el = await origFrameWaitForSelector(selector, options);
(frame as any).waitForSelector = async (selector: string, options?: {
state?: 'attached' | 'detached' | 'visible' | 'hidden';
strict?: boolean;
timeout?: number;
}) => {
const el = await origFrameWaitForSelector(selector, options ?? {});
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
+76 -55
View File
@@ -295,7 +295,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
}
// --- goto (invalidate isolated world on navigation) ---
const humanGoto = async (url: string, options?: any) => {
const humanGoto = async (url: string, options?: {
referer?: string;
timeout?: number;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
}) => {
const response = await originals.goto(url, options);
stealth.invalidate();
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
@@ -303,11 +307,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- click ---
const humanClickFn = async (selector: string, options?: any) => {
const humanClickFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config);
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX;
@@ -321,12 +325,13 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- dblclick ---
const humanDblclickFn = async (selector: string, options?: any) => {
const humanDblclickFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config);
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX;
cursor.y = cursorY;
@@ -341,11 +346,11 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- hover ---
const humanHoverFn = async (selector: string, options?: any) => {
const humanHoverFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await ensureCursorInit();
const callCfg = mergeConfig(cfg, options?.human_config);
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, callCfg, options?.timeout);
cursor.x = cursorX;
@@ -357,8 +362,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- type ---
const humanTypeFn = async (selector: string, text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
const humanTypeFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options);
await sleep(rand(100, 250));
@@ -367,8 +372,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- fill (clears existing content first) ---
const humanFillFn = async (selector: string, value: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
const humanFillFn = async (selector: string, value: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay));
await humanClickFn(selector, options);
await sleep(rand(100, 250));
@@ -381,9 +386,9 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- clear ---
const humanClearFn = async (selector: string, options?: any) => {
const humanClearFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector);
await humanClickFn(selector, options);
}
await sleep(rand(50, 150));
await originals.keyboardPress(SELECT_ALL);
@@ -392,46 +397,48 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- check ---
const humanCheckFn = async (selector: string, options?: any) => {
if (cfg.idle_between_actions) {
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
const humanCheckFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const checked = await originals.isChecked(selector).catch(() => false);
if (!checked) {
await humanClickFn(selector);
await humanClickFn(selector, options);
}
};
// --- uncheck ---
const humanUncheckFn = async (selector: string, options?: any) => {
if (cfg.idle_between_actions) {
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
const humanUncheckFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const checked = await originals.isChecked(selector).catch(() => true);
if (checked) {
await humanClickFn(selector);
await humanClickFn(selector, options);
}
};
// --- selectOption ---
const humanSelectOptionFn = async (selector: string, values: any, options?: any) => {
await humanHoverFn(selector);
const humanSelectOptionFn = async (selector: string, values: any, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanHoverFn(selector, options);
await sleep(rand(100, 300));
return originals.selectOption(selector, values, options);
};
// --- press (checks focus first — avoids redundant mouse moves) ---
const humanPressFn = async (selector: string, key: string, options?: any) => {
const humanPressFn = async (selector: string, key: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector);
await humanClickFn(selector, options);
}
await sleep(rand(50, 150));
await originals.keyboardPress(key);
};
// --- pressSequentially ---
const humanPressSequentiallyFn = async (selector: string, text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
const humanPressSequentiallyFn = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector, options);
}
@@ -441,7 +448,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- tap ---
const humanTapFn = async (selector: string, options?: any) => {
const humanTapFn = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await humanClickFn(selector, options);
};
@@ -461,14 +468,20 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page as any).clear = humanClearFn;
// --- mouse patches ---
page.mouse.move = async (x: number, y: number, options?: any) => {
page.mouse.move = async (x: number, y: number, options?: {
steps?: number;
}) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
cursor.y = y;
};
page.mouse.click = async (x: number, y: number, options?: any) => {
page.mouse.click = async (x: number, y: number, options?: {
button?: 'left' | 'right' | 'middle';
clickCount?: number;
delay?: number;
}) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
@@ -477,7 +490,7 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
};
// --- keyboard patches ---
page.keyboard.type = async (text: string, options?: any) => {
page.keyboard.type = async (text: string, options?: { delay?: number }) => {
const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp);
};
@@ -580,10 +593,10 @@ function patchSingleFrame(
const origFrameTap = (frame as any).tap?.bind(frame);
const origFrameDragAndDrop = frame.dragAndDrop.bind(frame);
const moveToFrameSelector = async (selector: string, options?: any, inputBias = false) => {
const callCfg = mergeConfig(cfg, options?.human_config);
const moveToFrameSelector = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> }), inputBias = false) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (callCfg.idle_between_actions) {
await humanIdle(raw, rand(callCfg.idle_between_duration[0], callCfg.idle_between_duration[1]), cursor.x, cursor.y, callCfg);
await humanIdle(raw, cursor.x, cursor.y, callCfg);
}
const locator = firstFrameLocator(frame, selector);
@@ -601,7 +614,7 @@ function patchSingleFrame(
return { callCfg, isInput };
};
const frameClick = async (selector: string, options?: any) => {
const frameClick = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options);
if (!moved) return origFrameClick(selector, options);
await humanClick(raw, moved.isInput, moved.callCfg);
@@ -609,14 +622,14 @@ function patchSingleFrame(
const getFrameCdp = async () => stealth.getCdpSession().catch(() => null);
const frameHover = async (selector: string, options?: any) => {
const frameHover = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options, false);
if (!moved) return origFrameHover(selector, options);
};
(frame as any).click = frameClick;
(frame as any).dblclick = async (selector: string, options?: any) => {
(frame as any).dblclick = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const moved = await moveToFrameSelector(selector, options);
if (!moved) return origFrameDblclick(selector, options);
await raw.down({ clickCount: 2 });
@@ -626,8 +639,8 @@ function patchSingleFrame(
(frame as any).hover = frameHover;
(frame as any).type = async (selector: string, text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(frame as any).type = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay));
await frameClick(selector, options);
await sleep(rand(100, 250));
@@ -635,8 +648,8 @@ function patchSingleFrame(
await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFrameType(selector, text, options));
};
(frame as any).fill = async (selector: string, value: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(frame as any).fill = async (selector: string, value: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
await sleep(randRange(callCfg.field_switch_delay));
await frameClick(selector, options);
await sleep(rand(100, 250));
@@ -648,23 +661,23 @@ function patchSingleFrame(
await humanType(page, rawKb, value, callCfg, cdp).catch(() => origFrameFill(selector, value, options));
};
(frame as any).check = async (selector: string, options?: any) => {
(frame as any).check = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => false) ?? false;
if (!checked) await frameClick(selector, options).catch(() => origFrameCheck(selector, options));
};
(frame as any).uncheck = async (selector: string, options?: any) => {
(frame as any).uncheck = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const checked = await firstFrameLocator(frame, selector).isChecked?.().catch(() => true) ?? true;
if (checked) await frameClick(selector, options).catch(() => origFrameUncheck(selector, options));
};
(frame as any).selectOption = async (selector: string, values: any, options?: any) => {
(frame as any).selectOption = async (selector: string, values: any, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await frameHover(selector, options);
await sleep(rand(100, 300));
return origFrameSelectOption(selector, values, options);
};
(frame as any).press = async (selector: string, key: string, options?: any) => {
(frame as any).press = async (selector: string, key: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options);
}
@@ -672,8 +685,8 @@ function patchSingleFrame(
await originals.keyboardPress(key);
};
(frame as any).pressSequentially = async (selector: string, text: string, options?: any) => {
const callCfg = mergeConfig(cfg, options?.human_config);
(frame as any).pressSequentially = async (selector: string, text: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
const callCfg = mergeConfig(cfg, options?.human_config ?? options);
if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options);
}
@@ -682,11 +695,11 @@ function patchSingleFrame(
await humanType(page, rawKb, text, callCfg, cdp).catch(() => origFramePressSequentially?.(selector, text, options));
};
(frame as any).tap = async (selector: string, options?: any) => {
(frame as any).tap = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
await frameClick(selector, options).catch(() => origFrameTap?.(selector, options));
};
(frame as any).clear = async (selector: string, options?: any) => {
(frame as any).clear = async (selector: string, options?: Partial<HumanConfig> & ({ timeout?: number; human_config?: Partial<HumanConfig> })) => {
if (!await isFrameSelectorFocused(frame, selector)) {
await frameClick(selector, options);
}
@@ -696,7 +709,15 @@ function patchSingleFrame(
await originals.keyboardPress('Backspace');
};
(frame as any).dragAndDrop = async (source: string, target: string, options?: any) => {
(frame as any).dragAndDrop = async (source: string, target: string, options?: {
force?: boolean;
noWaitAfter?: boolean;
sourcePosition?: { x: number; y: number };
strict?: boolean;
targetPosition?: { x: number; y: number };
timeout?: number;
trial?: boolean;
}) => {
const srcBox = await firstFrameLocator(frame, source).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null);
const tgtBox = await firstFrameLocator(frame, target).boundingBox({ timeout: options?.timeout ?? 30000 }).catch(() => null);
@@ -767,14 +788,14 @@ export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
}
const origNewContext = browser.newContext.bind(browser);
(browser as any).newContext = async (options?: any) => {
(browser as any).newContext = async (options?: Parameters<typeof origNewContext>[0]) => {
const context = await origNewContext(options);
patchContext(context, cfg);
return context;
};
const origNewPage = browser.newPage.bind(browser);
(browser as any).newPage = async (options?: any) => {
(browser as any).newPage = async (options?: Parameters<typeof origNewPage>[0]) => {
const page = await origNewPage(options);
if (!(page as any)._original) {
const ctx = page.context();
+21 -1
View File
@@ -172,13 +172,33 @@ export async function humanClick(
// Human idle / drift
// ---------------------------------------------------------------------------
export async function humanIdle(
export function humanIdle(
raw: RawMouse,
cx: number,
cy: number,
cfg: HumanConfig,
): Promise<void>;
export function humanIdle(
raw: RawMouse,
seconds: number,
cx: number,
cy: number,
cfg: HumanConfig,
): Promise<void>;
export async function humanIdle(
raw: RawMouse,
secondsOrCx: number,
cxOrCy: number,
cyOrCfg: number | HumanConfig,
maybeCfg?: HumanConfig,
): Promise<void> {
const hasExplicitSeconds = maybeCfg !== undefined;
const seconds = hasExplicitSeconds
? secondsOrCx
: rand((cyOrCfg as HumanConfig).idle_between_duration[0], (cyOrCfg as HumanConfig).idle_between_duration[1]);
const cx = hasExplicitSeconds ? cxOrCy : secondsOrCx;
const cy = hasExplicitSeconds ? (cyOrCfg as number) : cxOrCy;
const cfg = hasExplicitSeconds ? maybeCfg! : (cyOrCfg as HumanConfig);
const endTime = Date.now() + seconds * 1000;
let x = cx;
let y = cy;